目錄
·········前言
·········前期工作
·········編譯工作
·········編碼工作(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 即可。