DSP

NDK学习笔记:一起来变萝莉音!FMOD学习总结(下)

2019-07-13 15:17发布

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起来吧。