NDK开发-使用FMOD进行变声
2022-04-28
16 min read
FMOD一款强大的语音引擎,等同于视频届的FFMPEG.可以进行音频的多种修改和设置,它有提供客户端来进行直接使用,不过今天我们通过集成API来实现几个简单的变音效果。
1.NDK配置
现在需要注意的是在新版本的AndroidStudio中已经不支持在local.properties设置中设置ndk.dir的配置来设置使用的NDK版本,如果需要配置的话通过下面的方式:
android{
ndkVersion "major.minor.build" //ndkVersion "21.3.6528147"
}
如果不指定的话那么默认采用当前AGP支持的最高版本。
导入fmod的文件,这里包括两个部分,一个是动态库(so库),一个是头文件
通过头文件我们就可以使用对应的fmod的方法了。
将fmod的inc头文件拷贝到cpp目录下,将so库拷贝到jinLibs(默认,也可以是指定的so库目录)
修改默认的CMakeLists.txt文件:
#批量导入源文件,避免还需要一个个的导入源文件
file(GLOB allCPP *.c *.h *.cpp)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
#SHARED 表示这个库是一个动态库 STATIC表示这个库是一个静态库,动态库是以.so文件结尾 静态库是以.a结尾,并且Android的话
#在打包的最后只会包含有动态库,因为如果是静态库的话那么会将对应的代码拷贝到对应的地方,也就是在运行的时候不需要静态库了,而动态库对应的都是通过地址
#来进行链接的,那么库文件还是需要的
SHARED
# Provides a relative path to your source file(s).
//注意这里的名称要和上面批量导入的名称一致
${allCPP}
)
#导入库文件,也就是三方的so,不过需要注意这个是需要在jniLibs中有对应文件夹后才能生效,不然会在编译阶段报错
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
2.添加FMOD的头文件和库
将下载的FMOD中对应的头文件文件夹inc拷贝到cpp目录下,在jniLibs中拷贝需要使用到的的架构目录。
在CMakeLists.txt中添加配置信息:
#批量导入头文件,注意对应的目录,当前的根目录是CMakeLists.txt所在的目录
include_directories("inc")
#链接对应的库文件
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib}
fmod
fmodL
)
完整的CMakeLists.txt文件:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Declares and names the project.
project("ndktest")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#批量导入头文件
include_directories("inc")
#批量导入源文件
file(GLOB allCPP *.c *.h *.cpp)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${allCPP}
)
#导入库文件,也就是三方的so
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib}
fmod
fmodL
)
3.准备需要编辑的音频文件
这里我直接准备了一个mp3文件来进行编辑,当然也可以进行录制后编辑。在assets目录中放入mp3文件,这里为了方便操作,使用okio将文件拷贝到cache目录操作,这样也不需要进行权限的申请。
implementation 'com.squareup.okio:okio:2.10.0'
拷贝文件:
private fun copyFile(): String {
//将assets目录下的文件拷贝到应用的cache目录下,这里的source是okio的扩展方法
val targetFile = File(this.cacheDir, "test.mp3")
//使用use来自动关闭实现Closeable的对象
this.assets.open("test.mp3").source().use { source ->
targetFile.sink().buffer().use {
it.writeAll(source)
}
}
return targetFile.path;
}
4.使用NDK实现对应的方法调用
#include <jni.h>
#include <string>
#include <fmod.hpp>
#include "Student.h"
using namespace std;
using namespace FMOD;
//android的log打印需要导入的头文件P
#include <android/log.h>
#include <unistd.h>
#include <pthread.h>
//宏定义
#define LOG_TAG "ARM_NDK"
//...的参数由__VA_ARGS__来进行确认
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__);
#undef MODE_NORMAL
#define MODE_NORMAL 0
#undef MODE_FUNNY
#define MODE_FUNNY 1
#undef MODE_UNCLE
#define MODE_UNCLE 2
#undef MODE_LOLITA
#define MODE_LOLITA 3
#undef MODE_ROBOT
#define MODE_ROBOT 4
#undef MODE_ETHEREAL
#define MODE_ETHEREAL 5
#undef MODE_CHORUS
#define MODE_CHORUS 6
#undef MODE_HORROR
#define MODE_HORROR 7
extern "C" {
//因为cpp是C++,如果要导入c的代码,需要使用c的语言环境
};
extern "C" JNIEXPORT
jstring JNICALL
Java_com_arms_ndktest_MainActivity_stringFromJNI(
JNIEnv
*env,
jobject /* this */) {
string hello = "Hello from C++";
LOGE("测试打印");
auto *student = new Student("haha", 21);
const char *str = student->getName().c_str();
LOGE("%s", str);
delete student;
return env->
NewStringUTF(hello.c_str());
}
//没有返回值的JNI方法,非静态方法,非静态方法的话这里是jobject
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_MainActivity_testNoReturnFromJNI(JNIEnv
*env,
jobject thiz
) {
//如果是从java层传入的数组,那么直接修改操作只会在c++层生效,不会改变java层的数据
jintArray jintArray1;
int length = env->GetArrayLength(jintArray1);
int *pInt = env->GetIntArrayElements(jintArray1, NULL);
for (int i = 0; i < length; i++) {
*(pInt + i) += 100;
}
//通过下面的操作杆来达到修改对应java的数据,mode的模式0 刷新java数组,释放C++层数组 JNI_COMMIT:只提交只刷新Java数组,不释放C++,JNI_ABORT:只释放C++层数据
env->ReleaseIntArrayElements(jintArray1, pInt, 0);
}
//一个测试方法,使用c++来构建一个java的对象,并设置其中的参数
void test(JNIEnv *env) {
const char *className = "com/arms/ndktest/PeopleBean";
jclass pJclass = env->FindClass(className);
//这种方式不会调用构造方法
jobject pJobject = env->AllocObject(pJclass);
//这种方式,说白了就是要传入一个方法,这个方法就是构造方法从而来执行指定的构造方法
jmethodID pJmethodId = env->GetMethodID(pJclass, "<init>", "()V");
jobject object = env->NewObject(pJclass, pJmethodId);
jmethodID setNameMethod = env->GetMethodID(pJclass, "setName", "(Ljava/lang/String;)V");
jmethodID setAageMethod = env->GetMethodID(pJclass, "setAge", "(I)V");
jstring nameValue = env->NewStringUTF("123");
env->CallVoidMethod(pJobject, setNameMethod, nameValue);
env->CallVoidMethod(pJobject, setAageMethod, 11);
env->DeleteLocalRef(pJclass);
env->DeleteLocalRef(pJobject);
}
/**
最开始在MainActivity中来实现对应的方法,出入参数为播放的模式以及对应的音频文件路径
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_MainActivity_voiceChangeNative(JNIEnv *env, jobject thiz, jint mode,
jstring path) {
char *content_ = "播放完毕";
const char *path_ = env->GetStringUTFChars(path, NULL);
LOGE("mode:%d", mode);
System *system = 0;
Sound *sound = 0;
Channel *channel = 0;
DSP *dsp = 0;
System_Create(&system);
system->init(32, FMOD_INIT_NORMAL, 0);
system->createSound(path_, FMOD_DEFAULT, 0, &sound);
system->playSound(sound, 0, false, &channel);
switch (mode) {
case 1:
content_ = "原声播放";
break;
case 2:
content_ = "萝莉播放";
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);
channel->addDSP(0, dsp);
break;
case 3:
content_ = "大叔播放";
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 音轨
channel->addDSP(0, dsp);
break;
case 4:
content_ = "搞怪播放";
// 小黄人声音 频率快
// 从音轨拿 当前 频率
float mFrequency;
channel->getFrequency(&mFrequency);
// 修改频率
channel->setFrequency(mFrequency * 1.5f); // 频率加快 小黄人的声音
break;
case 5:
content_ = "惊悚播放";
// TODO 音调低
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 音轨
channel->addDSP(0, dsp); // 第一个音轨
// TODO 搞点回声
// 回音 ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50 0 完全衰减了
channel->addDSP(1, dsp); // 第二个音轨
// TODO 颤抖
// Tremolo 颤抖音 正常5 非常颤抖 20
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 6:
content_ = "空灵播放";
// 回音 ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50 0 完全衰减了
channel->addDSP(0, dsp);
break;
}
bool isPlayer = true;
while (isPlayer) {
channel->isPlaying(&isPlayer);
usleep(1000 * 1000);
}
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path, path_);
}
Channel *channel = 0;
extern "C"
JNIEXPORT jint JNICALL
Java_com_arms_ndktest_FmodSound_saveSound(JNIEnv *env, jobject cls, jstring path_jstr, jint type,
jstring save_jstr) {
Sound *sound;
DSP *dsp;
bool playing = true;
float frequency = 0;
System *mSystem;
JNIEnv *mEnv = env;
int code = 0;
System_Create(&mSystem);
const char *path_cstr = mEnv->GetStringUTFChars(path_jstr, NULL);
LOGI("saveAiSound-%s", path_cstr);
const char *save_cstr;
if (save_jstr != NULL) {
save_cstr = mEnv->GetStringUTFChars(save_jstr, NULL);
LOGI("saveAiSound-save_path=%s", save_cstr)
}
try {
if (save_jstr != NULL) {
char cDest[200];
strcpy(cDest, save_cstr);
mSystem->setSoftwareFormat(8000, FMOD_SPEAKERMODE_MONO, 0); //设置采样率为8000,channel为1
mSystem->setOutput(FMOD_OUTPUTTYPE_WAVWRITER); //保存文件格式为WAV
mSystem->init(32, FMOD_INIT_NORMAL, cDest);
mSystem->recordStart(0, sound, true);
}
//创建声音
mSystem->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);
mSystem->playSound(sound, 0, false, &channel);
LOGI("saveAiSound-%s", "save_start")
switch (type) {
case MODE_NORMAL:
LOGI("saveAiSound-%s", "save MODE_NORMAL");
break;
case MODE_FUNNY:
LOGI("saveAiSound-%s", "save MODE_FUNNY")
mSystem->createDSPByType(FMOD_DSP_TYPE_NORMALIZE, &dsp);
channel->getFrequency(&frequency);
frequency = frequency * 1.6;
channel->setFrequency(frequency);
break;
case MODE_UNCLE:
LOGI("saveAiSound-%s", "save MODE_UNCLE")
mSystem->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.8);
channel->addDSP(0, dsp);
break;
case MODE_LOLITA:
LOGI("saveAiSound-%s", "save MODE_LOLITA")
mSystem->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 1.8);
channel->addDSP(0, dsp);
break;
case MODE_ROBOT:
LOGI("saveAiSound-%s", "save MODE_ROBOT")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 50);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 60);
channel->addDSP(0, dsp);
break;
case MODE_ETHEREAL:
LOGI("saveAiSound-%s", "save MODE_ETHEREAL")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 300);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 20);
channel->addDSP(0, dsp);
break;
case MODE_CHORUS:
LOGI("saveAiSound-%s", "save MODE_CHORUS")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 100);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 50);
channel->addDSP(0, dsp);
break;
case MODE_HORROR:
LOGI("saveAiSound-%s", "save MODE_HORROR")
mSystem->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8);
channel->addDSP(0, dsp);
break;
default:
break;
}
mSystem->update();
} catch (...) {
LOGE("saveAiSound-%s", "save error!")
code = 1;
goto end;
}
while (playing) {
usleep(1000);
channel->isPlaying(&playing);
}
LOGI("saveAiSound-%s", "save over!")
goto end;
end:
if (path_jstr != NULL) {
mEnv->ReleaseStringUTFChars(path_jstr, path_cstr);
}
if (save_jstr != NULL) {
mEnv->ReleaseStringUTFChars(save_jstr, save_cstr);
}
sound->release();
mSystem->close();
mSystem->release();
return code;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_arms_ndktest_FmodSound_playSound(JNIEnv *env, jobject thiz, jstring path, jint type) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_FmodSound_stopPlay(JNIEnv *env, jobject thiz) {
channel->stop();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_FmodSound_resumePlay(JNIEnv *env, jobject thiz) {
channel->stop();
//检测当前的env执行过程是否有异常发生
jthrowable pJthrowable = env->ExceptionOccurred();
if (pJthrowable) {
//主动清除异常
env->ExceptionClear();
//JNI中主动抛出异常
jclass throwClass = env->FindClass("java/lang/NoSuchFieldError");
env->ThrowNew(throwClass, "dfsdf");
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_FmodSound_pausePlay(JNIEnv *env, jobject thiz) {
channel->stop();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_arms_ndktest_FmodSound_isPlaying(JNIEnv *env, jobject thiz) {
bool isPlaying = false;
channel->isPlaying(&isPlaying);
return isPlaying;
}
JavaVM *javaVm = nullptr;
/**
* JNIEnv *env 不能跨越线程,可以跨越函数 解决方案通过JavaVM.attchCurrentThread获取子线程的JNIEnv
* jobject thiz 不能跨越线程和函数 这个通过提升为全局变量来解决
* JavaVM *javaVm 可以跨越线程和函数
*
* 动态注册的代码
* @param javaVm
* @return
*/
JNIEXPORT jint JNI_OnLoad(JavaVM *javaVm, void *) {
//::javaVm = javaVm;
JNIEnv *jniEnv = nullptr;
int result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
if (result != JNI_OK) {
return -1;
}
return JNI_VERSION_1_6;
}
class MyContext {
public:
JNIEnv *jniEnv = nullptr;
jobject instance = nullptr;
};
void *myThreadTaskAction(void *pVoid) {
//这个函数中指针运行在子线程中
MyContext *myContext = static_cast<MyContext *>(pVoid);
JNIEnv *jniEnv = nullptr;
//通过JavaVM.attchCurrentThread获取子线程的JNIEnv
jint result = ::javaVm->AttachCurrentThread(&jniEnv, nullptr);
if (result != JNI_OK) {
return 0;
}
jclass activityClass = jniEnv->GetObjectClass(myContext->instance);
jmethodID methodId = jniEnv->GetMethodID(activityClass, "showAlter", "()V");
jniEnv->CallVoidMethod(myContext->instance, methodId);
::javaVm->DetachCurrentThread();
return nullptr;
}
/**
* 在C++的子线程中回掉Java层代码
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_arms_ndktest_MainActivity_callJNIAsync(JNIEnv *env, jobject thiz) {
env->GetJavaVM(&::javaVm);
auto *myContext = new MyContext;
myContext->jniEnv = env;
//jobject thiz 不能跨越线程和函数 这个通过提升为全局变量来解决
myContext->instance = env->NewGlobalRef(thiz);
pthread_t pid;
//c中的创建子线程
pthread_create(&pid, nullptr, myThreadTaskAction, myContext);
pthread_join(pid, nullptr);
}