陳亮蒿 李煒
(1 北京郵電大學(xué)網(wǎng)絡(luò)與交換技術(shù)國家重點(diǎn)實(shí)驗(yàn)室 北京 100876)
(2 東信北郵信息技術(shù)有限公司 北京 100191)
VoIP的基本原理是由專門設(shè)備或軟件將呼叫方的語音信號(hào)采樣并數(shù)字化、壓縮、轉(zhuǎn)換為一定長度的數(shù)字化語音分組,以數(shù)據(jù)分組的形式經(jīng)過分組交換網(wǎng)絡(luò)傳輸?shù)綄?duì)方,對(duì)方接收到數(shù)據(jù)分組后解壓縮,還原成語音信號(hào)。它提供了低成本、高靈活性、高效率的增強(qiáng)應(yīng)用。IP在硬件、軟件和網(wǎng)絡(luò)協(xié)議等方面的提高和發(fā)展,進(jìn)一步推動(dòng)了新的集成化基礎(chǔ)設(shè)施的迅速發(fā)展。
隨著PC、手機(jī)、寬帶網(wǎng)越來越普及,VoIP應(yīng)用越來越廣泛,不斷影響著人們的工作生活。基于Windows平臺(tái)的VoIP客戶端無疑是應(yīng)用最廣泛的,如QQ、MSN、Skype等。但Windows的版本眾多,各個(gè)版本提供的 API (Application Programming Interface)也不盡相同,開發(fā)時(shí)稍不注意就會(huì)嚴(yán)重影響語音質(zhì)量。
本文提出了一套Windows平臺(tái)下開發(fā)語音引擎的解決方案,有效解決了不同版本下引擎的兼容問題,使得引擎在不同Windows版本下能夠體現(xiàn)出各自音頻處理的優(yōu)勢(shì)。利用系統(tǒng)級(jí)API,討論了Windows XP、Vista、Windows 7下錄放音技術(shù)、回聲消除技術(shù)的實(shí)現(xiàn),并且通過算法設(shè)計(jì)有效解決了延遲抖動(dòng)問題。
VoIP引擎的總體結(jié)構(gòu)主要包括兩個(gè)部分:處理音頻的核心控制模塊和處理本地文件的文件播放模塊。基于這兩個(gè)核心模塊,可以在上層封裝出各種對(duì)外功能接口,例如針對(duì)C++的DLL (Dynamic Link Library),或者針對(duì)Java、C#等的應(yīng)用接口。系統(tǒng)總體結(jié)構(gòu)如圖1所示。

圖1 系統(tǒng)總體結(jié)構(gòu)
(1) 本地文件播放模塊:負(fù)責(zé)對(duì)本地的媒體文件進(jìn)行操作,如播放某文件、停止播放、是否循環(huán)播放等;
(2) 引擎核心控制模塊:通過調(diào)用下層模塊的功能接口,進(jìn)行相關(guān)數(shù)據(jù)邏輯處理,控制數(shù)據(jù)流以及播放錄制緩存等。而且,它對(duì)上層也提供了其調(diào)用接口及回調(diào)接口。下層模塊主要包括文件音頻模塊、會(huì)話模塊、設(shè)備音頻模塊、音頻編解碼模塊;
(3) 文件音頻模塊:負(fù)責(zé)音頻文件作為媒體傳輸源時(shí)的相關(guān)控制及信息保存,包括數(shù)據(jù)分發(fā)列表(一個(gè)音頻源在同一時(shí)刻對(duì)應(yīng)幾個(gè)會(huì)話)、音頻文件的文件路徑、文件中存儲(chǔ)的音頻編解碼格式等;
(4) 會(huì)話模塊:負(fù)責(zé)一路會(huì)話的描述。一路會(huì)話對(duì)應(yīng)一個(gè)RTP模塊,并且保存會(huì)話的其它相關(guān)信息,如當(dāng)前數(shù)據(jù)源是麥克風(fēng)輸入還是某個(gè)媒體文件、語音的收發(fā)模式(同時(shí)收發(fā)、只收不發(fā)、只發(fā)不收、不收不發(fā))、音頻編解碼格式等;
(5) 設(shè)備音頻處理模塊:包括音頻的錄制、播放,音頻設(shè)備的管理,以及音量的控制等。待播放的數(shù)據(jù)從引擎核心控制模塊中獲取,已錄制的數(shù)據(jù)也直接交給引擎核心控制模塊進(jìn)行處理;
(6) 音頻編解碼模塊:負(fù)責(zé)各種音頻編碼格式的相互轉(zhuǎn)換。將錄制的音頻轉(zhuǎn)換為指定編碼格式進(jìn)行傳輸,對(duì)接收到的音頻數(shù)據(jù)進(jìn)行解碼等。
2.1.1 Windows XP系統(tǒng)
Direct Sound是一套基于COM(Component Object Mode)接口的Windows應(yīng)用層音頻控制API,在Windows XP及其以前版本,作為標(biāo)準(zhǔn)的音頻應(yīng)用程序開發(fā)接口提供給開發(fā)者。底層直接連接驅(qū)動(dòng),有些底層接口直接由硬件實(shí)現(xiàn),運(yùn)行效率很高。
Direct Sound的功能包括音頻設(shè)備的控制、音量控制、錄放音。我們關(guān)心的錄放音也分為3種:播放、錄音和錄放雙工。如果要實(shí)現(xiàn)回聲消除(Acoustic Echo Cancellation, AEC),必須采用第3種錄放雙工的方式進(jìn)行初始化。對(duì)應(yīng)的初始化函數(shù)分別為:Direct Sound Create8、Direct Sound Capture Create8、Direct Sound Full Duplex Create8。從應(yīng)用效果看,這3個(gè)函數(shù)的效果就是獲得兩個(gè)接口:IDirect Sound8和IDirect Sound Capture8。其中,Direct Sound Create8獲取IDirect Sound8,Direct Sound Capture Create8獲取IDirect Sound Capture8,而Direct Sound Full Duplex Create8則是同時(shí)獲取兩個(gè)接口。
獲取總體接口后,用Direct Sound Create8函數(shù)初始化的程序必須調(diào)用Set Cooperative Level函數(shù)設(shè)置播放的優(yōu)先級(jí),否則后續(xù)步驟會(huì)失敗。
然后,要初始化播放或錄音的Buffer。初始化時(shí),將播放音的屬性、音頻格式、緩存大小、特效標(biāo)識(shí)等信息作為參數(shù),在初始化函數(shù)調(diào)用時(shí)告訴Direct Sound。具體設(shè)置哪些值和如何設(shè)置,請(qǐng)參看微軟Direct Sound幫助文檔。
Direct Sound錄放音有兩種形式:一種是靜態(tài)緩存,即一次性把要播放的音頻數(shù)據(jù)導(dǎo)入緩存;另一種是流模式,即播放錄制過程中會(huì)不斷有數(shù)據(jù)補(bǔ)充交互,緩存會(huì)被復(fù)用。對(duì)于我們的應(yīng)用,主要關(guān)心流模式。基于流模式,Direct Sound提供了buffer通知模型,即通過設(shè)置函數(shù),播放或者完成錄制多少字節(jié)的Buffer后,就發(fā)送一個(gè)Event事件。外部線程可以攔截響應(yīng)這個(gè)事件,對(duì)Buffer的數(shù)據(jù)進(jìn)行處理,實(shí)現(xiàn)連貫的平滑的錄放音。方法是通過IDirect Sound Buffer8或者IDirect Sound Capture Buffer的Query Interface方法獲取IDirect Sound Notify8接口,然后調(diào)用該接口的Set Notification Positions方法,設(shè)置通知事件和事件通知點(diǎn)。
由于CPU是被搶占運(yùn)行的,因此,通知時(shí)一般都有微小時(shí)間滯后。所以,事件觸發(fā)后的處理,如果有對(duì)緩存的精確處理,需要重新獲取緩存狀態(tài)信息。之后,就可以調(diào)用buffer的Start函數(shù),開始錄音或者放音;調(diào)用buffer的Stop函數(shù)可以立即結(jié)束錄音或者放音。
2.1.2 Vista與Windows 7系統(tǒng)
Vista和Windows 7在音頻播放錄制方面提出了一套全新的概念和架構(gòu)。在設(shè)備上,微軟取消了聲卡的概念,取而代之的是播放設(shè)備、麥克風(fēng)和線路輸入設(shè)備。Vista和Windows 7包含了多套應(yīng)用層音頻API,有的是老版本W(wǎng)indows的API,如Wave Xxx和Direct Sound,有的是新的,如Media Foundation中的SAR。在這些應(yīng)用層之下,微軟還開放了一套層次較低的公共API——Core Audio API,用于一些實(shí)時(shí)性較高或者對(duì)設(shè)備控制有較高需求的應(yīng)用,如圖2所示。

圖2 Vista和Windows 7操作系統(tǒng)的音頻處理架構(gòu)
在Vista及以后版本,Direct Sound不再作為微軟主流音頻應(yīng)用接口,但微軟在Vista和Windows 7上仍然保留了Direct Sound的大多數(shù)接口,使得一些老程序可以運(yùn)行。但是,該接口變成一套軟件接口進(jìn)行維護(hù),因此有些程序在Windows XP下聲音很流暢,而在Vista和Windows 7下卻不好,因此這里討論的錄放音技術(shù)是直接基于Core Audio API。
由于Core Audio API的定位是針對(duì)音頻設(shè)備的操作,而并不包括對(duì)音頻的處理。因此,我們的應(yīng)用還需要自己手動(dòng)加入音頻處理相關(guān)的模塊,如重抽樣和AEC等,微軟提供了DSP (Digital Signal Processor)系列接口對(duì)音視頻等進(jìn)行處理。
錄音的總體流程如下。
(1) Core Audio API調(diào)用錄制音頻API,初始化并開始錄音;
(2) 通過流操作,定期獲取錄制的音頻數(shù)據(jù);
(3) 將數(shù)據(jù)交給DSP的重抽樣接口,變換為我們需要格式的數(shù)據(jù);
(4) 將重抽樣處理過的數(shù)據(jù)傳遞給DSP的AEC回聲消除接口;
(5) 將經(jīng)過回聲消除處理過的數(shù)據(jù)交給應(yīng)用程序處理。
這個(gè)流程是標(biāo)準(zhǔn)的流程,不過DSP的AEC接口還提供了Source Mode類型接口,將前4個(gè)步驟合并為一個(gè)接口,大大簡化了操作。簡化后的錄音流程如下。
(1) 啟用并初始化DSP的AEC接口,模式要選擇Source Mode;
(2) 用AEC接口開始錄音,并定期取出數(shù)據(jù)交給應(yīng)用程序處理。
放音的總體流程如下。
(1) Core Audio API調(diào)用播放音頻API,初始化并獲得接口對(duì)象;
(2) 初始化并設(shè)置好重抽樣DSP;
(3) 獲取并鎖定播放緩沖,向其中填充通過重抽樣處理過之后的數(shù)據(jù),并解鎖緩沖;
(4) 開始播放 ;
(5) 另起線程,定期重復(fù)第3步,補(bǔ)充音頻數(shù)據(jù),直到終止播放。
2.2.1 Windows XP系統(tǒng)
在Windows XP操作系統(tǒng)下,微軟提供了基于Direct Sound的AEC解決方案。在Windows XP下啟用AEC,必須在DirectSound初始化時(shí),采用DirectSound FullDuplexCreate8函數(shù),及必須以雙工的方式進(jìn)行初始化,將播放緩沖、錄制緩沖及音頻格式等信息作為參數(shù)傳進(jìn)去。注意結(jié)構(gòu)體DSCBUFFERDESC的dwFlags一定要包含DSCBCAPS_CTRLFX標(biāo)識(shí),并且DSCEFFECTDESC一定要做如下初始化,并把指針傳給DSCBUFFERDESC的lpDSCFXDesc。
// 參數(shù):施加于錄制緩沖區(qū)的效果描述,AEC & NS DSCEFFECTDESC dsced[2];
ZeroMemory( &dsced[0], sizeof(DSCEFFECTDESC ) );
dsced[0].dwSize = sizeof(DSCEFFECTDESC);
dsced[0].dwFlags = DSCFX_LOCSOFTWARE;
dsced[0].guidDSCFXClass = GUID_DSCFX_CLASS_AEC;
dsced[0].guidDSCFXInstance = GUID_DSCFX_MS_AEC;
dsced[0].dwReserved1 = 0;
dsced[0].dwReserved2 = 0;
ZeroMemory( &dsced[1], sizeof( DSCEFFECTDESC) );
dsced[1].dwSize = sizeof(DSCEFFECTDESC);
dsced[1].dwFlags = DSCFX_LOCSOFTWARE;
dsced[1].guidDSCFXClass = GUID_DSCFX_CLASS_NS;
dsced[1].guidDSCFXInstance = GUID_DSCFX_MS_NS;
dsced[1].dwReserved1 = 0;
dsced[1].dwReserved2 = 0;
// 參數(shù):錄制緩沖區(qū)描述
DSCBUFFERDESC dscbd;
dscbd.dwSize = sizeof(DSCBUFFERDESC);
dscbd.dwFlags = DSCBCAPS_CTRLFX;
dscbd.dwBufferBytes = m_dwBlockNum * m_dwBlockSize ;
dscbd.dwReserved = 0;
dscbd.lpwfxFormat = &m_wfxRecord;
dscbd.dwFXCount = 2;
dscbd.lpDSCFXDesc = dsced;
然后,將DSCFXAec結(jié)構(gòu)體中的dwMode設(shè)置為DSCFX_AEC_MODE_FULL_DUPLEX,并通過錄音接口的函數(shù)進(jìn)行設(shè)置。后面的步驟和一般DirectSound的錄放音步驟相同,先分別設(shè)置緩存的上報(bào)通知事件點(diǎn),然后初始化并填充播放緩存,開始播放,開始錄制等。詳情請(qǐng)參看微軟DirectSound幫助文檔。
2.2.2 Vista與Windows 7系統(tǒng)
在Vista和Windows 7操作系統(tǒng)中,微軟提供了DSP系列接口對(duì)音視頻等進(jìn)行處理。其中,和音頻相關(guān)的處理,包括AEC和重抽樣Resample。盡管Windows 7較Vista有不少新的功能和應(yīng)用接口,不過它們共用的接口(Core Audio API)已經(jīng)能夠滿足我們的應(yīng)用需求。
實(shí)現(xiàn)AEC的過程如下:首先通過調(diào)用COM組件的方法,通過CoCreateInstance函數(shù)獲取CLSID_CWMAudioAEC的組件接口IMediaObject。再通過該接口的QueryInterface函數(shù),獲取IPropertyStore接口。通過IPropertyStore接口的SetValue函數(shù),將MFPKEY_WMAAECMA_SYSTEM_MODE屬性的值設(shè)為SINGLE_CHANNEL_AEC,即采用單聲道AEC模式。其它的屬性可根據(jù)用戶需要,分別設(shè)置。之后,通過IMediaObject接口的SetOutputType函數(shù),用DMO_MEDIA_TYPE結(jié)構(gòu)體作為參數(shù)傳入,設(shè)置輸出的音頻格式。然后調(diào)用IMediaObject接口的AllocateStreamingResources函數(shù)開始錄制。詳情請(qǐng)參看微軟Windows SDK文檔及Demo程序。
對(duì)不同的操作系統(tǒng)版本編寫設(shè)備音頻處理模塊,并封裝成不同的類,類之間無耦合關(guān)系。同時(shí),提取出公共接口,與核心控制模塊交互。
通過操作系統(tǒng)提供的系統(tǒng)API獲取當(dāng)前操作系統(tǒng)的版本,并根據(jù)相應(yīng)的版本,初始化對(duì)應(yīng)的設(shè)備音頻處理模塊。辨別操作系統(tǒng)版本的API為GetVersionEx,該API支持的最低版本的操作系統(tǒng)為Windows 2000,具體用法詳見微軟MSDN幫助文檔。常見Windows操作系統(tǒng)版本如表1所示。
IP網(wǎng)絡(luò)的一個(gè)特征就是網(wǎng)絡(luò)延遲與抖動(dòng),這將導(dǎo)致IP電話音質(zhì)下降。網(wǎng)絡(luò)延遲是指1個(gè)IP分組在網(wǎng)絡(luò)上傳輸所需的時(shí)間,抖動(dòng)是指IP分組傳輸時(shí)間的長短變化。如果抖動(dòng)較嚴(yán)重,那么有的語音分組會(huì)因遲到而被丟棄,產(chǎn)生語音的斷續(xù)及部分失真,嚴(yán)重影響音質(zhì)。為了降低或者消除抖動(dòng)的影響,我們采用緩沖技術(shù),即在接收方設(shè)定1個(gè)緩沖區(qū),語音分組到達(dá)時(shí)首先進(jìn)入緩沖池暫存,系統(tǒng)以穩(wěn)定平滑的速率將語音分組從緩沖池中取出、解壓、播放給收話者。這種緩沖技術(shù)可以在一定限度內(nèi)有效處理語音抖動(dòng),提高音質(zhì)。接下來,我們將以消除或者降低延遲抖動(dòng)的影響從而平滑語音流、提高語音質(zhì)量為目的,分別針對(duì)發(fā)送端與接收端進(jìn)行探討。

表1 常見Windows操作系統(tǒng)版本
在發(fā)送端,需要穩(wěn)定地發(fā)送數(shù)據(jù)分組。由于發(fā)送端的聲卡自身的定時(shí)器不一定準(zhǔn)確,可能偏快或偏慢:若偏快,將導(dǎo)致在單位時(shí)間內(nèi)發(fā)送的數(shù)據(jù)分組比預(yù)期的多,時(shí)間一長,接收端緩存而不能及時(shí)處理的數(shù)據(jù)分組越來越多,最終導(dǎo)致播放延遲越來越大,即延遲擴(kuò)大。如PC終端呼叫手機(jī)或固定電話時(shí),即會(huì)發(fā)生如此情況,因此,我們必須在發(fā)送端控制數(shù)據(jù)分組的平穩(wěn)發(fā)送,避免延遲擴(kuò)大。
首先,采用比較準(zhǔn)確的定時(shí)器(如CPU定時(shí)器);然后設(shè)置一個(gè)相對(duì)較長的計(jì)數(shù)周期t(時(shí)間較短則相對(duì)誤差較大,無可信性),如10s,計(jì)算出在該時(shí)間段內(nèi)理論上應(yīng)發(fā)出的數(shù)據(jù)分組個(gè)數(shù)a。(其中,若8kHz的采樣率,1個(gè)數(shù)據(jù)分組含160個(gè)采樣,表明1s發(fā)送50個(gè)數(shù)據(jù)分組,每20ms發(fā)送1個(gè)數(shù)據(jù)分組)
在發(fā)分組過程中,同時(shí)做如下處理。
Step1:在第n個(gè)計(jì)數(shù)周期內(nèi),通過計(jì)數(shù)器計(jì)算出實(shí)際向外發(fā)送的數(shù)據(jù)分組個(gè)數(shù)bn。若n=1,轉(zhuǎn)Step2;否則轉(zhuǎn)Step3。
Step2:若bn>a,即發(fā)分組速率偏快,記錄cn=bn-a;否則記錄cn=0。轉(zhuǎn)Step5。
Step3:若cn-1>0,則在當(dāng)前計(jì)數(shù)周期內(nèi)均勻丟棄cn-1個(gè)數(shù)據(jù)分組,即每隔t/cn-1時(shí)間丟棄1個(gè)數(shù)據(jù)分組。轉(zhuǎn)Step4。
Step4:若bn-a+cn-1>0,記錄cn=bn-a+cn-1;否則記錄cn=0。轉(zhuǎn)Step5。
Step5:n++。轉(zhuǎn)Step1。
流程圖如圖3所示。
這樣就能控制發(fā)送端的數(shù)據(jù)分組實(shí)際速率與理論速率達(dá)到動(dòng)態(tài)平衡,防止傳輸過多的數(shù)據(jù)分組,避免延遲擴(kuò)大。因丟棄分組是均勻的,用戶感覺不到這微小的差異,故此方案可行。

圖3 發(fā)送端數(shù)據(jù)分組的緩存控制

圖4 接收端數(shù)據(jù)分組的緩存控制
在接收端,需要平滑地讀取數(shù)據(jù)分組。這就需要一個(gè)緩存,來存放并整理剛來到的數(shù)據(jù)分組,在其達(dá)到一定數(shù)量時(shí),再取出相應(yīng)數(shù)據(jù)流暢地播放。如若沒有緩存,來一數(shù)據(jù)就開始播放,極有可能出現(xiàn)語音播放不流暢,先發(fā)送的數(shù)據(jù)分組也有可能后到達(dá),這樣的播放就會(huì)出現(xiàn)邏輯混亂的錯(cuò)誤。
在收包的過程中,需要做如下處理。
Step1:申請(qǐng)m大小的緩存buffer,設(shè)定閾值a與b(0 Step2:對(duì)接收到的數(shù)據(jù)分組按照其發(fā)送的原始序列進(jìn)行排序處理,并存放在buffer中,若buffer滿,則拒收當(dāng)前數(shù)據(jù)分組。記錄buffer中實(shí)際的數(shù)據(jù)分組個(gè)數(shù)c。 Step3:若c≤a,送全0數(shù)據(jù)給聲卡;否則若a 流程圖如圖4所示。 上述過程中的a、b、m的大小設(shè)定比較重要,這需要依據(jù)具體的應(yīng)用場(chǎng)合來進(jìn)行最佳的設(shè)置。上述方案可以保證數(shù)據(jù)的有序以及較為流暢地播放。