在音频处理的开源项目中,webrtc是一个很不错的例子。它包含降噪,去回声,增益,均衡等音频处理。这里我讲讲我所使用到的如何使用降噪方式。当然,具体它是如何降噪的,大家可以细看源码处理了。好了,线上源码。
以下是java 层MainActivity.java:
package com.test.jni;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.SeekBar;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
SeekBar skbVolume;//调节音量
boolean isProcessing = true;//是否录放的标记
boolean isRecording = false;//是否录放的标记
static final int frequency = 8000;
static final int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
static final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
int recBufSize, playBufSize;
AudioRecord audioRecord;
AudioTrack audioTrack;
private String outFilePath;
private OutputStream mOutputStream;
private static final int FLAG_RECORD_START = 1;
private static final int FLAG_RECORDING = 2;
private static final int FLAG_RECORD_FINISH = 3;
private WebrtcProcessor mProcessor;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获得合适的录音缓存大小
recBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
Log.e("", "recBufSize:" + recBufSize);
//获得合适的播放缓存大小
playBufSize = AudioTrack.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
//创建录音和播放实例
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, audioEncoding, recBufSize);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, frequency, channelConfiguration, audioEncoding, playBufSize, AudioTrack.MODE_STREAM);
findViewById(R.id.btnRecord).setOnClickListener(this);
findViewById(R.id.btnStop).setOnClickListener(this);
skbVolume = (SeekBar) this.findViewById(R.id.skbVolume);
skbVolume.setMax(100);//音量调节的极限
skbVolume.setProgress(50);//设置seekbar的位置值
audioTrack.setStereoVolume(0.7f, 0.7f);//设置当前音量大小
skbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
float vol = (float) (seekBar.getProgress()) / (float) (seekBar.getMax());
audioTrack.setStereoVolume(vol, vol);//设置音量
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
});
((CheckBox) findViewById(R.id.cb_ap)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton view, boolean checked) {
isProcessing = checked;
}
});
initProccesor();
}
@Override
protected void onDestroy() {
releaseProcessor();
android.os.Process.killProcess(android.os.Process.myPid());
super.onDestroy();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btnRecord) {
isRecording = true;
//启动线程,开始录音和一边播放
new RecordPlayThread().start();
} else if (v.getId() == R.id.btnStop) {
isRecording = false;
}
}
class RecordPlayThread extends Thread {
public void run() {
try {
short[] buffer = new short[recBufSize/2];
audioRecord.startRecording();//开始录制
audioTrack.play();//开始播放
saveToFile(FLAG_RECORD_START, null);
while (isRecording) {
//setp 1 从MIC保存数据到缓冲区
int bufferReadResult = audioRecord.read(buffer, 0, recBufSize/2);
short[] tmpBuf_src = new short[bufferReadResult];
System.arraycopy(buffer, 0, tmpBuf_src, 0, bufferReadResult);
//setp 2 进行处理
if (isProcessing) {
processData(tmpBuf_src);
} else {
}
//写入数据即播放
audioTrack.write(tmpBuf_src, 0, tmpBuf_src.length);
//saveToFile(FLAG_RECORDING, tmpBuf_src);
}
saveToFile(FLAG_RECORD_FINISH, null);
audioTrack.stop();
audioRecord.stop();
} catch (Exception t) {
t.printStackTrace();
}
}
};
class RecordPlayThread2 extends Thread {
public void run() {
try {
byte[] buffer = new byte[recBufSize];
audioRecord.startRecording();//开始录制
audioTrack.play();//开始播放
saveToFile(FLAG_RECORD_START, null);
while (isRecording) {
//setp 1 从MIC保存数据到缓冲区
int bufferReadResult = audioRecord.read(buffer, 0, recBufSize);
byte[] tmpBuf_src = new byte[bufferReadResult];
System.arraycopy(buffer, 0, tmpBuf_src, 0, bufferReadResult);
//setp 2 进行处理
if (isProcessing) {
processData(tmpBuf_src);
} else {
}
//写入数据即播放
audioTrack.write(tmpBuf_src, 0, tmpBuf_src.length);
saveToFile(FLAG_RECORDING, tmpBuf_src);
}
saveToFile(FLAG_RECORD_FINISH, null);
audioTrack.stop();
audioRecord.stop();
} catch (Exception t) {
t.printStackTrace();
}
}
};
/**
* 保存录音数据到本地wav文件
* @param flag
* @param data
*/
private void saveToFile(int flag, byte[] data){
switch (flag){
case FLAG_RECORD_START:
String pcmPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/record.pcm";
try {
mOutputStream = new FileOutputStream(pcmPath);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
case FLAG_RECORDING:
if(mOutputStream != null){
try {
mOutputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
break;
case FLAG_RECORD_FINISH:
try {
if(mOutputStream != null){
mOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
pcmPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/record.pcm";
String wavePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/record.wav";
AudioEncodeUtil.convertPcm2Wav(pcmPath, wavePath);
break;
}
}
/**
* 初始化降噪
*/
private void initProccesor(){
mProcessor = new WebrtcProcessor();
mProcessor.init(frequency);
}
/**
* 释放降噪资源
*/
private void releaseProcessor(){
if(mProcessor != null){
mProcessor.release();
}
}
/**
* 处理需要降噪的音频数据
* @param data
*/
private void processData(byte[] data){
if(mProcessor != null){
mProcessor.processNoise(data);
}
}
/**
* 处理需要降噪的音频数据
* @param data
*/
private void processData(short[] data){
if(mProcessor != null){
mProcessor.processNoise(data);
}
}
}
以上代码主要是实现一边录音,一边播放录音的声音,类似ktv,在其中对每次获取的录音数据tmpBuf_src交给WebrtcProcessor处理,这里可以读取为byte[]或者short[] 数据,但是交给底层webrtc处理时,都是需要转换为short[] 数据的。然后这边采样率我采用8000,采样编码位16,单声道。
接下来看看WebrtcProcessor.java的处理:
package com.test.jni;
import android.util.Log;
/**
* 音频降噪处理
*/
public class WebrtcProcessor {
static {
try {
//加载降噪库
System.loadLibrary("webrtc");
} catch (UnsatisfiedLinkError e) {
Log.e("TAG", "Couldn't load lib: - " + e.getMessage());
}
}
/**
* 处理降噪
* @param data
*/
public void processNoise(byte[] data){
if(data == null) return;
int newDataLength = data.length/2;
if(data.length % 2 == 1){
newDataLength += 1;
}
//此处是将字节数据转换为short数据
short[] newData = new short[newDataLength];
for(int i=0; ibyte low = 0;
byte high = 0;
if(2*i < data.length){
low = data[2*i];
}
if((2*i+1) < data.length){
high = data[2*i+1];
}
newData[i] = (short) (((high << 8) & 0xff00) | (low & 0x00ff));
}
// 交给底层处理
processNoise(newData);
//处理完之后, 又将short数据转换为字节数据
for(int i=0; iif(2*i < data.length){
data[2*i] = (byte) (newData[i] & 0xff);
}
if((2*i+1) < data.length){
data[2*i+1] = (byte) ((newData[i] >> 8) & 0xff);
}
}
}
/**
* 初始化降噪设置
* @param sampleRate 采样率
* @return 是否初始化成功
*/
public native boolean init(int sampleRate);
/**
* 处理降噪
* @param data
* @return
*/
public native boolean processNoise(short[] data);
/**
* 释放降噪资源
*/
public native void release();
}
此处你可能需要将字节数据转换为short数据,要特别小心,如果不小心转错了,你的音频数据就乱码了,来的后果是,听到的声音基本都是沙沙声,我之前就是在这里踩了坑,底层调试了很久也没解决,后面才意识到可能上层出错了,调试之后发现是这里。正常呢,调试时看short数据时,如果它们的数值不是很大,那应该是没问题的,如果大部分都是4000以上,或者-4000以下的,那很可能是转换的时候出问题了,一般来说数值都是几十,几百的样子。
好了,现在看看底层大概是如何实现的:
#include
#include "audio_ns.h"
#include "noise_suppression.h"
//此处是为了里面的底层方法能被java层识别
extern "C" {
//降噪的实例,句柄
NsHandle* handle = NULL;
//降噪处理
void innerProcess(short in_sample[], short out_sample[], int length){
int curPosition = 0;
//此处以160为单位, 依次调用audio_ns_process处理数据,因为这个方法一次只能处理160个short音频数据
while(curPosition < length){
audio_ns_process((int) handle, in_sample + curPosition, out_sample + curPosition);
curPosition += 160;
}
}
JNIEXPORT jboolean JNICALL
Java_com_test_jni_WebrtcProcessor_init(JNIEnv *env, jobject instance, jint sample_rate) {
//初始化降噪实例
handle = (NsHandle *) audio_ns_init(sample_rate);
return false;
}
JNIEXPORT jboolean JNICALL
Java_com_test_jni_WebrtcProcessor_processNoise(JNIEnv *env, jobject instance, jshortArray sample) {
if(!handle)
return false;
//获取数据长度
jsize length = env->GetArrayLength(sample);
//转换为jshort数组
jshort *sam = env->GetShortArrayElements(sample, 0);
//将sam的数据全部复制给新的in_sample
short in_sample[length];
for(int i=0; i//传入in_sample作为需要处理音频数据, 处理之后的数据返回到sam中
innerProcess(in_sample, sam, length);
//将sam中的数据,再转换回sample中
env->ReleaseShortArrayElements(sample, sam, 0);
return true;
}
JNIEXPORT void JNICALL
Java_com_test_jni_WebrtcProcessor_release(JNIEnv *env, jobject instance) {
// 释放降噪资源
if(handle){
audio_ns_destroy((int) handle);
}
}
}
上面代码描述的比较清晰了,就是实际上webrtc降噪一次性只处理了80个short数据,在8000采样率中是这样的,意思就是说webrtc每次只能处理10毫秒,0.01秒的数据。那么依次类推,针对44100采样率的数据处理的话,每次能处理的数据长度就应该是441个short数据了,有不同采样率需求的朋友,可以自行修改测试。接下来看看webrtc的降噪是如何初始化和处理的:
#include "audio_ns.h"
#include "noise_suppression.h"
#include
int audio_ns_init(int sample_rate){
NsHandle* NS_instance;
int ret;
//创建WebRtcNs实例
if ((ret = WebRtcNs_Create(&NS_instance) )) {
printf("WebRtcNs_Create failed with error code = %d", ret);
return ret;
}
//初始化WebRtcNs实例,此处需要指定采样,告诉它一次可以处理多少个short音频数据,
//如果是8000, 则一次可以处理80,如果是44100, 则一次可以处理441个
//也就是说,一次性可以处理10ms时间的数据
if ((ret = WebRtcNs_Init(NS_instance, sample_rate) )) {
printf("WebRtcNs_Init failed with error code = %d", ret);
return ret;
}
//设置降噪的力度,0,1,2, 0最弱,2最强
if ( ( ret = WebRtcNs_set_policy(NS_instance, 2) ) ){
printf("WebRtcNs_set_policy failed with error code = %d", ret);
return ret;
}
return (int)NS_instance;
}
int audio_ns_process(int ns_handle , short *src_audio_data ,short *dest_audio_data){
//get handle
NsHandle* NS_instance = (NsHandle* )ns_handle;
//noise suppression
if(
//此处这么做,是因为,真正的WebRtcNs_Process,一次只能处理80个shorts音频数据
WebRtcNs_Process(NS_instance ,src_audio_data ,NULL ,dest_audio_data , NULL) ||
WebRtcNs_Process(NS_instance ,&src_audio_data[80] ,NULL ,&dest_audio_data[80] , NULL) ){
printf("WebRtcNs_Process failed with error code = " );
return -1;
}
return 0;
}
void audio_ns_destroy(int ns_handle){
//释放WebRtcNs资源
WebRtcNs_Free((NsHandle *) ns_handle);
}
以上是调用真正的webrtc代码处理降噪了,注释也比较详细,大家自己看。
那么真正底层的webrtc处理降噪是怎么样的呢?这个,呵呵,我觉得吧,浅尝则止,这个不是一般能看懂的,我是看不懂,核心大部分是算法,如果不熟悉降噪的算法和各个数据的意义的话,那看着简直是看天书啊。当然啦,大家想看的或者想要源码进行测试的话,我后面会提供项目源码下载的。
你以为这样就完了吗?那好像还不够丰富啊,因此还有一点我想分享给大家的,就是音频处理中,还有最开始我所说过的各种音频处理,绝不仅仅只有降噪,在当前网上开放的android音频处理项目源码如此稀缺的环境中(我想说,真是百度了好久的android音频处理,却找不出几个可以运行测试的android项目源码,实在香菇),该如何进行其它的音频处理呢。幸运的是,webrtc这个项目里,提供了很多音频处理的模块,大家可以去网上把它下载下来,找到对应的模块,比如增益,在webrtc/modules/audio_processing/agc目录下,把里面的文件拷到自己项目中编译,当然可能还会设计到其它目录的文件,找到拷过来,后面应该就可以编译了。至于怎么编译,找到我项目中的CMakeList.txt文件,依葫芦画瓢,替换修改就是了。
可是好像还有一个问题,那就是编译之后我该怎么用啊??这个,才是重点啊!是啊,我当时也是一头雾水。好吧,本着助人为乐的精神(嘿嘿),我就传授一个从不外传的绝技吧(好像好高级,好期待啊),那就是搜索github(程序员都该知道的超牛逼网站),比如我想找webrtc降噪,那么我就搜WebRtcNs_Process, 找到一些合适的项目,看人家是如何调用实现的,这样就可以实现啦。
好了,就说到这里,等着吃饭了。下面是项目下载地址。
http://download.csdn.net/detail/hesong1120/9687830