NDK 入门(二)—— 调音小项目

NDK 入门系列主要介绍 JNI 的相关内容,目录如下:

NDK 入门(一)—— JNI 初探
NDK 入门(二)—— 调音小项目
NDK 入门(三)—— JNI 注册与 JNI 线程
NDK 入门(四)—— 静态缓存与 Native 异常

前面介绍了很多 C/C++ 以及 JNI 相关的理论知识,本篇我们来实践一下,实现一个调音小 Demo。这个 Demo 需要用到一个专业的调音软件 FMOD 及其 API,先了解一下 FMOD。

1、FMOD 介绍

FMOD Ex 声音系统是为游戏开发者准备的革命性音频引擎,像 cocos2d,unity3d 游戏引擎默认就集成了 fmod。如今采用了 FMOD 作为音频引擎的游戏包括 Far Cry(孤岛惊魂)、Tom Clancy's Ghost Recon(幽灵行动),甚至著名的 World Of Warcraft(魔兽争霸)。

1.1 FMOD Studio

你可以在 FMOD 官网 下载 FMOD Studio 开始音频创作,该软件对非商业用途免费。

请添加图片描述

你可以对一段音频从音调、音色等多个方面添加特效以达到想要的特效。上图中就是对音调 PITCH 进行修改,从 0.0 ~ 2.0 有截然不同的效果,后续的 Demo 就会通过对 PITCH 设置不同的值以达到不同角色说话的效果。

1.2 FMOD API

FMOD 音效引擎库是用 C/C++ 写出来的,并且支持 Android 平台在内的多个平台,我们可以在 FMOD 官网 下载 FMOD Engine,将其 API 代码拷贝到我们的 Demo 中编译使用:

请添加图片描述

2、项目配置

主要是对 CMakeLists.txt 和模块的 build.gradle 进行配置,我们一步一步来。

2.1 拷贝 FMOD API 并配置 CMake

主要是拷贝 FMOD API 的头文件和库文件到我们的 Demo 中,并且配置 CMakeLists.txt 文件。步骤如下:

  1. 导入头文件,将 fmodstudioapi20219android\api\core\inc 目录下的头文件拷贝到项目 cpp\fmod\inc 目录下,在 CMakeLists.txt 中添加这些头文件:

    # 导入头文件
    include_directories(fmod/inc)
    
  2. 将 \fmod\inc 下的文件添加到 audio 动态库中:

    # 将所有 .c .h .cpp 文件都存入 allSources 这个对象中
    file(GLOB allSources *.c *.cpp)
    # 将 allSources 表示的文件全加入 audio 动态库中
    add_library(audio SHARED ${allSources})
    
  3. 导入库文件,将 fmodstudioapi20219android\api\core\lib 下所需平台的动态库文件拷贝到 app\main\jniLibs 目录下,并将该路径追加到 CMAKE_CXX_FLAGS 这个 C++ 编译器标志中:

    # ${CMAKE_CXX_FLAGS} 表示在 CMAKE_CXX_FLAGS 的原值上追加 -L
    # 所表示的路径:CMakeLists 所在的路径,向上一级到了 main,然后
    # 是 main/jniLibs 下 CPU 平台的文件夹
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
    

    注意 FMOD 提供了四个平台:arm64-v8a、armeabi-v7a、x86、x86_64 的动态库,你只需将需要的动态库拷贝到项目中就好,全都拷贝会增大 APK 体积

  4. 将 FMOD 动态库链接到目标库。第 3 步的动态库文件有两个,分别是 libfmod.so 和 libfmodL.so,需要将这两个库链接到目标库,也就是我们产出的 audio 库:

    # audio 是目标库,fmod 和 fmodL 分别是 libfmod 和 
    # libfmodL 去掉 lib 前缀的名称
    target_link_libraries( audio ${log-lib}fmodfmodL)
    

关于以上步骤,有几点要说明的:

  • 通过 adb 命令查看自己手机的 CPU 是哪个平台的架构:

    PS root> adb shell getprop ro.product.cpu.abi
    x86_64
    
  • 平台库要默认添加到 /app/src/main/jniLibs 目录下,因为 jniLibs 是 Gradle 默认的 C/C++ 的库目录,如果想修改,可以在 build.gradle 中修改源集:

    android {// ...sourceSets {main {jniLibs.srcDirs = ['myLibs']}}// ...
    }
    

有关在 AS 中对 CMake 配置的完整教程,可以参考官网教程配置 CMake。

2.2 拷贝 fmod.jar 与 gradle 配置

我们还需将 fmodstudioapi20219android\api\core\lib 下的 fmod.jar 拷贝到 app\libs 目录下,然后在 app 模块的 build.gradle 中依赖它:

dependencies {implementation files('libs\\fmod.jar')
}

此外,我们还需配置 abiFilters:

android {defaultConfig {ndk {// 仅在 64 位模拟器上运行,因此只打包该平台abiFilters "x86_64"}}

由于我只在模拟器上运行 Demo,所以只给 abiFilters 配置了 x86_64,这样 gradle 在编译打包时,就只会将 x86_64 目录下的库文件打包进 APK 中,避免默认情况下,gradle 将 jniLibs 下所有平台的库都打包进 APK,使 APK 体积无谓的增大。

到这里基本上就配置完成了,文件结构如下:

请添加图片描述

3、功能实现

实现效果就是点击 UI 上的按钮,然后播放相应效果的音频。原声资源文件如上图可见,存放在 src/assets 目录下。UI 效果:

请添加图片描述

那么编码就分为 Java 与 Native 两个部分。

3.1 Java 源码与 Java 头文件

Java 部分代码首先要导入动态库 audio 并且定义播放音效的 Native 方法:

	static {System.loadLibrary("audio");}// mode 表示采用哪种音效,audioPath 是原声文件路径private native void nativeChangeVoice(int mode, String audioPath);

然后定义模式常量与路径:

	// 正常private static final int MODE_NORMAL = 0;// 萝莉private static final int MODE_LOLITA = 1;// 大叔private static final int MODE_UNCLE = 2;// 惊悚private static final int MODE_THRILLER = 3;// 搞怪private static final int MODE_QUIRKY = 4;// 空灵private static final int MODE_ETHEREAL = 5;private static final String ORIGIN_AUTO_PATH = "file:///android_asset/daxian.mp3";

FMOD 需要初始化才可使用,在 Activity 销毁时要反注册 FMOD:

	@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityMainBinding.inflate(LayoutInflater.from(this));setContentView(mBinding.getRoot());initButtonListeners();// 初始化 FMODFMOD.init(this);}@Overrideprotected void onDestroy() {super.onDestroy();// 解除 FMOD 的 Receiver 注册FMOD.close();}

功能代码就是给按钮设置监听器,点击后就调用 Native 方法传入对应的音效。注意,这个 Native 方法是一个耗时方法,因此要放在子线程中调用:

	// 处理 Native 方法的线程池private ExecutorService mExecutorService = new ThreadPoolExecutor(1, 6, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread t = Executors.defaultThreadFactory().newThread(r);t.setName("ThreadPool-" + t.getId());return t;}});private void initButtonListeners() {// native 进行的音频处理操作一定要放在子线程中执行mBinding.btnNormal.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_NORMAL, ORIGIN_AUTO_PATH)));mBinding.btnLuoli.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_LOLITA, ORIGIN_AUTO_PATH)));mBinding.btnDashu.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_UNCLE, ORIGIN_AUTO_PATH)));mBinding.btnJingsong.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_THRILLER, ORIGIN_AUTO_PATH)));mBinding.btnGaoguai.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_QUIRKY, ORIGIN_AUTO_PATH)));mBinding.btnKongling.setOnClickListener(v -> mExecutorService.submit(() ->nativeChangeVoice(MODE_ETHEREAL, ORIGIN_AUTO_PATH)));}

最后写一个通知方法,弹出 Toast 告知用户音频已经播放完,这个方法供 Native 调用:

	// 供 C++ 展示音频播放完毕的方法。由于 JNI 调用 Java 方法时会忽略掉方法的// 可见度,因此这里使用 private 并不影响 JNI 调用该方法private void playFinish(String msg) {// 因为调用 nativeChangeVoice() 是在线程池中进行的,所以 native 层调用// 本方法也是在子线程中,需要切换到主线程进行 UI 操作new Handler(getMainLooper()).post(() ->Toast.makeText(this, msg, Toast.LENGTH_SHORT).show());}

以上是 Java 代码。接下来要根据这个源文件生成对应的 JNI 的头文件。在 src\main\java 目录下运行命令:

javah com.jni.lesson3.MainActivity

会在该目录下生成一个头文件 com_jni_lesson3_MainActivity.h,里面有声明的常量和 Native 方法:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_lesson3_MainActivity */#ifndef _Included_com_jni_lesson3_MainActivity
#define _Included_com_jni_lesson3_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
#undef com_jni_lesson3_MainActivity_MODE_NORMAL
#define com_jni_lesson3_MainActivity_MODE_NORMAL 0L
#undef com_jni_lesson3_MainActivity_MODE_LOLITA
#define com_jni_lesson3_MainActivity_MODE_LOLITA 1L
#undef com_jni_lesson3_MainActivity_MODE_UNCLE
#define com_jni_lesson3_MainActivity_MODE_UNCLE 2L
#undef com_jni_lesson3_MainActivity_MODE_THRILLER
#define com_jni_lesson3_MainActivity_MODE_THRILLER 3L
#undef com_jni_lesson3_MainActivity_MODE_QUIRKY
#define com_jni_lesson3_MainActivity_MODE_QUIRKY 4L
#undef com_jni_lesson3_MainActivity_MODE_ETHEREAL
#define com_jni_lesson3_MainActivity_MODE_ETHEREAL 5L
/** Class:     com_jni_lesson3_MainActivity* Method:    nativeChangeVoice* Signature: (ILjava/lang/String;)V*/
JNIEXPORT void JNICALL Java_com_jni_lesson3_MainActivity_nativeChangeVoice(JNIEnv *, jobject, jint, jstring);#ifdef __cplusplus
}
#endif
#endif

将该文件拷贝到 src\main\cpp 目录下备用。

之所以没用 Kotlin 写上层的代码就是因为 javah 无法直接通过 kt 源代码生成头文件,而使用 kt 直接生成头文件的方法暂时还没跑通。

3.2 Native 播放音频并添加特效

在 native-lib.cpp 中,先要导入 fmod.hpp 和 com_jni_lesson3_MainActivity.h 这两个头文件,然后声明 FMOD 的命名空间,再开始实现 Native 方法:

#include <jni.h>
#include <string>
#include <fmod.hpp> // 导入 fmod 头文件
#include <unistd.h>
#include "com_jni_lesson3_MainActivity.h"// 声明 fmod 的命名空间
using namespace FMOD;extern "C"
JNIEXPORT void JNICALL
Java_com_jni_lesson3_MainActivity_nativeChangeVoice(JNIEnv *env, jobject thiz, jint mode,jstring _path) {// C++11 将字符串字面量视为常量,因此不可以写 // char *resultString = "xxx",需要在其前面加 const 形成// 常量指针,或者使用 std 空间的 string 也是可以的:
//    std::string result = "默认音效播放完毕";const char *resultString = "默认音效播放完毕";const char *path = env->GetStringUTFChars(_path, nullptr);/** 1.初始化 FMOD 组件*/// 音效引擎系统System *system = nullptr;System_Create(&system);// 三个参数依次为:最大音轨数、系统初始化标记、额外的驱动数据system->init(32, FMOD_INIT_NORMAL, nullptr);// 声音Sound *sound = nullptr;// 四个参数依次为:音频文件路径、声音初始化标记、额外信息、声音对象的二级指针system->createSound(path, FMOD_DEFAULT, nullptr, &sound);// 音轨(声音播放的通道)Channel *channel = nullptr;// DSP:digital signal process 数字信号处理DSP *dsp = nullptr;/** 2.播放声音*/// 四个参数依次为:声音、分组音轨、是否暂停、通道system->playSound(sound, nullptr, false, &channel);/** 3.增加特效*/switch (mode) {case com_jni_lesson3_MainActivity_MODE_NORMAL:resultString = "原声播放完毕";break;case com_jni_lesson3_MainActivity_MODE_LOLITA:resultString = "萝莉播放完毕";// 创建 DSP 类型的 Pitch,这是音调system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);// 将音调 Pitch 调节到 2.0dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);// 添加音效到音轨中channel->addDSP(0, dsp);break;case com_jni_lesson3_MainActivity_MODE_UNCLE:resultString = "大叔播放完毕";// 与上一个 case 类似,将音调调至 0.7system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);channel->addDSP(0, dsp);break;case com_jni_lesson3_MainActivity_MODE_QUIRKY:resultString = "搞怪小黄人播放完毕";// 从音轨获取频率并加快至 1.5 倍float frequency;channel->getFrequency(&frequency);channel->setFrequency(frequency * 1.5f);break;case com_jni_lesson3_MainActivity_MODE_THRILLER:resultString = "惊悚播放完毕";/** 调整三个方面的特效:低音调 + 回声 + 颤抖,放在三个音轨上*/// 1.音调调至 0.7system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);channel->addDSP(0, dsp);// 2.回声system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);// 设置回声延时为 200 ms,默认 500 msdsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);// 设置回声衰减度为 10,默认 50dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);channel->addDSP(1, dsp);// 3.颤抖音system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20);dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f);channel->addDSP(2, dsp);break;case com_jni_lesson3_MainActivity_MODE_ETHEREAL:resultString = "空灵播放完毕";system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);channel->addDSP(0, dsp);break;}/** 4.播放完毕后进行回收操作并通知 Java 层*/bool isPlaying = true;while (isPlaying) {// 播放完成后会把 isPlaying 修改为 falsechannel->isPlaying(&isPlaying);// 休眠 1 秒,usleep 单位是微秒usleep(1000 * 1000);}// 回收 JNI 对象sound->release();system->close();system->release();env->ReleaseStringUTFChars(_path, path);// 通知 Java 播放完毕jclass javaClass = env->GetObjectClass(thiz);jmethodID playFinishID = env->GetMethodID(javaClass, "playFinish", "(Ljava/lang/String;)V");jstring message = env->NewStringUTF(resultString);env->CallVoidMethod(thiz, playFinishID, message);
}

主要步骤分为 4 大步,已经在注释中详细说明了,基本上就是通过 dsp 对象调节音调、延迟、衰减度、回声等特效实现不同的效果。当然我们不是专业的调音师,调教出来的效果只能说是马马虎虎了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4889.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字滤波器设计笔记1

系统结构 1.先利用matlab的simulink和FDA进行滤波器建模设计&#xff0c;通过仿真后&#xff0c;确定模型达到相应的性能要求&#xff0c;再利用verilog进行电路设计。最后使用modelsim进行功能验证。其中testbench的输入数据&#xff0c;利用matlab模型的输入数据。 2.Matlab…

最最普通程序员,如何利用工资攒够彩礼,成为人生赢家

今天我们不讲如何提升你的专业技能去涨工资&#xff0c;不讲面试技巧如何跳槽涨工资&#xff0c;不讲如何干兼职赚人生第一桶金&#xff0c;就讲一个最最普通的程序员&#xff0c;如何在工作几年后&#xff0c;可以攒够彩礼钱&#xff0c;婚礼酒席钱&#xff0c;在自己人生大事…

pytho爬取南京房源成交价信息并导入到excel

# encoding: utf-8 # File_name: import requests from bs4 import BeautifulSoup import xlrd #导入xlrd库 import pandas as pd import openpyxl# 定义函数来获取南京最新的二手房房子成交价 def get_nanjing_latest_second_hand_prices():cookies {select_city: 320100,li…

信息系统项目管理师——第5章信息系统工程(一)

近几期的考情来看&#xff0c;本章选择题稳定考4分&#xff0c;考案例的可能性有&#xff0c;需要重点学习。本章节专业知识点特别多。但是&#xff0c;只考课本原话&#xff0c;大家一定要把本章至少通读一遍&#xff0c;还要多刷题&#xff0c;巩固重点知识。 1 软件工程 软…

deepin 开源之夏重磅来袭!超优质项目已上线,欢迎来战

内容来源&#xff1a;deepin 社区 「开源之夏」是由中国科学院软件研究所“开源软件供应链点亮计划”发起并长期支持的一项暑期开源活动&#xff0c;旨在鼓励在校学生积极参与开源软件的开发维护&#xff0c;培养和发掘更多优秀的开发者&#xff0c;促进优秀开源软件社区的蓬勃…

javamail发送qq邮箱失败案例分析

文章目录 javaMail报错:Unsupported or unrecognized SSL message原因分析: ssl与tls端口总结 javaMail报错:Unsupported or unrecognized SSL message c.n.m.service.impl.EmailServiceImpl : 邮件发送异常, Mail server connection failed; nested exception is javax.m…

Spring AI 来啦,快速上手

Spring AI Spring框架在软件开发领域&#xff0c;特别是在Java企业级应用中&#xff0c;一直扮演着举足轻重的角色。它以其强大的功能和灵活的架构&#xff0c;帮助开发者高效构建复杂的应用程序。而Spring Boot的推出&#xff0c;更是简化了新Spring应用的初始搭建和开发过程…

【分治算法】【Python实现】棋盘覆盖

文章目录 [toc]问题描述分治算法时间复杂性Python实现 个人主页&#xff1a;丷从心 系列专栏&#xff1a;分治算法 学习指南&#xff1a;Python学习指南 问题描述 在一个 2 k 2 k 2^{k} \times 2^{k} 2k2k个方格组成的棋盘中&#xff0c;若恰有一个方格与其他方格不同&…

httpClient提交报文中文乱码

httpClient提交中文乱码&#xff0c;ContentType类型application/json 指定提交参数的编码即可 StringEntity se new StringEntity(paramBody.toJSONString(),"UTF-8");se.setContentType("application/json");context.httpPost.setHeader("Cookie&…

JUC并发-共享模型-无锁-乐观锁(非阻塞)

1、问题提出 有如下需求&#xff0c;保证 account.withdraw 取款方法的线程安全 public class TestAccount {public static void main(String[] args) {Account account new AccountCas(10000);Account.demo(account);} }class AccountUnsafe implements Account {private I…

2024LarkXR新增功能系列之五 | 单端口支持多并发

实时云渲染技术在为虚拟现实、游戏、和各种应用程序提供强大的渲染支持的同时&#xff0c;也带来了一些网络和运维上的挑战。在传统的设置中&#xff0c;实时云渲染推流技术需要为每个视频流单独占用服务器的一个端口。这种方法在多用户同时访问的情况下可能会导致端口资源的快…

MemFire解决方案-物联网数据平台解决方案

方案背景 随着各种通讯、传感技术发展&#xff0c;数据通讯成本的急剧下降&#xff0c;数以万亿计的智能设备&#xff08;智能手环、智能电表、智能手机、各种传感器设备等&#xff09;接入网络&#xff0c;并源源不断的产生海量的实时数据。这些海量数据的价值挖掘&#xff0…

Node私库Verdaccio使用记录,包的构建,推送和拉取

Node私库Verdaccio使用记录&#xff0c;包的构建&#xff0c;推送和拉取 Verdaccio是一个轻量级的私有npm代理注册中心&#xff0c;它可以帮助你在本地搭建一个npm仓库&#xff0c;非常适合企业内部使用。通过使用Verdaccio&#xff0c;你可以控制和缓存依赖包&#xff0c;提高…

边OTG边充电芯片LDR6500

随着科技的飞速发展&#xff0c;智能移动设备已成为我们生活中不可或缺的一部分。而在这些设备的连接与数据传输中&#xff0c;Type-C接口以其高效、便捷的特性逐渐占据了主导地位。OTG&#xff08;On-The-Go&#xff09;技术则进一步扩展了Type-C接口的功能&#xff0c;使得设…

构建安全高效的数字货币钱包:开发指南

在加密货币领域的蓬勃发展中&#xff0c;数字货币钱包成为了连接用户与区块链的重要桥梁。作为存储、发送和接收加密资产的工具&#xff0c;数字货币钱包的安全性和效率至关重要。本文将介绍如何构建一个安全高效的数字货币钱包&#xff0c;并提供开发指南&#xff0c;帮助开发…

2024中国(江西)国际先进陶瓷材料及智能装备博览会

2024中国&#xff08;江西&#xff09;国际先进陶瓷材料及智能装备博览会 “中国&#xff08;江西&#xff09;国际先进陶瓷材料及智能装备博览会” 陶瓷三新展 &#xff08;新材料、新装备、新技术&#xff09; 绿色智能、引领未来 2024年11月1日-11月3日 中国江西 南昌…

深度学习系列64:数字人wav2lip详解

1. 整体流程 第一步&#xff0c;加载视频/图片和音频/tts。用melspectrogram将wav文件拆分成mel_chunks。 第二步&#xff0c;调用face_detect模型&#xff0c;给出人脸检测结果&#xff08;可以改造成从文件中读取&#xff09;&#xff0c;包装成4个数组batch&#xff1a;img…

html显示PDF并兼容IE浏览器的解决方案

方案一、vue-pdf插件 缺点&#xff1a;IE11显示空白&#xff0c;编译后的Edge测试环境可以正常线上&#xff0c;打到线上报错&#xff0c;谷歌和百分浏览器显示完美 1、vue 只显示核心代码&#xff0c;需要安装vue-pdf插件 <vue-pdf :src"ivcPdfUrl"></v…

Spring声明式事务(@Transactional)原理之-ProxyTransactionManagementConfiguration

文章目录 目录 文章目录 前言 一、切入点以及切面的匹配规则 1.1 TransactionAttributeSourcePointcut事务的切入点匹配 二、TransactionInterceptor切面的具体逻辑 2.1 声明式事务实现的具体逻辑 总结 前言 上一篇文章已经说过了声明式事务的原理其实就是SpringAop动态…

C语言基础:初识指针(二)

当你不知道指针变量初始化什么时&#xff0c;可以初始化为空指针 int *pNULL; 我们看NULL的定义&#xff0c;可以看出NULL是0被强制转化为Void* 类型的0&#xff1b;实质还是个0&#xff1b; 如何避免野指针&#xff1a; 1. 指针初始化 2. 小心指针越界 3. 指针指向空间…