NDK学习笔记:一起来变萝莉音!FMOD学习总结(下)
一、创建自己的变音demo
上一节我已经能够在AndroidStudio上跑起了fmod的基础教程。还有疑问的同学可以
重新阅读跟着来跑一次。这章节计划参照官方的play_sound.cpp + effect.cpp,实现类似变音器的效果。并且带大家熟悉AS的NDK代码的编写流程,毕竟往下的计划文章都慢慢的向NDK靠近,所以希望同学们都能熟练掌握这一块。
事不宜迟,开始撸码。
首先我们从java层入手,创建EffectActivity,跟随页面的生命周期,记得进行java.FMOD的初始化/关闭工作。代码如下:
public class EffectActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fmod_effect);
org.fmod.FMOD.init(this); // 初始化java.FMOD,全局引用Context = this;
}
@Override
protected void onDestroy() {
super.onDestroy();
org.fmod.FMOD.close(); // 回收java.FMOD,全局引用Context = null;
}
}
UI 效果图就不截取了,就是六个按钮,分别对应6种声效:原声、萝莉、大叔、惊悚、搞怪、幽灵。
入口Activity的准备工作做好之后,我们就可以开始音效NDK方法的入口编写了。创建工具类VoiceEffectUtils,如下图所示:
可以看到工具类定义6种类型,三个native方法,但是方法出现错误,编译器提示我们找不到jni方法的native声明与实现。我们在项目目录的/src/main/cpp/fmod/ 创建 effect_sound.cpp,这样就能让编译器找到native方法的实现。
#include
JNIEnv *gJNIEnv;
extern "C"
{
void Java_org_zzrblog_fmod_VoiceEffectUtils_init(JNIEnv *env, jclass clazz )
{
gJNIEnv = env;
}
void Java_org_zzrblog_fmod_VoiceEffectUtils_play(JNIEnv *env, jclass clazz, jint type )
{
}
void Java_org_zzrblog_fmod_VoiceEffectUtils_release(JNIEnv *env, jclass clazz )
{
gJNIEnv = NULL;
}
} /* extern "C" */
全局初始化释放方法暂时不知道要做什么,来一习惯性操作,暂存JNIEnv指针的地址,释放方法置空。
至此,NDK基本构建完成,可以开始往里面进行方法的实现了。
二、实现play方法
首先还是从java层入手,根据页面生命周期进行VoiceEffectUtils的初始化和释放的操作,然后把UI的六个按钮分别都响应play的六种声效类型。EffectActivity的代码修改如下:
public class EffectActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fmod_effect);
org.fmod.FMOD.init(this);
VoiceEffectUtils.init();
}
@Override
protected void onDestroy() {
super.onDestroy();
VoiceEffectUtils.release();
org.fmod.FMOD.close();
}
public void onClickedEffect(View btn){
//String path = Environment.getExternalStorageDirectory().getAbsolutePath()+File.pathSeparator+"singing.wav";
//String path = "file:///android_asset/"+"singing.wav";
switch (btn.getId()) {
case R.id.btn_normal:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_NORMAL);
break;
case R.id.btn_luoli:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_LUOLI);
break;
case R.id.btn_dashu:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_DASHU);
break;
case R.id.btn_jingsong:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_JINGSONG);
break;
case R.id.btn_gaoguai:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_GAOGUAI);
break;
case R.id.btn_kongling:
VoiceEffectUtils.play(VoiceEffectUtils.MODE_KONGLING);
break;
default:
break;
}
}
}
接下来,到effect_sound.cpp里面根据不同类型实现不同音效的转变。
#define MODE_NORMAL 0
#define MODE_LUOLI 1
#define MODE_DASHU 2
#define MODE_JINGSONG 3
#define MODE_GAOGUAI 4
#define MODE_KONGLING 5
//cpp内全局引用,可以跨越多个线程,在手动释放之前,一直有效
JNIEnv *gJNIEnv;
jstring gMediaPath;
extern "C"
{
void Java_org_zzrblog_fmod_VoiceEffectUtils_init(JNIEnv *env, jclass clazz )
{
gJNIEnv = env;
jstring obj = gJNIEnv->NewStringUTF("file:///android_asset/dfb.mp3");
gMediaPath = (jstring) gJNIEnv->NewGlobalRef(obj); //创建cpp内全局引用。
}
void Java_org_zzrblog_fmod_VoiceEffectUtils_release(JNIEnv *env, jclass clazz )
{
if(gJNIEnv != NULL)
{
gJNIEnv->DeleteGlobalRef(gMediaPath);
}
gJNIEnv = NULL;
}
}
首先我们在init方法做些准备工作。这里硬编码assets里面的一个音频文件,代码不太优雅。有强迫症的朋友可以从java读取正确的路径,然后传参进init方法。然后我们根据之前在VoiceEffectUtils.java中的音效类型,在cpp也进行类型的宏定义。准备工作就绪,我们开始play的实现。
void Java_org_zzrblog_fmod_VoiceEffectUtils_play(JNIEnv *env, jclass clazz, jint type )
{
FMOD::System *system;
void *extradriverdata = 0;
FMOD::Sound *sound;
FMOD::Channel *channel;
bool playing = true; //判断音频是否还在播放.
//初始化 FMOD.System
System_Create(&system);
system->init(32, FMOD_INIT_NORMAL, extradriverdata);
//创建 FMOD.Sound
const char* path_c_str = env->GetStringUTFChars(gMediaPath, NULL);
LOGD("%s", path_c_str);
system->createSound(path_c_str, FMOD_DEFAULT, NULL, &sound);
//根据type 处理不同特效
try {
switch (type)
{
case MODE_NORMAL: //触发音频播放
system->playSound(sound, 0, false, &channel);
break;
// ...
default:
break;
}
} catch (...) {
LOGW("%s","VoiceEffectUtils play 发生异常...");
goto end;
}
//update一下FMOD的各种状态位。
system->update();
//每秒钟判断下是否在播放
while(playing) {
channel->isPlaying(&playing);
usleep(1000 * 1000); //单位是微秒
}
end:
LOGI("%s","VoiceEffectUtils play end ...");
env->ReleaseStringUTFChars(gMediaPath,path_c_str);
sound->release();
system->close();
system->release();
}
我们来简单解读以上代码:首先
第一步初始化FMOD.System,最大支持32个channel。
第二步创建FMOD.Sound,可以指定音频文件路径path_name,也可以直接传入音频数据data,我们现在这里就是载入初始化的 "file:///android_asset/dfb.mp3" 。成功创建Sound对象之后,
第三步就是通过system->playSound(sound, 0, false, &channel); 触发播放音频,并指向到其中一个
channel如果需要添加特效(DSP)就是通过channel对象操作的。我们现在是原声播放不做特效,直接跳到
system->update();并轮询当前的播放状态,如果还没播放结束就睡眠等待,注意这里的usleep是nuistd.h中,POSIX标准定义的unix的方法,单位是微秒。最后一步当播放结束,我们进行
各种对象的回收操作。
接下来一一介绍和实现其他音效功能。废话不说,先上代码干货:
//根据type 处理不同特效
try {
switch (type)
{
case MODE_NORMAL:{
//触发音频播放
system->playSound(sound, 0, false, &channel);
}break;
case MODE_LUOLI:{
//DSP digital signal process
//FMOD_DSP_TYPE_PITCHSHIFT fmod中预定义好的dsp,用于音调的提升或者降低
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
//设置音调的参数
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH,2.5);
//触发音频播放,获取音频通道
system->playSound(sound, 0, false, &channel);
//添加到channel
channel->addDSP(0,dsp);
}break;
case MODE_JINGSONG:{
//惊悚
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO,&dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.5);
system->playSound(sound, 0, false, &channel);
channel->addDSP(0,dsp);
}break;
case MODE_DASHU:{
//大叔
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT,&dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH,0.8);
system->playSound(sound, 0, false, &channel);
channel->addDSP(0,dsp);
}break;
case MODE_GAOGUAI:{
//搞怪 提高说话的速度
float frequency = 0;
system->playSound(sound, 0, false, &channel);
channel->getFrequency(&frequency);
frequency = frequency * 1.6f;
channel->setFrequency(frequency);
}break;
case MODE_KONGLING:{
//空灵
system->createDSPByType(FMOD_DSP_TYPE_ECHO,&dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY,300);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK,20);
system->playSound(sound, 0, false, &channel);
channel->addDSP(0,dsp);
}break;
default:
break;
}
} catch (...) {
LOGW("%s","VoiceEffectUtils play 发生异常...");
goto end;
}
... ...
代码如上,我们以萝莉特效来着重分析。首先我们来了解下什么是DSP:数字信号处理,英文:Digital Signal Processing,缩写为DSP。广义来说,数字信号处理是研究用数字方法对信号进行分析、变换、滤波、检测、调制、解调以及快速算法的一门技术学科。随着数字电路与系统技术以及计算机技术的发展,数字信号处理技术也相应地得到发展,其应用领域十分广泛。语音信号处理是信号处理中的重要分支之一。它包括的主要方面有:语音的识别,语言的理解,语音的合成,语音的增强,语音的数据压缩等。详细介绍可以参考
这里。
我们这里从FMOD内置的dsp功能开始,创建一个FMOD_DSP_TYPE_PITCHSHIFT的dsp,设置音调的参数 dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.5); 此类设置参数的接口方法和OpenGL状态机原理一样,请注意并不是以方法命名为引导的。 创建设置dsp之后,我们就可以在system->playSound(sound, 0, false, &channel); 触发音频播放并获取音频内部的一个通道channel,把dsp添加到此通道上channel->addDSP(index,dsp),并指定索引号。
其他效果同理,代码大致一样,就是创建dsp类型不一样,还有就是dsp的配置参数不一样。惊悚特效我们需要产生一样颤音的效果;大叔特效和萝莉的相反,我们只需要降低音频音调就可以了;搞怪特效我们这里尝试把音频的速度(频率)加大;最后空灵的效果,我们让他产生声音回旋的,达到空无一人效果就搞定。
代码算是搞定了,但是还不能运行demo,我们还需要到CMakeLists增加特效库的编译脚本,在原来的基础上增加如下语句:
... ...
add_library( # 生成动态库的名称
fmod-effect-lib
# 指定是动态库SO
SHARED
# 编译库的源代码文件
src/main/cpp/fmod/effect_sound.cpp)
target_link_libraries( # 指定目标链接库
fmod-effect-lib
# 添加预编译库到目标链接库中
${log-lib}
fmod
fmodL)
然后build -> make Module "app" 查看src/main/jniLibs/下是否生成特效库 libfmod-effect-lib.so
最后在VoiceEffectUtils.java静态代码块中System.loadLibrary("fmod-effect-lib");
开心的run起来吧。