目录
·········前言
·········前期工作
·········编译工作
·········编码工作(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 即可。