Android-NDK開發(fā)-利用fmod實現(xiàn)變聲

最近在學NDK開發(fā),自己在接觸一些第三方開源C/C++庫的時候,會碰到一些問題,這里記錄下來,就相當于筆記了。
廢話不多說,先進入fmod官網(wǎng),要注冊登入才能下載,點擊下載,一直往下滑,會看到FMOD Studio API,選擇Android版本下載。


image.png

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


image.png

api文件夾里面的才是我們要用的。我們要用源碼,所以選擇api文件夾下的core文件夾。有樣例、有頭文件、有編譯好的so文件。

創(chuàng)建一個ndk項目:

我們把native-lib修改成正規(guī)的名稱,我這里命名成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來開發(fā),肯定要告訴系統(tǒng),所以需要在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被引用。

然后我們要將這兩個庫關(guān)聯(lián)到我們自己的庫中。

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

這時我們檢驗是否成功就是跑起來試試。

創(chuàng)建變聲類,并生成頭文件。

這里我隨便創(chuàng)了個類叫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","播放狀態(tài)-"+flag);
        playing = flag;
    }

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

值的提醒的是我們在類里面加了setPlaying方法與isPlaying方法,是為了防止連續(xù)點擊播放造成多聲音重疊問題。
生成頭文件我們進入build目錄下,選擇intermediates->javac->debug->comp...->classes->我們自己的包名目錄下,就能找到QQFixUtile.class,如果沒有生成,說明你沒有編譯過,就編譯一下就行。


image.png

我們的目的是在cmd命令下進入到classes目錄下,然后執(zhí)行 javah 包名.類名,,,不要加.class后綴!


image.png

這個時候就會生成一個頭文件,我們剪切他到cpp目錄下。
image.png

image.png

在我們的cpp文件中實現(xiàn)頭文件的方法

下面是整個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轉(zhuǎn)為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);
        //創(chuàng)建聲音
        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 -> 音效 創(chuàng)建fmod中預定義好的音效
                //FMOD_DSP_TYPE_PITCHSHIFT dsp,提升或者降低音調(diào)用的一種音效
                system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT,&dsp);
                //設置音調(diào)的參數(shù)
                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","發(fā)生異常");
        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、引用系統(tǒng)頭文件我們用<>括號,引用我們自己的或開源庫的用""號#include "inc/fmod.hpp"要加入inc/作為指示,因為fmod.hpp在inc的文件夾下。h文件和hpp文件不同之處在于h文件是只有方法的聲明,hpp有方法聲明也有方法實現(xiàn)。
2、fmod使用步驟簡單說明如下:
初始化:System_Create(&system);--》system->init(32, FMOD_INIT_NORMAL, NULL);--》創(chuàng)建聲音:system->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);--》創(chuàng)建音效:system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT,&dsp);--》設置音調(diào)的參數(shù):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秒,他這里單位是微秒,我們得根據(jù)音頻有多長就設置多長,所以我們用while循環(huán)。這里怕出現(xiàn)異常就加了try catch,但是沒有拋給java層。
3、這里會調(diào)用我們在QQFixUtile定義的setPlaying方法,涉及到c調(diào)用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實現(xiàn)

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","檢查播放狀態(tài)-"+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","檢查播放狀態(tài)-"+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","檢查播放狀態(tài)-"+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","檢查播放狀態(tài)-"+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","檢查播放狀態(tài)-"+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","檢查播放狀態(tài)-"+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, "您拒絕了開啟權(quán)限,可去設置界面打開", 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、每一次播放前查看播放狀態(tài),如果在播放就擋住。
3、這里問了方便,每次都是判斷權(quán)限成功后調(diào)用播放方法。
4、FMOD.init(this); 與FMOD.close();要加上,并且FMOD.init(this); 要在QQFixUtile實例化前

demo已經(jīng)上傳到github
https://github.com/yanjinloving/QQFix

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容