speex 回声消除(2)

为什么需要声学回声消除呢?在一般的VOIP软件或视频会议系统中,假设我们只有A和B两个人在通话,首先,A的声音传给B,B然后用喇叭放出来,而这时B的MIC呢则会采集到喇叭放出来的声音,然后传回给A,如果这个传输的过程中时延足够大,A就会听到一个和自己刚才说过的话一样的声音,这就是回声,声学回声消除器的作用就是在B端对B采集到的声音进行处理,把采集到声音包含的A的声音去掉再传给A,这样,A就不会听到自己说过的话了。
声学回声消除的原理我就不说了,这在网上有很多文档,网上缺少的是实现,所以,我在这把一个开源的声学回声消除器介绍一下,希望对有些有人用,如果有人知道怎么把这消除器用的基于实时流的VOIP软件中,希望能一起分享一下。
这个声学回声消除器是一个著名的音频编解码器speex中的一部分,1.1.9版本后的回声消除器才起作用,以前版本的都不行,我用的也是这个版本,测试表明,用同一个模拟文件,它有效果比INTEL IPP库4.1版中的声学回声消除器的还要好。
先说编译。首先,从www.speex.org上下载speex1.1.9的源代码,解压,打开speex\win32\libspeex中的libspeex.dsw,这个工作区里有两个工程,一个是 libspeex,另一个是libspeex_dynamic。然后,将libspeex中的mdf.c文件添加到工程libspeex中,编译即可。

以下是我根据文档封装的一个类,里面有一个测试程序: //file name: speexEC.h

[html]  view plain  copy
 print ?
  1. #ifndef SPEEX_EC_H  
  2. #define SPEEX_EC_H  
  3. #include <stdio.h>  
  4. #include <stdlib.h>  
  5. #include "speex/speex_echo.h"  
  6. #include "speex/speex_preprocess.h"  
  7. class CSpeexEC  
  8. {  
  9. public:  
  10. CSpeexEC();  
  11. ~CSpeexEC();  
  12. void Init(int frame_size=160, int filter_length=1280, int sampling_rate=8000);  
  13. void DoAEC(short *mic, short *ref, short *out);  
  14.   
  15. protected:  
  16. void Reset();  
  17.   
  18. private:  
  19. bool      m_bHasInit;  
  20. SpeexEchoState*   m_pState;  
  21.     SpeexPreprocessState* m_pPreprocessorState;  
  22. int      m_nFrameSize;  
  23. int      m_nFilterLen;  
  24. int      m_nSampleRate;  
  25. float*      m_pfNoise;  
  26. };  
  27.   
  28. #endif  

[html]  view plain  copy
 print ?
  1. //fine name:speexEC.cpp  
  2. #include "SpeexEC.h"  
  3.   
  4. CSpeexEC::CSpeexEC()  
  5. {  
  6. m_bHasInit   = false;  
  7. m_pState   = NULL;  
  8. m_pPreprocessorState  = NULL;  
  9. m_nFrameSize   = 160;  
  10. m_nFilterLen   = 160*8;  
  11. m_nSampleRate   = 8000;  
  12. m_pfNoise   = NULL;  
  13. }  
  14.   
  15. CSpeexEC::~CSpeexEC()  
  16. {  
  17. Reset();  
  18. }  
  19.   
  20. void CSpeexEC::Init(int frame_size, int filter_length, int sampling_rate)  
  21. {  
  22. Reset();  
  23.   
  24. if (frame_size<=0 || filter_length<=0 || sampling_rate<=0)  
  25. {  
  26.   m_nFrameSize  =160;  
  27.   m_nFilterLen  = 160*8;  
  28.   m_nSampleRate = 8000;  
  29. }  
  30. else  
  31. {  
  32.   m_nFrameSize  =frame_size;  
  33.   m_nFilterLen  = filter_length;  
  34.   m_nSampleRate = sampling_rate;  
  35. }  
  36.   
  37. m_pState = speex_echo_state_init(m_nFrameSize, m_nFilterLen);  
  38. m_pPreprocessorState = speex_preprocess_state_init(m_nFrameSize, m_nSampleRate);  
  39. m_pfNoise = new float[m_nFrameSize+1];  
  40. m_bHasInit = true;  
  41. }  
  42.   
  43. void CSpeexEC::Reset()  
  44. {  
  45. if (m_pState != NULL)  
  46. {  
  47.   speex_echo_state_destroy(m_pState);  
  48.   m_pState = NULL;  
  49. }  
  50. if (m_pPreprocessorState != NULL)  
  51. {  
  52.   speex_preprocess_state_destroy(m_pPreprocessorState);  
  53.   m_pPreprocessorState = NULL;  
  54. }  
  55. if (m_pfNoise != NULL)  
  56. {  
  57.   delete []m_pfNoise;  
  58.   m_pfNoise = NULL;  
  59. }  
  60. m_bHasInit = false;  
  61. }  
  62.   
  63. void CSpeexEC::DoAEC(short* mic, short* ref, short* out)  
  64. {  
  65. if (!m_bHasInit)  
  66.   return;  
  67.   
  68.  speex_echo_cancel(m_pState, (const spx_int16_t *)mic, (const spx_int16_t *)ref, (spx_int16_t *)out, (spx_int32_t *)m_pfNoise);  
  69.     speex_preprocess(m_pPreprocessorState, (__int16 *)out, (spx_int32_t *)m_pfNoise);  
  70.      
  71. }  

可以看出,这个回声消除器类很简单,只要初始化一下就可以调用了。但是,要注意的是,传给回声消除器的两个声音信号,必须同步得非常的好,就是说,在B端,接收到A说的话以后,要把这些话音数据传给回声消除器做参考,然后再传给声卡,声卡再放出来,这有一段延时,这时,B再采集,然后传给回声消除器,与那个参考数据比较,从采集到的数据中把频域和参考数据相同的部分消除掉。如果传给消除器的两个信号同步得不好,即两个信号找不到频域相同的部分,就没有办法进行消除了。
测试程序:
[html]  view plain  copy
 print ?
  1. #define NN 160  
  2. void main()  
  3. {  
  4. FILE* ref_fd, *mic_fd, *out_fd;  
  5. short ref[NN], mic[NN], out[NN];  
  6. ref_fd = fopen ("ref.pcm", "rb"); //打开参考文件,即要消除的声音  
  7. mic_fd = fopen ("mic.pcm",  "rb");//打开mic采集到的声音文件,包含回声在里面  
  8. out_fd = fopen ("echo.pcm", "wb");//消除了回声以后的文件  
  9.   
  10. CSpeexEC ec;  
  11. ec.Init();  
  12.   
  13. while (fread(mic, 1, NN*2, mic_fd))  
  14.    {  
  15.       fread(ref, 1, NN*2, ref_fd);    
  16.       ec.DoAEC(mic, ref, out);  
  17.       fwrite(out, 1, NN*2, out_fd);  
  18.    }  
  19.    
  20.    fclose(ref_fd);  
  21.    fclose(mic_fd);  
  22.    fclose(out_fd);  
  23. }  

以上的程序是用文件来模拟回声和MIC,但在实时流中是大不一样的,在一般的VOIP软件中,接收对方的声音并传到声卡中播放是在一个线程中进行的,而采集本地的声音并传送到对方又是在另一个线程中进行的,而声学回声消除器在对采集到的声音进行回声消除的同时,还需要播放线程中的数据作为参考,而要同步这两个线程中的数据是非常困难的,因为稍稍有些不同步,声学回声消除器中的自适应滤波器就会发散,不但消除不了回声,还会破坏原始采集到的声音,使被破坏的声音难以分辨。我做过好多尝试,始终无法用软件来实现对这两个线程中的数据进行同步,导致实现失败,希望有经验的网友们一起分享一下这方面的经验。

示例代码:


Sample code

This section shows sample code for encoding and decoding speech using the Speex API. The commands can be used to encode and decode a file by calling:
% sampleenc in_file.sw | sampledec out_file.sw
where both files are raw (no header) files encoded at 16 bits per sample (in the machine natural endianness).

sampleenc.c

sampleenc takes a raw 16 bits/sample file, encodes it and outputs a Speex stream to stdout. Note that the packing used is NOT compatible with that of speexenc/speexdec.


#include <speex/speex.h>
#include <stdio.h>

#define FRAME_SIZE 160
int main(int argc, char **argv)
{
char *inFile;
FILE *fin;
short in[FRAME_SIZE];
float input[FRAME_SIZE];
char cbits[200];
int nbBytes;

void *state;

SpeexBits bits;
int i, tmp;

state = speex_encoder_init(&speex_nb_mode);

tmp=8;
speex_encoder_ctl(state, SPEEX_SET_QUALITY, &tmp);
inFile = argv[1];
fin = fopen(inFile, "r");

speex_bits_init(&bits);
while (1)
{

fread(in, sizeof(short), FRAME_SIZE, fin);
if (feof(fin))
break;

for (i=0;i<FRAME_SIZE;i++)
input[i]=in[i];

speex_bits_reset(&bits);

speex_encode(state, input, &bits);

nbBytes = speex_bits_write(&bits, cbits, 200);

fwrite(&nbBytes, sizeof(int), 1, stdout);

fwrite(cbits, 1, nbBytes, stdout);
}

speex_encoder_destroy(state);

speex_bits_destroy(&bits);
fclose(fin);
return 0;
}

sampledec.c

sampledec reads a Speex stream from stdin, decodes it and outputs it to a raw 16 bits/sample file. Note that the packing used is NOT compatible with that of speexenc/speexdec.


#include <speex/speex.h>
#include <stdio.h>

#define FRAME_SIZE 160
int main(int argc, char **argv)
{
char *outFile;
FILE *fout;

short out[FRAME_SIZE];

float output[FRAME_SIZE];
char cbits[200];
int nbBytes;

void *state;

SpeexBits bits;
int i, tmp;

state = speex_decoder_init(&speex_nb_mode);

tmp=1;
speex_decoder_ctl(state, SPEEX_SET_ENH, &tmp);
outFile = argv[1];
fout = fopen(outFile, "w");

speex_bits_init(&bits);
while (1)
{

fread(&nbBytes, sizeof(int), 1, stdin);
fprintf (stderr, "nbBytes: %d\n", nbBytes);
if (feof(stdin))
break;

fread(cbits, 1, nbBytes, stdin);

speex_bits_read_from(&bits, cbits, nbBytes);

speex_decode(state, &bits, output);

for (i=0;i<FRAME_SIZE;i++)
out[i]=output[i];

fwrite(out, sizeof(short), FRAME_SIZE, fout);
}

speex_decoder_destroy(state);

speex_bits_destroy(&bits);
fclose(fout);
return 0;
}

在Speex(www.speex.org)的最新版本中,开始集成了回音消除的模块,而回音消除一直是Voip之中亟待解决的主要问题。
很多朋友和我说speex的aec模块的效能并不好,我们先来看一下speex的aec的api调用方式。

/*
*创建AEC对象
*/
SpeexEchoState *echo_state = speex_echo_state_init(frame_size, filter_length);

frame_size 的取值最好是一个编码的frame大小, 在低带宽条件下,一般延迟20ms,而大小为160
filter_length,最好是房间内反射时间的1/3
如: 一个房间的反射时延为300ms
    那么这个filter_length就最好是100ms(这个长度又被称为tail length).

而其中filter_length的设定是一个关键。

/*
*执行AEC
*/
speex_echo_cancel(echo_state, input_frame, echo_frame, output_frame, residue);

其中:
input_frame: 就是被声卡捕捉到的声音
echo_frame: 是由扬声器播放出的声音,这个声音是需要从 input_frame中抵消的声音.

output_frame 是处理完以后输出的声音

residue是一个可选参数,如果不使用可以将之设置为NULL, 也可以通过preprocessor 来控制

问题的关键是 处理input和echo 之间的关系,
也就是说在捕捉到的信号和播放的信号之间的延迟必须足够的小,才可以提高效率.

writetosndcard(echo_frame, frame_size)
readfromsndcard(input_frame, frame_size)
speex_echo_cancel(echo_state, input_frame, echo_frame, output_frame, residue)
如果你想要尽可能的减小信号中的回音,那么可以将residue这个参数设置为噪音参数.

我相信在大多数情况下,都是因为声音捕捉和声音播放之间的同步问题没有处理好,导致的音频质量下降。

/*
*销毁和复位
*/
speex_echo_state_destroy(echo_state);

speex_echo_state_reset(echo_state);
不再复述了!

说明:
据说在Speex的最新的1.2beta版本上,Speex提供了可选择的,简化的API,来提高echo执行过程中的同步问题。

猜你喜欢

转载自blog.csdn.net/nick123chao/article/details/77414487