基於Speex的聲學回聲消除

 所謂聲學回聲消除,是爲了解決VoIP(網絡電話)中這樣一個問題:即A與B進行通話,A端有麥克風和揚聲器分別用來採集A的聲音和播放B的聲音,B端有麥克風和揚聲器分別用來採集B的聲音和播放A的聲音,很明顯,由於聲音傳播的特性,A端的麥克風在採集A的聲音的同時,也採集到了A端揚聲器播放的來自B的聲音,也就是A端採集到的聲音是一個混合的聲音,這個聲音通過網絡發給B時,B就不僅能聽到A的聲音,也能聽見B前幾秒自己的聲音,這就是在B端聽到了B自己的回聲,同理在A端也可以聽到A自己的回聲,這顯然不是我們想要的。

        聲學回聲消除一般可以通過硬件和軟件分別實現,目前來說,硬件實現比較簡單,軟件實現較難,這裏的難並不是說回聲消除算法很難,而是在應用算法時的實時同步問題很難,目前軟件實現較好的應該是微軟,但似乎也對硬件配置和操作系統有要求。而Speex提供了聲學回聲消除算法庫,本文就簡單用Speex對一段錄音進行回聲消除,當然這不是實時處理的。

        Speex中回聲消除API封裝在語音處理API中,在最新版本的Speex中將語音處理相關的API獨立封裝成libspeexdsp。

        應用Speex回聲消除API流程很簡單:包含相關頭文件——創建回聲消除器狀態——對每幀進行回聲消除——銷燬回聲消除器狀態。一般可以與Speex中的預處理器一起使用,已達到較好的聲音效果,應用預處理器API的流程也很簡單:包含相關頭文件——創建預處理器狀態——對每幀進行預處理——銷燬預處理器狀態。當然可以設置預處理器狀態,在此我們使用默認設置。可以看到兩者流程相同,因而寫成一個CSpeexEC類,這是一個開源的回聲消除器,對其中的兩個函數調用做了稍微的修改。原文參見http://www.360doc.com/content/11/1008/18/11192_154383516.shtml,原文所用speex版本是1.1.9,我們用的是speex-1.2beta3-win32,執行預處理和回聲消除的函數進行了更新。

1、speexEC.h

       

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
#ifndef SPEEX_EC_H
#define SPEEX_EC_H
#include <stdio.h>
#include <stdlib.h>
/*在項目屬性裏設置VC++目錄的包含目錄和庫目錄分別爲speex庫中的include和lib,我用的是speex-1.2beta3-win32*/
#include <speex/speex_echo.h>
#include <speex/speex_preprocess.h>
class CSpeexEC
{
public:
CSpeexEC();
~CSpeexEC();
void Init(int frame_size=160, int filter_length=1280, int sampling_rate=8000);
void DoAEC(short *mic, short *ref, short *out);
protected:
void Reset();
private:
bool m_bHasInit;
SpeexEchoState* m_pState;
SpeexPreprocessState* m_pPreprocessorState;
int m_nFrameSize;
int m_nFilterLen;
int m_nSampleRate;
spx_int32_t* m_pfNoise;
};
#endif
 來自CODE的代碼片
speexEC.h

2、speexEC.cpp

       

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
#include "stdafx.h"
#include "SpeexEC.h"
CSpeexEC::CSpeexEC()
{
m_bHasInit = false;
m_pState = NULL;
m_pPreprocessorState = NULL;
m_nFrameSize = 160;
m_nFilterLen = 160*8;
m_nSampleRate = 8000;
m_pfNoise = NULL;
}
CSpeexEC::~CSpeexEC()
{
Reset();
}
void CSpeexEC::Init(int frame_size, int filter_length, int sampling_rate)
{
Reset();
if (frame_size<=0 || filter_length<=0 || sampling_rate<=0)
{
m_nFrameSize =160;
m_nFilterLen = 160*8;
m_nSampleRate = 8000;
}
else
{
m_nFrameSize =frame_size;
m_nFilterLen = filter_length;
m_nSampleRate = sampling_rate;
}
m_pState = speex_echo_state_init(m_nFrameSize, m_nFilterLen);
m_pPreprocessorState = speex_preprocess_state_init(m_nFrameSize, m_nSampleRate);
m_pfNoise = new spx_int32_t[m_nFrameSize+1];
m_bHasInit = true;
}
void CSpeexEC::Reset()
{
if (m_pState != NULL)
{
speex_echo_state_destroy(m_pState);
m_pState = NULL;
}
if (m_pPreprocessorState != NULL)
{
speex_preprocess_state_destroy(m_pPreprocessorState);
m_pPreprocessorState = NULL;
}
if (m_pfNoise != NULL)
{
delete []m_pfNoise;
m_pfNoise = NULL;
}
m_bHasInit = false;
}
void CSpeexEC::DoAEC(short* mic, short* ref, short* out)
{
if (!m_bHasInit)
return;
/*1.1.9版本中所使用的函數*/
//speex_echo_cancel(m_pState, mic, ref, out, m_pfNoise);
//speex_preprocess(m_pPreprocessorState, (__int16 *)out, m_pfNoise);
/*1.2beta3-win32版本中使用的函數,從參數可以看出最新版本沒有參數m_pfNoise,所以CSpeex中可以刪除數據成員m_pfNoise*/
/*本文依然保留,是爲了測試兩個版本的差別,從結果來看,至少人耳似乎聽不出有多大差別*/
speex_echo_cancellation(m_pState, mic, ref, out);
speex_preprocess_run(m_pPreprocessorState, (__int16 *)out);
}
 來自CODE的代碼片
speexEC.cpp

3、測試驅動文件echocancel.cpp

       

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
#include "stdafx.h"
#include "speexEC.h"
#include <stdio.h>
#include <speex/speex_echo.h>
#define NN 160
void main()
{
FILE *ref_fd;
FILE *mic_fd;
FILE *out_fd;
short ref[NN];
short mic[NN];
short out[NN];
ref_fd = fopen("FarEnd.pcm", "r+b");
mic_fd = fopen("NearEnd.pcm", "r+b");
out_fd = fopen("out.pcm", "w+b");
CSpeexEC ec;
ec.Init();
while (fread(mic, 1, NN*2, mic_fd))
{
fread(ref, 1, NN*2, ref_fd);
ec.DoAEC(mic, ref, out);
fwrite(out, 1, NN*2, out_fd);
}
fclose(ref_fd);
fclose(mic_fd);
fclose(out_fd);
}
 來自CODE的代碼片
echocancel.cpp

        程序需要兩個FarEnd.pcm和NearEnd.pcm文件作爲輸入,輸出out.pcm文件,其中FarEnd.pcm爲遠端回放音頻,即待消除的回聲參考文件,NearEnd.pcm是近端麥克風採集音頻,是人的語音和回聲的混合音頻,out.pcm是對NearEnd.pcm進行回聲消除後的文件。我們用matlab將純淨語音與FarEnd.pcm線性相加來產生NearEnd.pcm,其中採樣率爲8kHz,這樣FarEnd與NearEnd是嚴格同步的,注意,是線性相加,而且可以將FarEnd乘以一個幅值再與純淨語音相加。從下圖和聽覺結果來看,完全同步時回聲消除效果很好。

 

                                                      FarEnd-8kHz.pcm

  

                                                      NearEnd-8kHz.pcm

                             

                                                         out-8kHz.pcm

        但實際VoIP中麥克風採集到的人的語音和揚聲器播放的聲音並不是簡單的線性混合,一方面是由於房間內的混響,它們更接近於卷積混合;更重要的是,麥克風採集到的回放和參考回放不是嚴格同步的,即FarEnd.pcm信號與NearEnd.pcm中的回放FarEnd信號不是同步的,一般會有幾幀的延時,這是因爲FarEnd.pcm是直接從聲卡提取的,而NearEnd.pcm中的回放FarEnd信號是經過聲卡經揚聲器播放,再被麥克風採集的。

        爲此,我們選取一段歌曲作爲FarEnd.pcm,在播放的FarEnd.pcm的同時開始錄音,產生NearEnd.pcm。程序進行回聲消除的結果如下:

               

                                                        FarEnd1-8kHz.pcm

               

                                                         NearEnd1-8kHz.pcm

               

                                                           out1-8kHz.pcm

        從上圖及聽力結果來看,在這樣沒同步的情況下,回聲消除的效果並不理想。注意FarEnd.pcm參考回放的幅值比NearEnd.pcm錄音到的回放幅值大,這是因爲聲音在傳播過程中是會衰減的。

        上圖的結果是在採樣率爲8kHz下進行的,一個有趣的現象是,若提高採樣率,效果似乎變好了。爲此,我們將音頻採樣率轉爲44.1kHz並相應修改程序中的採樣率,得到結果如下圖所示:

 

                                                          out2-44.1kHz.pcm

        從上圖來看似乎看不出效果變好,但是從聽覺效果來看,回聲的的確確是變小了。這應該跟回聲消除算法的收斂有關,因爲採樣率變大,每秒的採樣點多。採樣率爲8kHz,幀長160對應20ms;採樣率爲44.1kHz,幀長160對應約3.6ms,所以可能跟幀大小相關。具體什麼原因,我還沒想到一個很好的解釋方法。

        以上就是基於Speex的回聲消除簡單應用,在能保證同步的情況下效果不錯,但不同步時效果變差,可以適當提高採樣率(frame_size不變)以提高處理效果。

發佈了117 篇原創文章 · 獲贊 294 · 訪問量 66萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章