Android NDK 导入 C库的开发流程学习;通过使用fmod的C库,实现变声器功能。

导入库文件
1)复制fmod的C库到cpp目录下
2)复制fmod的so库到jniLibs目录下
3)复制fmod的jar库到libs目录下
4)将声音文件复制到assets目录下
fmod库文件资源:https://wwgl.lanzout.com/ilXHP0pn7f4f

打开CMakeLists.txt导入fmod库文件
cmake_minimum_required(VERSION 3.10.2) # 最低支持的CMake版本
project("whine")
# TODO 第一步 导入头文件(fmod库)
include_directories("inc")
# 批量导入所有文件
file(GLOB allCPP *.c *.h *.cpp)
add_library(
native-lib # 打包出来库的名称 libnative-lib.so
SHARED # 动态库
${allCPP} # 添加所有库文件
)
# TODO 第二步 导入库文件(fmod库)
# ${CMAKE_CXX_FLAGS} 原先的CMake环境 +
# -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI} 加上新的fmod库的路径
# CMAKE_SOURCE_DIR == CMakeLists.txt所在的路径
# /../ 回退到cpp目录
# jniLibs/ 进入jniLibs目录
# ${CMAKE_ANDROID_ARCH_ABI} 使用当前手机CPU系统结构对应的库 如:arm64-v8a
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
find_library(
log-lib
log # 自动寻找log库
)
# TODO 第三步 链接具体的库,到 libnative-lib.so总库
target_link_libraries(
native-lib
${log-lib}
fmod # 具体的库 链接到 libnative-lib.so里面去
fmodL # 具体的库 链接到 libnative-lib.so里面去
)
打开build.gradle,指定CPU架构和依赖fmod的jar包
defaultConfig {
// TODO 第四步
externalNativeBuild {
cmake {
// cppFlags "" // 默认五大平台
// 指定CPU架构,Cmake的本地库, 例如:native-lib ---> arm64-v8a
abiFilters "arm64-v8a"
}
}
// TODO 第五步
// 指定CPU架构,打入APK lib/CPU平台
ndk {
abiFilters "arm64-v8a"
}
}
dependencies {
// TODO 第六步:依赖fmod的jar包
implementation files("libs\\fmod.jar");
}
准备工作完成,开始编写Activity代码
1)定义变声模式
/**
* 定义变声模式
* 0 正常; 1 萝莉; 2 大叔; 3 惊悚; 4 搞怪; 5 回声;
*/
private static final int MODE_NORMAL = 0;
private static final int MODE_LUOLI = 1;
private static final int MODE_DASHU = 2;
private static final int MODE_JINGSONG = 3;
private static final int MODE_GAOGUAI = 4;
private static final int MODE_KONGLING = 5;
2)加载编译后的lib库
static {
System.loadLibrary("native-lib");
}
3)初始化fmod.jar库,并在页面关闭的时候释放
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FMOD.init(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
FMOD.close();
}
4)编写变声native层方法和变声结束后提供native调用的方法
public native void voiceChangeNative(int modeNormal, String path);
private void playerEnd(String msg) {
isPlaying = false;
runOnUiThread(() -> Toast.makeText(this, msg, Toast.LENGTH_SHORT).show());
}
5)Activity完整代码
package com.whine;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import org.fmod.FMOD;
/**
* TODO Android NDK 导入 C库,开发流程 之 变声器
*/
public class MainActivity extends AppCompatActivity {
/**
* 定义变声模式
* 0 正常; 1 萝莉; 2 大叔; 3 惊悚; 4 搞怪; 5 回声;
*/
private static final int MODE_NORMAL = 0;
private static final int MODE_LUOLI = 1;
private static final int MODE_DASHU = 2;
private static final int MODE_JINGSONG = 3;
private static final int MODE_GAOGUAI = 4;
private static final int MODE_KONGLING = 5;
/**
* 声音路径
*/
private String soundPath;
private boolean isPlaying = false;
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// TODO 第七步 初始化FMOD库
FMOD.init(this);
initData();
}
private void initData() {
soundPath = "file:///android_asset/sound.mp3";
}
// 点击事件
public void onFix(View view) {
switch (view.getId()) {
case R.id.btn_normal:
voiceChange(MODE_NORMAL, soundPath); // 真实开发中,必须子线程 JNI线程(很多坑)
break;
case R.id.btn_luoli:
voiceChange(MODE_LUOLI, soundPath);
break;
case R.id.btn_dashu:
voiceChange(MODE_DASHU, soundPath);
break;
case R.id.btn_jingsong:
voiceChange(MODE_JINGSONG, soundPath);
break;
case R.id.btn_gaoguai:
voiceChange(MODE_GAOGUAI, soundPath);
break;
case R.id.btn_kongling:
voiceChange(MODE_KONGLING, soundPath);
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
FMOD.close();
}
private void voiceChange(int modeNormal, String path) {
if (isPlaying) {
Toast.makeText(this, "正在播放,请稍后!", Toast.LENGTH_SHORT).show();
return;
}
isPlaying = true;
new Thread(() -> voiceChangeNative(modeNormal, path)).start();
}
/**
* 给C++(native)层调用的函数
* JNI 调用 Java函数的时候,忽略掉 私有、公开 等
*
* @param msg
*/
private void playerEnd(String msg) {
isPlaying = false;
runOnUiThread(() -> Toast.makeText(this, msg, Toast.LENGTH_SHORT).show());
}
/**
* 变声native层方法
*
* @param modeNormal
* @param path
*/
public native void voiceChangeNative(int modeNormal, String path);
}
编写native层头文件(声明文件)
1)导入需要使用到的头文件
#include <jni.h>
#include <fmod.hpp>
#include <string>
#include <android/log.h>
2)定义log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
3)定义变声模式宏
#undef com_whine_MainActivity_MODE_NORMAL
#define com_whine_MainActivity_MODE_NORMAL 0L
#undef com_whine_MainActivity_MODE_LUOLI
#define com_whine_MainActivity_MODE_LUOLI 1L
#undef com_whine_MainActivity_MODE_DASHU
#define com_whine_MainActivity_MODE_DASHU 2L
#undef com_whine_MainActivity_MODE_JINGSONG
#define com_whine_MainActivity_MODE_JINGSONG 3L
#undef com_whine_MainActivity_MODE_GAOGUAI
#define com_whine_MainActivity_MODE_GAOGUAI 4L
#undef com_whine_MainActivity_MODE_KONGLING
#define com_whine_MainActivity_MODE_KONGLING 5L
4)头文件完整代码
// TODO 第八步 编写头文件(声明文件)
#include <jni.h>
// 导入FMOD的头文件
#include <fmod.hpp>
#include <string>
// 导入log库
#include <android/log.h>
extern "C" {
// log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
// 创建变声模式宏
#undef com_whine_MainActivity_MODE_NORMAL
#define com_whine_MainActivity_MODE_NORMAL 0L
#undef com_whine_MainActivity_MODE_LUOLI
#define com_whine_MainActivity_MODE_LUOLI 1L
#undef com_whine_MainActivity_MODE_DASHU
#define com_whine_MainActivity_MODE_DASHU 2L
#undef com_whine_MainActivity_MODE_JINGSONG
#define com_whine_MainActivity_MODE_JINGSONG 3L
#undef com_whine_MainActivity_MODE_GAOGUAI
#define com_whine_MainActivity_MODE_GAOGUAI 4L
#undef com_whine_MainActivity_MODE_KONGLING
#define com_whine_MainActivity_MODE_KONGLING 5L
JNIEXPORT void JNICALL Java_com_whine_MainActivity_voiceChangeNative
(JNIEnv *, jobject, jint, jstring);
}
编写native层实现文件
1)转换Java层传递的声音路径jstring -> char *
const char *pathChar = env->GetStringUTFChars(path, NULL);
2)定义和初始化音效引擎系统
// 定义音效引擎系统 指针
System *system = 0;
// 声音
Sound *sound = 0;
// 音轨
Channel *channel = 0;
// 数字信号处理 指针
DSP *dsp = 0;
// 创建系统
System_Create(&system);
// 初始化系统 参数1:最大音轨数,参数2:系统初始化标记,参数3:额外数据
system->init(32, FMOD_INIT_NORMAL, 0);
// 创建声音 参数1:路径, 参数2:声音初始化标记, 参数3:额外数据, 参数4:声音指针
system->createSound(pathChar, FMOD_DEFAULT, 0, &sound);
// 播放声音,并赋值通道 参数1:声音,参数2:分组音轨,参数3:控制,参数4:通道
system->playSound(sound, 0, false, &channel);
3)判断变声模式,调节音效
switch (mode_normal) {
case com_whine_MainActivity_MODE_NORMAL:
resultChar = "原生,播放完毕!";
break;
case com_whine_MainActivity_MODE_LUOLI:
resultChar = "萝莉,播放完毕!";
// 音调高 -- 萝莉 2.0
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 3.0f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
case com_whine_MainActivity_MODE_DASHU:
resultChar = "大叔,播放完毕!";
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节0.7
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
case com_whine_MainActivity_MODE_GAOGUAI:
resultChar = "搞怪小黄人,播放完毕!";
// 小黄人声音 频率快
// 1.从音轨拿 当前 频率
float frequency;
channel->getFrequency(&frequency);
// 2.加快频率加快 小黄人的声音
channel->setFrequency(frequency * 1.5f);
break;
case com_whine_MainActivity_MODE_JINGSONG:
resultChar = "惊悚,播放完毕!";
// 惊悚音效:特点:很多声音的拼接
// TODO 1.音调低
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节0.7
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
// TODO 2.回声
// 回音 ECHO
// 1.创建DSP类型的ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
// 2.设置回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
// 3.设置回音 衰减度 Default = 50 0 完全衰减了
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
// 4.添加音效进去 第二个音轨
channel->addDSP(1, dsp);
// TODO 3.颤抖
// Tremolo 颤抖音
// 1.创建DSP类型的Tremolo
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
// 2.设置颤抖 正常 5,非常颤抖 20
dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20);
// 3.
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f);
// 4.第三个音轨
channel->addDSP(2, dsp);
break;
case com_whine_MainActivity_MODE_KONGLING:
resultChar = "空灵,播放完毕!";
// 回音 ECHO
// 1.创建DSP类型的ECHO 音调条件
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
// 2.设置回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
// 3.设置回音 衰减度 Default = 50 0 完全衰减了
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
// 4.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
}
4)检测声音是否播放完毕
bool isPlay = true;
while (isPlay) {
// 如果真的播放完成了,音轨是知道的,内部会修改isPlayer=false
channel->isPlaying(&isPlay);
// 休眠100mS
usleep(100 * 1000);
}
5)回收C层资源
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path, pathChar);
6)播放完毕,通知java层(调用Java层的播放完成方法)
jclass jClazz = env->GetObjectClass(thiz);
jmethodID methodId = env->GetMethodID(jClazz, "playerEnd", "(Ljava/lang/String;)V");
jstring value = env->NewStringUTF(resultChar);
env->CallVoidMethod(thiz, methodId, value);
7)实现文件完整代码
#include <unistd.h>
#include "whine.h"
using namespace FMOD; // fmod的命名空间
extern "C"
JNIEXPORT void JNICALL
Java_com_whine_MainActivity_voiceChangeNative(JNIEnv *env, jobject thiz, jint mode_normal,
jstring path) {
// 定义一个结果字符串返回
char *resultChar = "播放完毕!";
// 转换路径jstring -> char *
const char *pathChar = env->GetStringUTFChars(path, NULL);
// 定义音效引擎系统 指针
System *system = 0;
// 声音
Sound *sound = 0;
// 音轨
Channel *channel = 0;
// 数字信号处理 指针
DSP *dsp = 0;
// 创建系统
System_Create(&system);
// 初始化系统 参数1:最大音轨数,参数2:系统初始化标记,参数3:额外数据
system->init(32, FMOD_INIT_NORMAL, 0);
// 创建声音 参数1:路径, 参数2:声音初始化标记, 参数3:额外数据, 参数4:声音指针
system->createSound(pathChar, FMOD_DEFAULT, 0, &sound);
// 播放声音,并赋值通道 参数1:声音,参数2:分组音轨,参数3:控制,参数4:通道
system->playSound(sound, 0, false, &channel);
LOGI("init done");
// 判断模式,增加特效
switch (mode_normal) {
case com_whine_MainActivity_MODE_NORMAL:
resultChar = "原生,播放完毕!";
break;
case com_whine_MainActivity_MODE_LUOLI:
resultChar = "萝莉,播放完毕!";
// 音调高 -- 萝莉 2.0
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 3.0f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
case com_whine_MainActivity_MODE_DASHU:
resultChar = "大叔,播放完毕!";
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节0.7
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
case com_whine_MainActivity_MODE_GAOGUAI:
resultChar = "搞怪小黄人,播放完毕!";
// 小黄人声音 频率快
// 1.从音轨拿 当前 频率
float frequency;
channel->getFrequency(&frequency);
// 2.加快频率加快 小黄人的声音
channel->setFrequency(frequency * 1.5f);
break;
case com_whine_MainActivity_MODE_JINGSONG:
resultChar = "惊悚,播放完毕!";
// 惊悚音效:特点:很多声音的拼接
// TODO 1.音调低
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节0.7
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
// TODO 2.回声
// 回音 ECHO
// 1.创建DSP类型的ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
// 2.设置回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
// 3.设置回音 衰减度 Default = 50 0 完全衰减了
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
// 4.添加音效进去 第二个音轨
channel->addDSP(1, dsp);
// TODO 3.颤抖
// Tremolo 颤抖音
// 1.创建DSP类型的Tremolo
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
// 2.设置颤抖 正常 5,非常颤抖 20
dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20);
// 3.
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f);
// 4.第三个音轨
channel->addDSP(2, dsp);
break;
case com_whine_MainActivity_MODE_KONGLING:
resultChar = "空灵,播放完毕!";
// 回音 ECHO
// 1.创建DSP类型的ECHO 音调条件
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
// 2.设置回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
// 3.设置回音 衰减度 Default = 50 0 完全衰减了
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
// 4.添加音效进去 第一个音轨
channel->addDSP(0, dsp);
break;
}
// 等待播放完毕 再回收
bool isPlay = true;
while (isPlay) {
// 如果真的播放完成了,音轨是知道的,内部会修改isPlayer=false
channel->isPlaying(&isPlay);
// 休眠100mS
usleep(100 * 1000);
}
LOGI("play done");
// 回收资源
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path, pathChar);
// 通知java层
jclass jClazz = env->GetObjectClass(thiz);
jmethodID methodId = env->GetMethodID(jClazz, "playerEnd", "(Ljava/lang/String;)V");
jstring value = env->NewStringUTF(resultChar);
env->CallVoidMethod(thiz, methodId, value);
}