Android NDK开发之旅30--使用fmod模仿QQ变声特效

前言

我们这次用到的是fmod这个库,fmod是音效引擎游戏开发革命引擎,著名的游戏开发引擎CosCos2D、 Unity都封装了这个库。

FMOD的如下优点:
  • 使用FMOD我们可以使用更少的资源创建更加高级和丰富的音效,减少运行时内存资源消耗;
  • 音效管理只需要在FMOD Studio中管理好即可;
  • 编程人员只需要依赖于各种字符串形式的Sound Event和简单的播放API即可,使用简单;
  • 平台支持较为完善。

项目演示

image.png

原理分析

  • 原声:直接播放音频文件
  • 萝莉:对音频提高八度左右
  • 大叔:对音频减低八度左右
  • 惊悚:增加音频的颤音
  • 搞笑:增加音频的播放速度
  • 空灵:增加音频的回音

资源下载与使用

1.fmod下载
2.解压压缩包以后,打开api文件夹,会有fsbank、lowlevel、studio:
  • fsbank,bank是一个fmod的概念,包含里一些声音的事件,也可以通过
    FMOD Studio Tool来制作,这里是通过代码去制作。
  • lowlevel,我们需要的文件都在这里,包含了基础的声音处理功能。
  • studio,这里存放的api跟界面相关的,我们不需要使用这些界面。
3.使用下载包中api-> lowlevel 下的库与资源文件
  • examples示例程序,里面有一个Activity,以及对应的cpp文件,供我们参考写。
  • inc目录,放的是fmod相关的头文件,直接Copy到我们的Android项目当中。
  • lib目录,放的是需要预编译调用的so库,以及一个jar包,直接Copy到我们的Android项目当中。

代码编写

1、我们创建一个EffectUtils类,编写我们的fmod变声处理

package org.fmod.example;

/**
 * Created by Xionghu on 2017/11/24.
 * Desc:
 */

public class EffectUtils {

    //音效类型
    public static final int MODE_NORMAL = 0;
    public static final int MODE_LUOLI = 1;
    public static final int MODE_DASHU = 2;
    public static final int MODE_JINGSONG = 3;
    public static final int MODE_GAOGUAI = 4;
    public static final int MODE_KONGLING = 5;
    /**
     * 音效处理
     * @param path
     * @param type
     */
    public native static void fix(String path,int type);

    static
    {
         System.loadLibrary("fmodL");
         System.loadLibrary("fmod");
         System.loadLibrary("qq_voicer");
    }
}

2、javah我们的声明文件EffectUtils生成头文件

生成头文件请参考超级简单的Android Studio jni 实现(无需命令行)

注意:新版Android Studio ndk-build 要指定Application.mk和Android.mk路径


生成org_fmod_example_EffectUtils.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_fmod_example_EffectUtils */

#ifndef _Included_org_fmod_example_EffectUtils
#define _Included_org_fmod_example_EffectUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_fmod_example_EffectUtils
 * Method:    fix
 * Signature: (Ljava/lang/String;I)V
 */
JNIEXPORT void JNICALL Java_org_fmod_example_EffectUtils_fix
  (JNIEnv *, jclass, jstring, jint);

#ifdef __cplusplus
}
#endif
#endif

3、编写变声核心代码 effects_qq_voicer.cpp

注意paly_sound.cpp 、effects.cpp是fmod示例代码,大家可以在编写代码 effects_qq_voicer.cpp之前进行测试
effects_qq_voicer.cpp
#include "inc/fmod.hpp"
#include "org_fmod_example_EffectUtils.h"

#include <stdlib.h>
#include <unistd.h>
#include <android/log.h>

#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"kpioneer",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"kpioneer",FORMAT,##__VA_ARGS__);

#define MODE_NORMAL  0
#define MODE_LUOLI 1
#define MODE_DASHU 2
#define MODE_JINGSONG 3
#define MODE_GAOGUAI 4
#define MODE_KONGLING 5

using namespace FMOD;

JNIEXPORT void JNICALL Java_org_fmod_example_EffectUtils_fix
(JNIEnv *env, jclass jcls, jstring path_jstr, jint type) {
    LOGI("%s", "fix normal55555555555");
    System *system;
    Sound *sound;
    Channel *channel;
    DSP *dsp;
    float frequency = 0;
    bool playing = true;

    const char* path_cstr = env->GetStringUTFChars(path_jstr, NULL);
    try {
    //初始化
    System_Create(&system);
    //手机录音一般是16位  如果是32位的音频要填32 否则无法播放声音
    system->init(16, FMOD_INIT_NORMAL, NULL);
    //创建声音
    system->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);
    switch (type) {
    case MODE_NORMAL:
        //原生播放
        LOGI("%s", path_cstr);
        system->playSound(sound, 0, false, &channel);
        LOGI("%s", "fix normal");
        break;
    case MODE_LUOLI:
        //萝莉
        //DSP digital signal process
        //dsp -> 音效
        //FMOD_DSP_TYPE_PITCH  dsp ,提升或者降低音调用的一种音效
        // FMOD_DSP_TYPE_PITCHSHIFT 在fmod_dsp_effects.h中
        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);
        LOGI("%s", "fix luoli");
        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
        channel->addDSP(0, dsp);
        LOGI("%s", "fix dashu");
        break;
        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_GAOGUAI:
        //搞怪
        //提高说话的速度
        system->playSound(sound, 0, false, &channel);
        channel->getFrequency(&frequency);
        frequency = frequency * 1.6;
        channel->setFrequency(frequency);
        LOGI("%s", "fix gaoguai");
        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);
        LOGI("%s", "fix kongling");

        break;

    default:
        break;
    }
    }catch(...){
        LOGE("%s","发生异常");
        goto END;
    }
    system->update();
    //进程休眠 单位微秒 us
    //每秒钟判断是否在播放
    while (playing) {
        channel->isPlaying(&playing);
        usleep(1000 * 1000);
    }
    goto END;
    //释放资源
END:
    env->ReleaseStringUTFChars(path_jstr,path_cstr);
    sound->release();
    system->close();
    system->release();


}
注意myvoice.wav音频文件的位数
16位音频 录音工具
则在c++中要相应写16,否则无法播放
    //手机录音一般是16位  如果是32位的音频要填32 否则无法播放声音
    system->init(16, FMOD_INIT_NORMAL, NULL);

4.写mk文件

Android.mk
LOCAL_PATH := $(call my-dir)


include $(CLEAR_VARS)
LOCAL_MODULE := fmod
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/$(TARGET_ARCH_ABI)/libfmod.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := fmodL
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/$(TARGET_ARCH_ABI)/libfmodL.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)


LOCAL_MODULE    := qq_voicer
#LOCAL_SRC_FILES := play_sound.cpp common.cpp common_platform.cpp  #音效果一 用MainActivity
#LOCAL_SRC_FILES := effects.cpp common.cpp common_platform.cpp   #音效果二 用MainActivity
LOCAL_SRC_FILES :=effects_qq_voicer.cpp    #qq变声用 QQVoiceActivity
LOCAL_SHARED_LIBRARIES := fmod fmodL
LOCAL_LDLIBS := -llog
LOCAL_CPP_FEATURES := exceptions  #支持异常处理
include $(BUILD_SHARED_LIBRARY)

Applicatoin.mk
APP_MODULES := qq_voicer
APP_ABI :=   arm64-v8a armeabi armeabi-v7a #表示 编译目标 ABI(应用二进制接口)
APP_STL := gnustl_static ##支持标准STL C++
APP_PLATFORM := android-14

5.编写Android调用变声主程序QQVoiceActivity

package org.fmod.example;

import android.Manifest;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.tbruyelle.rxpermissions2.RxPermissions;
import org.fmod.FMOD;
import java.io.File;
import io.reactivex.functions.Consumer;

/**
 * Created by Xionghu on 2017/11/24.
 * Desc:
 */

public class QQVoiceActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FMOD.init(this);
        setContentView(R.layout.activity_qq_voice);


    }

    public void mFix(final View btn) {
        RxPermissions rxPermission = new RxPermissions(QQVoiceActivity.this);
        rxPermission.request(
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO)
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean granted) {
                        if (granted) {
                            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "myvoice.wav";
                           //String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "drumloop.wav";
                            Log.d("QQVoiceActivity", path);
                            switch (btn.getId()) {
                                case R.id.btn_normal:
                                    EffectUtils.fix(path, EffectUtils.MODE_NORMAL);
                                    break;

                                case R.id.btn_luoli:
                                    EffectUtils.fix(path, EffectUtils.MODE_LUOLI);
                                    break;

                                case R.id.btn_dashu:
                                    EffectUtils.fix(path, EffectUtils.MODE_DASHU);
                                    break;

                                case R.id.btn_jingsong:
                                    EffectUtils.fix(path, EffectUtils.MODE_JINGSONG);
                                    break;

                                case R.id.btn_gaoguai:
                                    EffectUtils.fix(path, EffectUtils.MODE_GAOGUAI);
                                    break;

                                case R.id.btn_kongling:
                                    EffectUtils.fix(path, EffectUtils.MODE_KONGLING);
                                    break;

                                default:
                                    break;
                            }


                        } else {
                              Toast.makeText(QQVoiceActivity.this, "权限被拒绝,请到设置中打开",Toast.LENGTH_SHORT).show();
                        }
                    }
                });


    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        FMOD.close();
    }
}
myvoice.wav是用专业录音app工具录制,放在sdcard根目录下

5其它文件配置

build.gradle中
        ndk{
            moduleName "qq_voicer"
        }
        sourceSets.main{
            jni.srcDirs = []
            jniLibs.srcDir "src/main/libs"
        }
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'io.reactivex.rxjava2:rxjava:2.0.5'
    compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
AndroidManifest.xml中
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE "/>

源码下载

Github:https://github.com/kpioneer123/QQVoicerChange

特别感谢:

动脑学院Jason






微信号kpioneer

推荐阅读更多精彩内容