来源 http://www.qttaudio.com/android-aec.html
http://www.baike.com/wiki/%E5%9B%9E%E9%9F%B3%E6%B6%88%E9%99%A4%E6%8A%80%E6%9C%AF
在即时通讯应用中,需要进行双方,或是多方的实时语音交流,在要求较高的场合,通常都是采用外置音箱放音,这样必然会产生回音,即一方说话后,通过对方的音箱放音,然后又被对方的Mic采集到回传给自己。如果不对回音进行处理,将会影响通话质量和用户体验,更严重的还会形成震荡,产生啸叫。
回声消除就是在Mic采集到声音之后,将本地音箱播放出来的声音从Mic采集的声音数据中消除掉,使得Mic录制的声音只有本地用户说话的声音。
传统的回声消除都是采用硬件方式,在硬件电路上集成DSP处理芯片,如我们常用的固定电话、手机等都有专门的回音消除处理电路,而采用软件方式实现回声消除一直存在技术难点。
回声消除已经成为即时通讯中提供全双工语音的标准方法。声学回声消除是通过消除或者移除本地话筒中拾取到的远端的音频信号来阻止远端的声音返回去的一种处理方法。这种音频的移除都是通过数字信号处理来完成的。回声消除技术是数字信号处理的典型应用之一。
4.1(API level 16)的时候增加了一个API:AcousticEchoCanceler。
下面简单的介绍下AcousticEchoCanceler的使用方法:
private AcousticEchoCanceler canceler;
//判断当前机型是否支持AEC
public boolean isDeviceSupportAEC()
{
return AcousticEchoCanceler.isAvailable();
}
//初始化AEC
public boolean initAEC(int audioSession)
{
if (canceler != null)
{
return false;
}
canceler = AcousticEchoCanceler.create(audioSession);
canceler.setEnabled(true);
return canceler.getEnabled();
}
public boolean setAECEnabled(boolean enable)
{
if (null == canceler)
{
return false;
}
canceler.setEnabled(enable);
return canceler.getEnabled();
}
public boolean release()
{
if (null == canceler)
{
return false;
}
canceler.setEnabled(false);
canceler.release();
return true;
}
sessionid = audioRecord.getAudioSessionId();
初始化AudioTrack时,也需要额外的处理sessionid:
audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, frequency,
channelOUT, audioEncoding, tmpSize, AudioTrack.MODE_STREAM, sessionid);
上述代码是根据官方文档介绍编写的,而根据实际测试,离实际产品应用还有很长的距离。
Speex提供开源的的回音消除算法,从提供的文档(https://speex.org/docs/manual/speex-manual/node7.html#SECTION00740000000000000000)和测试用例看,效果还是非常棒的。下面简单介绍speex echo cancellation api的使用方法,官方示例文件testecho.c:
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include
#include
#include "speex/speex_echo.h"
#include "speex/speex_preprocess.h"
#define NN 128
#define TAIL 1024
int main(int argc, char **argv)
{
FILE *echo_fd, *ref_fd, *e_fd;
short echo_buf[NN], ref_buf[NN], e_buf[NN];
SpeexEchoState *st;
SpeexPreprocessState *den;
int sampleRate = 8000;
if (argc != 4)
{
//mic_signal是mic实时采集到的音频数据(包含speaker_signal),speaker_signal是由喇叭放出去的音频数据(也可称为远端数据),output是将mic_signal中去掉speaker_signal的数据,就得到了干净无回声的数据
fprintf(stderr, "testecho mic_signal.sw speaker_signal.sw output.sw\n");
exit(1);
}
echo_fd = fopen(argv[2], "rb");
ref_fd = fopen(argv[1], "rb");
e_fd = fopen(argv[3], "wb");
st = speex_echo_state_init(NN, TAIL);
den = speex_preprocess_state_init(NN, sampleRate);
speex_echo_ctl(st, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
speex_preprocess_ctl(den, SPEEX_PREPROCESS_SET_ECHO_STATE, st);
while (!feof(ref_fd) && !feof(echo_fd))
{
fread(ref_buf, sizeof(short), NN, ref_fd);
fread(echo_buf, sizeof(short), NN, echo_fd);
//ref_buf是采集到的声音,至少包含两部分:我们的说话声和喇叭放出来的声音(echo_buf)
//echo_buf是喇叭放出来的声音
//e_buf是干净的声音
speex_echo_cancellation(st, ref_buf, echo_buf, e_buf);
speex_preprocess_run(den, e_buf);
fwrite(e_buf, sizeof(short), NN, e_fd);
}
speex_echo_state_destroy(st);
speex_preprocess_state_destroy(den);
fclose(e_fd);
fclose(echo_fd);
fclose(ref_fd);
return 0;
}
WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的技术。
WebRTC源代码中设计了两个回声消除模块,AEC(Acoustic Echo Canceller)和AECM(Acoustic Echo Canceller Mobile),AEC是在电脑端使用的回声消除器,而AECM是在移动端使用。由于电脑与移动设备的差别比较明显,在处理速度上,编解码器的性能上和内存方面都有着较大的差异。
在VoIP实时语音通话中,之所以会产生声学回声的原因是,近端通话者的声音被自己的麦克风拾取后通过网络传到远端,远端扬声器播放出来的声音被麦克风拾取后通过网络又重新发回近端。加上网络和数据处理等各种延迟的影响,使得近端通话者能够从扬声器中听到自己的刚才所说的话,就产生了回声。
下面主要来简单介绍一下AECM模块 中echo_control_mobile.c的两个函数:
//farend 是远端传过来的音频数据,也就是将要从喇叭放出来的声音,类似于speex的echo_buf
WebRtcAecm_BufferFarend(void *aecmInst, const int16_t *farend, int16_t nrOfSamples)
//nearendNoisy 是从mic采集到的声音,类似于speex的ref_buf
//nearendClean 可以放入经过Noise Reduction的音频数据
//msInSndCarBuf 该值需要特别计算才能够取得良好的去回音效果,也即将听筒播放出来的声音被mic采集到时的一个精确的值,才能使AECM达到良好的回音消除效果
WebRtcAecm_Process(void *aecmInst, const int16_t *nearendNoisy, const int16_t *nearendClean,
int16_t *out, int16_t nrOfSamples, int16_t msInSndCardBuf)
Java(Android )
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
//初始化QttAudioEngine
QttAudioEngine.me().init(this, "your appkey");
//创建QttAudioStream
QttAudioStream stream = QttAudioEngine.me().createStream();
//设置mic的编码参数,pcm,单声道,48000hz采样率
QttAudioEngine.me().setMicCodecParams("pcm", 1, 48000, 0);
//设置mic采集回调函数
QttAudioEngine.me().setMicCaptureCb(new QttCaptureCallbak() {
@Override
public void onCapture(byte[] buf, int bufLength, Object userdata) {
QttAudioStream tmpStream = (QttAudioStream) userdata;
//将mic采集到的播放出来,实现音频回放功能
tmpStream.writePayload(buf, 0, bufLength);
}
}, stream);
//启动stream,开始工作
stream.start();
} catch (QttException e) {
e.printStackTrace();
}
}
}
static void onCapture(char *buf, size_t len, void *userdata) {
QttAudioStream *stream = (QttAudioStream *)userdata;
//将mic采集到的播放出来,实现音频回放功能
qtt_stream_write_payload(stream, buf, len);
}
int main() {
//创建QttAudioEngine
QttAudioEngine *engine = qtt_engine_init("your appkey");
if (engine == NULL) {
fprintf(stderr, "qtt_engine_init fail\n");
return -1;
}
//创建QttAudioStream
QttAudioStream *stream = qtt_engine_create_stream(engine);
if (stream == NULL) {
fprintf(stderr, "qtt_engine_create_stream fail\n");
return -1;
}
//设置mic的编码参数,pcm,单声道,48000hz采样率
qtt_engine_set_mic_codec_params(engine, "pcm", 1, 48000, 0);
//设置mic采集回调函数
qtt_engine_set_mic_capture_callback(engine, onCapture, stream);
//启动stream,开始工作
qtt_stream_start(stream);
char c = getchar();
qtt_stream_stop(stream);
qtt_stream_free(stream);
qtt_engine_free(engine);
return 0;
}