Android-NDK开发-利用fmod实现变声

最近在学NDK开发,自己在接触一些第三方开源C/C++库的时候,会碰到一些问题,这里记录下来,就相当于笔记了。
废话不多说,先进入fmod官网,要注册登入才能下载,点击下载,一直往下滑,会看到FMOD Studio API,选择Android版本下载。


image.png

挺大的,60多M,解压后,会看到如下文件。


image.png

api文件夹里面的才是我们要用的。我们要用源码,所以选择api文件夹下的core文件夹。有样例、有头文件、有编译好的so文件。

创建一个ndk项目:

我们把native-lib修改成正规的名称,我这里命名成qqfix,注意这里要修改一个名称,步骤要很多,要一步步来:
1、修改默认的cpp下的native-lib.cpp文件名,右键rename就行
2、修改cmake文件下的引用 add_library、target_link_libraries


image.png

image.png

编译运行,这样你的ndk名称就改成了自己定义的。

加入FMOD文件

把我下载下来的FMOD文件放入进来。


image.png

当然我们这里只用了armeabi-v7a的so库,所以要在gradle里面额外注释说明


image.png

cmak引入第三方so

我们引入第三方so来开发,肯定要告诉系统,所以需要在cmake里面配置。主要是两个库fomd、fmodL
加入第三方libfmod so

add_library(fmod SHARED IMPORTED)
set_target_properties(fmod PROPERTIES IMPORTED_LOCATION
        ${CMAKE_SOURCE_DIR}/armeabi-v7a/libfmod.so)

加入第三方libfmodL so

add_library(fmodL SHARED IMPORTED)
set_target_properties(fmodL PROPERTIES IMPORTED_LOCATION
        ${CMAKE_SOURCE_DIR}/armeabi-v7a/libfmodL.so)

这里的${CMAKE_SOURCE_DIR}/armeabi-v7a/libfmod.so指的是cmake目录下的armeabi-v7a的libfmod.so被引用。

然后我们要将这两个库关联到我们自己的库中。

target_link_libraries( # Specifies the target library.
        qqfix
        fmod
        fmodL
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

注意这里面的fmod、fmodL要和add_library与set_target_properties时的名称一致,不然会找不到库了。整体代码如图:


image.png

这时我们检验是否成功就是跑起来试试。

创建变声类,并生成头文件。

这里我随便创了个类叫QQFixUtile,把他作为JNI类。具体代码如下:

public class QQFixUtile {
    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;
    public boolean playing = false;
    static {
        System.loadLibrary("fmod");
        System.loadLibrary("fmodL");
        System.loadLibrary("qqfix");
    }

    /**
     * 包房声音
     * @param path 声音路径
     * @param type 播放类型
     */
    public native void fixVoice(String path,int type);

    /**
     * 专门提供给JNI使用
     * @param flag
     */
    private void setPlaying(boolean flag){
        Log.d("yanjin","播放状态-"+flag);
        playing = flag;
    }

    /**
     * 用来判断是否正在播放,如果是就不能再播放
     * @return
     */
    public boolean isPlaying() {
        return playing;
    }
}

值的提醒的是我们在类里面加了setPlaying方法与isPlaying方法,是为了防止连续点击播放造成多声音重叠问题。
生成头文件我们进入build目录下,选择intermediates->javac->debug->comp...->classes->我们自己的包名目录下,就能找到QQFixUtile.class,如果没有生成,说明你没有编译过,就编译一下就行。


image.png

我们的目的是在cmd命令下进入到classes目录下,然后执行 javah 包名.类名,,,不要加.class后缀!


image.png

这个时候就会生成一个头文件,我们剪切他到cpp目录下。
image.png

image.png

在我们的cpp文件中实现头文件的方法

下面是整个cpp的代码

#include <jni.h>
#include <string>
#include <unistd.h>
#include <android/log.h>
#include "com_yanjin_qqfix_QQFixUtile.h"
#include "inc/fmod.hpp"

//Android log输出的宏定义
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"jason",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"jason",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_com_yanjin_qqfix_QQFixUtile_fixVoice
        (JNIEnv *env, jobject jobj, jstring jpath, jint jtype){
    //播放声音的路径需要从jstring转为c的字符串
    const char* path_cstr = env->GetStringUTFChars(jpath,NULL);
    LOGI("%s",path_cstr);
    System *system;
    Sound *sound;
    Channel *channel;
    DSP *dsp;
    float frequency = 0;
    bool playing = true;
    //设置正在播放声音
    jclass  jclaz = (env)->GetObjectClass(jobj);
    jmethodID mid = (env)->GetMethodID(jclaz,"setPlaying","(Z)V");
    (env)->CallVoidMethod(jobj,mid,playing);
    try {
        //初始化
        System_Create(&system);
        system->init(32, FMOD_INIT_NORMAL, NULL);
        //创建声音
        system->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);
        switch (jtype){
            case MODE_NORMAL:
                //原生播放
                system->playSound(sound, 0, false, &channel);
                LOGI("%s","fix normal");
                break;
            case MODE_LUOLI:
                //萝莉
                //DSP digital signal process
                //dsp -> 音效 创建fmod中预定义好的音效
                //FMOD_DSP_TYPE_PITCHSHIFT 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);
                LOGI("%s","fix luoli");
                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
                channel->addDSP(0,dsp);
                LOGI("%s","fix dashu");
                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;
        }
    }catch (...){
        LOGE("%s","发生异常");
        goto end;
    }
    system->update();
    
    //单位是微秒
    //每秒钟判断下是否在播放
    while(playing){
        channel->isPlaying(&playing);
        usleep(200 * 1000);
    }
    goto end;

end:
    //设置没有在播放声音
    (env)->CallVoidMethod(jobj,mid,playing);
    //释放资源
    env->ReleaseStringUTFChars(jpath,path_cstr);
    sound->release();
    system->close();
    system->release();

}
这里需要注意:

1、引用系统头文件我们用<>括号,引用我们自己的或开源库的用""号#include "inc/fmod.hpp"要加入inc/作为指示,因为fmod.hpp在inc的文件夹下。h文件和hpp文件不同之处在于h文件是只有方法的声明,hpp有方法声明也有方法实现。
2、fmod使用步骤简单说明如下:
初始化:System_Create(&system);--》system->init(32, FMOD_INIT_NORMAL, NULL);--》创建声音:system->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);--》创建音效:system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT,&dsp);--》设置音调的参数:dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH,2.5);--》播放system->playSound(sound, 0, false, &channel);--》添加音效到轨道:channel->addDSP(0,dsp);--》播放更新: system->update();--》播放时睡眠:usleep(200 * 1000);
记住:这里的usleep(200 * 1000);很重要,我之前没加,一直没声音,然后看官方的usleep(50 * 1000 * 1000);这样用的,所以我用了就好了,后面查资料加问别人才知道这个是播放声音时需要睡眠的时间,demo直接给了50秒,他这里单位是微秒,我们得根据音频有多长就设置多长,所以我们用while循环。这里怕出现异常就加了try catch,但是没有抛给java层。
3、这里会调用我们在QQFixUtile定义的setPlaying方法,涉及到c调用java,里面需要获取方法的签名,背不了的就用java命令输出就行,进入classes目录输入javap -s com.yanjin.qqfix.QQFixUtile就是 javap -s 包名.类名。

最后来java代码

1、布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/m_btn_normal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="普通播放"/>
    <Button
        android:id="@+id/m_btn_luoli"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="萝莉播放"/>
    <Button
        android:id="@+id/m_btn_jingsong"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="惊悚播放"/>
    <Button
        android:id="@+id/m_btn_dashu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="大叔播放"/>
    <Button
        android:id="@+id/m_btn_gaoguai"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="搞鬼播放"/>
    <Button
        android:id="@+id/m_btn_kongling"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="空灵播放"/>
</LinearLayout>

2、activity实现

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    private final int PERMISSION_CODE = 1;
    private int mCurrentType = QQFixUtile.MODE_NORMAL;
    private QQFixUtile mQqFixUtile;
    private String mVoiceRootDirPath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FMOD.init(this);
        mVoiceRootDirPath = Environment.getExternalStorageDirectory().getPath()+ File.separator+"Voice Recorder"+File.separator+"123.m4a";
        mQqFixUtile = new QQFixUtile();
        findViewById(R.id.m_btn_normal).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_NORMAL;
                requestPermission();
            }
        });
        findViewById(R.id.m_btn_luoli).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_LUOLI;
                requestPermission();
            }
        });
        findViewById(R.id.m_btn_jingsong).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_JINGSONG;
                requestPermission();
            }
        });
        findViewById(R.id.m_btn_dashu).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_DASHU;
                requestPermission();
            }
        });
        findViewById(R.id.m_btn_gaoguai).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_GAOGUAI;
                requestPermission();
            }
        });
        findViewById(R.id.m_btn_kongling).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("yanjin","检查播放状态-"+mQqFixUtile.isPlaying());
                if(mQqFixUtile.isPlaying()){
                    Toast.makeText(MainActivity.this, "正在播放请稍后", Toast.LENGTH_SHORT).show();
                    return;
                }
                mCurrentType = QQFixUtile.MODE_KONGLING;
                requestPermission();
            }
        });
    }

    private void requestPermission() {
        PermissionHelper.with(this).requestCode(PERMISSION_CODE).requestPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.RECORD_AUDIO
        ).request();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PermissionHelper.requestPermissionsResult(this, requestCode, permissions, grantResults);
    }

    @PermissionDenied(requestCode = PERMISSION_CODE)
    private void onPermissionDenied() {
        Toast.makeText(this, "您拒绝了开启权限,可去设置界面打开", Toast.LENGTH_SHORT).show();
    }


    @PermissionPermanentDenied(requestCode = PERMISSION_CODE)
    private void onPermissionPermanentDenied() {
        Toast.makeText(this, "您选择了永久拒绝,可在设置界面重新打开", Toast.LENGTH_SHORT).show();
    }

    @PermissionSucceed(requestCode = PERMISSION_CODE)
    private void onPermissionSuccess() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mQqFixUtile.fixVoice(mVoiceRootDirPath, mCurrentType);

            }
        }).start();

    }

    @Override
    protected void onDestroy() {
        FMOD.close();
        super.onDestroy();
    }
}

这里注意

1、FMOD找不到,那是我们前面忽略了一个jar包,我们引入就行。我之前是找了半天,眼瞎了。
2、每一次播放前查看播放状态,如果在播放就挡住。
3、这里问了方便,每次都是判断权限成功后调用播放方法。
4、FMOD.init(this); 与FMOD.close();要加上,并且FMOD.init(this); 要在QQFixUtile实例化前

demo已经上传到github
https://github.com/yanjinloving/QQFix

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351