【WebRTC】在Android上用NDK編譯WebRtc音頻增益模塊(AGC),降噪(NS),java調用JNI實現功能

目錄

·········前言

·········前期工作

·········編譯工作

·········編碼工作(AGC-JAVA)

·········編碼工作(AGC-JNI)

·········編碼工作(NS-JAVA)

 ········編碼工作(NS-JNI)

·········總結

前言

       本文章是基於某個博客主寫的移植到Android NDK上編譯(這邊我沒有用cMake)

       https://www.cnblogs.com/mod109/p/5767867.html

       WebRtc單獨模塊編譯的資料網上很多,零零散散,但是看了很多程序大多數都是舊的版本模塊源碼進行編譯的,並且都是在

C++/C  的基礎上去運行,很少涉及到在NDK下的編譯和JNI的使用。

       後續會更新NS(降噪),aecm,aec(迴音消除) 模塊在NDK上編譯JNI上使用的文章。

       筆者也是今年纔開始正式的接觸WebRt,目前正在深入分析其源代碼,主要研究的是 底層模塊(非算法)、Android模塊,希

望後續能把一些經驗分享出來。

     

前期工作

      1:Android Studio 開發工具 ,再次強調一下,這次沒有使用 cMake 編譯,使用的是傳統的 ndk-build 編譯,開發工具需要自行

配置一下,當然後面會提供源碼  ,你完全可以 自己來配置cMake編譯。 

      2:首先不認識WebRtc的請先百一下度,特別是模塊的作用,如果是大佬的話請洗耳恭聽。

      3 : audacity2.3.exe  下載一下這個工具,幹什麼用 搜一下就知道。

         長什麼樣,長這樣  ↓

         

        如何導入PCM數據:文件->導入->選擇PCM數據->設置對應的採樣率等參數

      4:本章文重點講解NDK、JNI等使用,需要有一定的基礎適合看本文章

             

 

編譯工作

     後續會把Demo上傳,建議想做這些工作但是沒做過的可以自行另起一個項目來做。

     首先觀瀾我隨手一建的項目工程目錄

      

       ⑴ 這個是要處理的原始音頻文件,當然你也可以直接從SD卡讀取,爲了方便,這邊直接從assets 讀取。

       ⑵ AudioProcessJni 爲native 加載的類

           其餘爲測試類和工具類。

           (裏面代碼寫的比較亂沒有整理,demo下載完後自行調整 )

       ⑶ agc文件夾是  NS和AGC編譯所需的頭文件和依賴文件,最開始是先調試AGC的,所以文件夾名稱沒有改,

            ns_jni_wrapper.c 爲JNI層實現的功能

      這裏貼一下Android.mk

       這玩意不簡單的 , 裏面有一些編譯錯誤的默認配置項

     

      code:            

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
APP_ABI := armeabi x86
LOCAL_MODULE    := webrtc_audio
app_platform:=android-21
LOCAL_SRC_FILES := \
        agc/complex_bit_reverse.c \
        agc/complex_fft.c \
        agc/cross_correlation.c \
        agc/dot_product_with_scale.c \
        agc/downsample_fast.c \
        agc/energy.c \
        agc/fft4g.c \
        agc/get_scaling_square.c \
        agc/min_max_operations.c \
        agc/real_fft.c \
        agc/resample.c \
        agc/resample_48khz.c \
        agc/resample_by_2.c \
        agc/resample_by_2_internal.c \
        agc/resample_by_2_mips.c \
        agc/copy_set_operations.c \
        agc/division_operations.c \
        agc/spl_init.c \
        agc/spl_sqrt.c \
        agc/spl_sqrt_floor.c \
        agc/ring_buffer.c \
        agc/resample_fractional.c \
        agc/splitting_filter.c \
        agc/vector_scaling_operations.c \
        agc/analog_agc.c \
        agc/digital_agc.c \
        agc/ns_core.c \
        agc/nsx_core.c \
        agc/nsx_core_c.c \
        agc/nsx_core_neon_offsets.c \
        agc/noise_suppression.c \
        agc/noise_suppression_x.c \
        ns_jni_wrapper.c \

#undefined reference to 錯誤問題解決辦法
LOCAL_ALLOW_UNDEFINED_SYMBOLS := true
APP_SHORT_COMMANDS := true
LOCAL_LDLIBS    := -llog
APP_CPPFLAGS := -frtti -std=c++11
include $(BUILD_SHARED_LIBRARY)

  Application.mk

#APP_ABI := armeabi armeabi-v7a x86
APP_ABI := armeabi x86
APP_STL := stlport_static
APP_CPPFLAGS := -frtti -std=c++11
APP_SHORT_COMMANDS := true
LOCAL_LDFLAGS += -fuse-ld=bfd

 

 編碼前當然你要先把so 編譯過來才能調試

 編譯過程....(省略)

 

           好啦,當我們已經把so庫成功的編出來了,並且能正常  loadLibrary

 

編碼工作(AGC-JAVA)

    首先看下 AudioProcessJni文件中對Agc 操作的native  函數有哪些?

     

public class AudioProcessJni {

     static{
          System.loadLibrary("webrtc_audio");  //加載native code的動態庫
     }
     //Audio agc音頻增益接口
     public   native  static  void  AgcFree();
     public   native  static  int   AgcFun(ByteBuffer buffer1 , short[] sArr, int frameSize);
     public   native  static  long  AgcInit(long minLevel, long maxLevel,long fs);
     public   native  static  void  AgcProcess();  //agc test  model
}

AgcInit : 初始化agc模塊功能 

AgcFun:音頻增益處理函數

AgcFree:模塊釋放銷燬

AgcProcess: 這個是增益整個搓成完全在JNI層處理,不和java 通信。

其中AgcFun中的  ByteBuffer  是java和JNI 交互數據的重點,他是java和JNI共享的一個內存塊,當然你也可以給個返回值,看個人的編碼習慣。

 

那麼我們先來看java的測試代碼:

 

 public static final  String  AGC_OUT_FILE_PATCH_DICTORY="/storage/emulated/0/Pictures/agc";
 public static final  String  AGC_OUT_FILE_PATCH="/storage/emulated/0/Pictures/agc/byby_8K_1C_16bit_agc.pcm";

文件的輸出目錄以及輸出的文件。 

  public static    void  agc_audio_test(Activity  act)  {
        try {
            int nbread = 0;
            //讀取Assets文件流
            InputStream is = act.getAssets().open("byby_8K_1C_16bit.pcm");

            //輸出的文件目錄
            File  file = new File(AGC_OUT_FILE_PATCH_DICTORY);
            if (!file.exists()){
                boolean mkdirs = file.mkdirs();
                if (mkdirs) {
                    log("create dictroy success");
                } else {
                    log("create dictroy file");
                    return;
                }
            }
            //輸出的文件
            file = new File(AGC_OUT_FILE_PATCH);
            //調用初始刷Agc模塊
            long res = AudioProcessJni.AgcInit(0, 255, 8000);
            log(" AudioProcessJni.AgcInit  res = "+ res);
            log("sleep 2s ");
            Thread.sleep(2000);
            //初始化byte轉換工具
            BytesTransUtil bytesTransUtil = BytesTransUtil.getInstance();
            // rData 爲讀取的緩衝區 分配160字節
            byte[] rData = new byte[160];
            ByteBuffer  outBuffer = ByteBuffer.allocateDirect(160);
            FileOutputStream  fos = new FileOutputStream(file);
            //--------------------開始讀取---------------------------
            while((nbread=is.read(rData))>-1){
                short[] shorts = bytesTransUtil.Bytes2Shorts(rData);
                res = AudioProcessJni.AgcFun(outBuffer, shorts,80);
                for (int i = 0 ;i< 80 ;i++){

                    shorts[i] = (short) (outBuffer.get(2*i) + ( outBuffer.get(2*i+1) << 8));
                }
                  fos.write(bytesTransUtil.Shorts2Bytes(shorts),0,nbread);
            }
            log(" 結束Agc = " );
            if (fos!=null){

                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
            log("e:error -> "+e.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {

            AudioProcessJni.AgcFree();
        }
    }

  這個是java測試的流程代碼,也比較清晰,重點來講解下以下這段代碼

  

 //--------------------開始讀取---------------------------
            while((nbread=is.read(rData))>-1){
                short[] shorts = bytesTransUtil.Bytes2Shorts(rData);
                res = AudioProcessJni.AgcFun(outBuffer, shorts,80);
                for (int i = 0 ;i< 80 ;i++){

                    shorts[i] = (short) (outBuffer.get(2*i) + ( outBuffer.get(2*i+1) << 8));
                }
                  fos.write(bytesTransUtil.Shorts2Bytes(shorts),0,nbread);
            }
            log(" 結束Agc = " );

   ① 循環讀取文件流中的數據,每次讀取160個byte, 有點需要注意,底層的處理接口需要傳入的是short[] 數組,文件流讀取的

        byte[]數組需要轉換, 這邊的話在java 層轉換或者在JNI層用C轉換都是可以的,建議在JNI上轉換,不過需要注意的是有無

        符號的問題,我這邊爲了方便直接在java層進行轉換。

  ② Byte2Shorts 吧 byte數組轉換成了short數組 並傳入AgcFun [JNI層] 去處理, 返回值res < 0 是錯誤的 這個根據JNI層的邏輯

       去調整,我這邊爲了代碼簡潔,沒有做判斷。

  ③ 最後處理完的音頻數據會存放在outBuffer 這個ByteBuff 的變量裏,那麼在上層就可以去讀取它了。

       剛開始的時候以爲傳上來就可以用了,然後打印了數據的十六進制,發現值不一樣,正負數處理溢出的問題,這時候才慢慢

       意識到,java  基本變量都是有符號的,因爲底層是定義成無符號的,爲什麼定義成無符號這個研究JNI的時候再來說。

      那麼通過

                 shorts[i] = (short) (outBuffer.get(2*i) + ( outBuffer.get(2*i+1) << 8)); 

     這個處理將數據還原成正確的。注 : 寫文件操作在JNI層是很方便實現的,AgcProcess讀寫都是JNI層實現的,這邊放到上層 

     考慮到有些人 java這邊可能需要用到 這個處理完的buffer。 現在只是讀取文件,其實從MIC 讀取的PCM文件也基本類似。

     因此:將數據還原成了short[]之後 寫入文件還要轉到byte[]

       

       不知道各位小夥伴是否有更好的解決方法,或者看了我的demo之後又什麼更好的解決辦法一定要通知我。值得是有無符號 

       轉換這一塊。

 編碼工作(AGC-JNI)

 現在來看下JNI的處理,其實也很簡單一共3個函數先貼代碼:

   初始化函數:

JNIEXPORT jlong JNICALL Java_com_webrtc_ns_AudioProcessJni_AgcInit(JNIEnv *env, jclass cls, jlong minLevel , jlong maxLevel , jlong fs){

        minLevel = 0;
		maxLevel = 255;
		agc_samples  =fs;
		int agcMode  = kAgcModeFixedDigital;
        LOGE("Java_com_webrtc_ns_AudioProcessJni_AgcInit!  -> %d \n", sizeof(short));
        if ( (   WebRtcAgc_Create(&agcHandle) ) != 0) { //allocate dynamic memory on native heap for NS instance pointed by hNS.

             LOGE("Noise_Suppression WebRtcNs_Create err! \n");
             return  NS_ERROR;  //error occurs

        }
        LOGE("Java_com_webrtc_ns_AudioProcessJni_AgcCreate success! \n");
        if (0 !=  WebRtcAgc_Init(agcHandle, minLevel, maxLevel, agcMode, agc_samples) )
	    {
             LOGE("WebRtcAgc_Init WebRtcNs_Init err! \n");
             return  NS_ERROR;  //error occurs
	    }

      LOGE("Java_com_webrtc_ns_AudioProcessJni_AgcInit success! \n");
        WebRtcAgc_config_t agcConfig;
		agcConfig.compressionGaindB = 20; //在Fixed模式下,越大聲音越大
		agcConfig.limiterEnable     = 1;
		agcConfig.targetLevelDbfs   = 3;  //dbfs表示相對於full scale的下降值,0表示full scale,越小聲音越大
		WebRtcAgc_set_config(agcHandle, agcConfig);

		return NS_SUCCESS;
}

  處理函數 & 銷燬函數:

JNIEXPORT jint JNICALL Java_com_webrtc_ns_AudioProcessJni_AgcFun(JNIEnv *env, jclass type, jobject jdirectBuff,jshortArray sArr_, jint frameSize) {
    if(agc_buffer == NULL){
        LOGE("gc_buffer == NULL! \n");
        void* buffer = (*env)->GetDirectBufferAddress(env,jdirectBuff);
        agc_buffer = buffer;
    }
    uint8_t saturationWarning;
    int outMicLevel = 0;
    int micLevelOut = 0;
    int i =0 ;
    int inMicLevel  = micLevelOut;
    const short *pData    = NULL;
    short *pOutData    = NULL;
    pOutData = (short*)malloc(frameSize*sizeof(short));
    pData  =(*env)->GetShortArrayElements(env,sArr_,NULL);
    if(agcHandle == NULL){
        LOGE("agcHandle is null! \n");
        return  -3;
    }
    if(frameSize <= 0){
        return  -2;
    }
    int  agcProcessResult =  WebRtcAgc_Process(agcHandle,
                                               pData,
                                               NULL,
                                               frameSize,
                                               pOutData,
                                               NULL,
                                               inMicLevel,
                                               &outMicLevel,
                                               0,
                                               &saturationWarning);
    if (0 !=  agcProcessResult )
    {
        LOGE("failed in WebRtcAgc_Process!  agcProcessResult = %d \n" ,agcProcessResult);
        return  NS_ERROR ;  //error occurs
    }
    //memset(agc_buffer, 0,  160);
    shortTobyte(80,pOutData,agc_buffer);
    (*env)->ReleaseShortArrayElements(env, sArr_, pData, 0);
    return  AGC_SUCCESS;
}

JNIEXPORT void JNICALL Java_com_webrtc_ns_AudioProcessJni_AgcFree(JNIEnv *env , jclass  cls){

    WebRtcAgc_Free(agcHandle);
}

① 初始化函數看起來很簡單,確實很簡單。

    關於一下

 WebRtcAgc_config_t agcConfig;
 agcConfig.compressionGaindB = 20; //在Fixed模式下,越大聲音越大
 agcConfig.limiterEnable     = 1;
 agcConfig.targetLevelDbfs   = 3;  //dbfs表示相對於full scale的下降值,0表示full scale,越小聲音越大

   可以自己編譯調試, 用工具去比對效果。

②  void* buffer = (*env)->GetDirectBufferAddress(env,jdirectBuff);   獲取java層分配的ByteBuff 實例,最後一個 char指針指向了

      這個 實例,需要注意的是 這裏 

    unsigned  char* agc_buffer = NULL;   

      是無符號的,如果有符號的話後面可能會有溢出問題,所以在底層char指針表示的我這邊都用無符號。

       WebRtcAgc_Process 函數處理數據最後得到 short * 的數據 , 最後通過 shortTobyte 函數將short 的值賦給 ByteBuff 的實

      例,這樣java層就能得到這個數據。

③釋放工作比較簡單    WebRtcAgc_Free  即可。

 

 編碼工作(NS-JAVA)

 編碼工作(NS-JNI)

 總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章