美文网首页别人家的Android开发经验
Android-NDK开发-利用fmod实现变声

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

作者: 坑逼的严 | 来源:发表于2019-05-06 14:57 被阅读0次

    最近在学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

    相关文章

      网友评论

        本文标题:Android-NDK开发-利用fmod实现变声

        本文链接:https://www.haomeiwen.com/subject/tqproqtx.html