NDK开发-使用FMOD进行变声

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);
}