win32下的錄音編程
1 引言
在win32 apis基礎(chǔ)上編寫錄音程序繁瑣易錯,使用封裝好的類是個不錯的注意。不幸的是所謂封裝好的類對你而言,往往是代碼羅嗦且功能不足,因此盡管你可能希望在某個項目上因使用封裝好的類而避開win32 apis,可最終你發(fā)現(xiàn)你還得面對它。不是為了編寫自己的類,就是為了修改別人的代碼。
win32 apis中有一組被稱成多媒體控制接口(即mci)的函數(shù),該接口提供了多媒體編程所需的系統(tǒng)級apis。對絕大多數(shù)c/c++程序員而言,這些函數(shù)也就是windows多媒體編程的最低層接口。
由于錄音代碼直接操作真實的錄音設(shè)備而非單純的邏輯過程,因此會遇到一些“意外”或與時序有關(guān)的困難,從而使編寫健壯的代碼成了一件困難的事。
錄音的目的往往是將聲音寫入文件保存下來或是通過網(wǎng)絡(luò)發(fā)送,這兩類需求對錄音代碼影響較大。前者無實時性要求,一般也不限制數(shù)據(jù)量,代碼較易編寫,而后者既對實時性又要求,又對數(shù)據(jù)量(也就是音頻格式)有要求,且網(wǎng)絡(luò)系統(tǒng)的穩(wěn)定性也難比文件系統(tǒng)。期望在本文中試圖解決所有的問題是不現(xiàn)實的,諸如如何壓縮音頻數(shù)據(jù)以減小網(wǎng)絡(luò)帶寬需求或如何通過網(wǎng)絡(luò)傳輸音頻數(shù)據(jù)之類的問題,讀者需參閱專業(yè)著作,本文只講述如何獲得適宜某類需求的原始音頻數(shù)據(jù)。
本文中的示例代碼均是一個名為cwaverecord的錄音類的成員函數(shù),該類是一個網(wǎng)絡(luò)電話程序的核心類。為減小篇幅或簡單明了,作了簡化處理等。
2 基本過程
mci按打開設(shè)備、配置設(shè)備、實現(xiàn)功能(或曰發(fā)送命令)、撤銷配置、關(guān)閉設(shè)備的標(biāo)準(zhǔn)次序組織apis。對于錄音編程而言,其要點在于根據(jù)音頻格式打開對應(yīng)的設(shè)備、配置錄音所需的參數(shù)(主要是設(shè)置數(shù)據(jù)區(qū)以及根據(jù)數(shù)據(jù)接收方式設(shè)置回調(diào)函數(shù)或消息)、按一定次序發(fā)送命令給設(shè)備、接收數(shù)據(jù)并配置參數(shù)以繼續(xù)錄音、停止錄音釋放資源、關(guān)閉設(shè)備等幾個步驟上。所需的函數(shù)說明于mmsystem.h,引入庫是winmm.lib。
2.1 打開設(shè)備
調(diào)用waveinopen()打開錄音設(shè)備,打開成功后函數(shù)返回mmsyserr_noerror,而第一個參數(shù)返回設(shè)備句柄。
調(diào)用該函數(shù)時,必須指定設(shè)備表示符(可能有多個設(shè)備)、音頻格式以及返回音頻數(shù)據(jù)的方式。
你可以將設(shè)備表示符,即第二個參數(shù)設(shè)為wave_mapper。這意味著,你不在意具體使用哪個設(shè)備,由系統(tǒng)決定。
第三個參數(shù)是個waveformatex結(jié)構(gòu)指針,對應(yīng)的結(jié)構(gòu)指定音頻格式。一個音頻格式結(jié)構(gòu)的例子:
pcmwaveformat wf_pcm8s11k =
{
{
wave_format_pcm, // pcm格式
1, // 單聲道
11025l, // 11.025khz
11025l, // 11025字節(jié)/s
1 // 字節(jié)對齊
},
8 // 8位采樣
};
后面的三個參數(shù)用于指定獲取音頻數(shù)據(jù)的方式:事件方式、線程方式、窗口方式以及回調(diào)函數(shù)方式。最后一個參數(shù),即第六個參數(shù)指定方式,而第四個參數(shù)和第五個參數(shù)指定該方式所需的信息。
調(diào)用示例:
waveinopen( &m_hwavein, dwdeviceid, m_pwf,
(dword)waveinproc, (dword)this, callback_function );
該示例使用回調(diào)函數(shù)方式獲取音頻數(shù)據(jù)。其中dwdeviceid通常即為wave_mapper,m_pwf指向音頻格式結(jié)構(gòu),缺省值即wf_pcm8s11k的地址,waveinproc是回調(diào)函數(shù)(內(nèi)部細(xì)節(jié)后面論述)。值得注意的是,this指針被設(shè)為實例數(shù)據(jù),這是一個編程小技巧,方便waveinproc調(diào)用處理函數(shù)。
2.2 配置設(shè)備
按指定音頻格式成功打開錄音設(shè)備之后,需要配置錄音設(shè)備,也即為音頻數(shù)據(jù)分配數(shù)據(jù)區(qū)。需要考慮的第一個問題是數(shù)據(jù)塊的個數(shù)和大小。
系統(tǒng)一般在填滿一個數(shù)據(jù)塊后將其返回,這意味著大的數(shù)據(jù)塊導(dǎo)致獲取數(shù)據(jù)的頻率降低。如果試圖通過網(wǎng)絡(luò)傳輸這些數(shù)據(jù),這意味著傳輸次數(shù)降低,但每次傳輸?shù)臄?shù)據(jù)量較大,這將導(dǎo)致音頻的實時性降低。另外,在網(wǎng)絡(luò)傳輸過程中,數(shù)據(jù)包(指每次發(fā)送的udp或tcp數(shù)據(jù)包)的開始一般需設(shè)置一個信息頭,紀(jì)錄本數(shù)據(jù)包所含音頻數(shù)據(jù)的長度、錄制起始時間、錄制時間、發(fā)送時間、識別用的標(biāo)識符以及其他一些信息,方便接受者處理。如果設(shè)置的音頻數(shù)據(jù)塊太小,比如100字節(jié),將導(dǎo)致低的網(wǎng)絡(luò)傳輸效率。這意味著網(wǎng)絡(luò)將為小量數(shù)據(jù)啟動傳輸過程,而這小量數(shù)據(jù)中又有相當(dāng)一部分屬于附加信息,這是不合算的。
數(shù)據(jù)塊的個數(shù)相對來說要好處理的多。較多的數(shù)據(jù)塊只有一個問題,那就是浪費,但這對windows這樣的操作系統(tǒng)而言,浪費幾百k的內(nèi)存根本不是問題。數(shù)據(jù)塊個數(shù)不能太少,這是因為在你處理上次返回的數(shù)據(jù)塊時,錄音設(shè)備正在工作,需要內(nèi)存。如果在某一段時間內(nèi),比如說網(wǎng)絡(luò)傳輸變壞,你處理數(shù)據(jù)的速度也將變慢,而錄音設(shè)備卻如常工作。這導(dǎo)致你無法迅速地返回當(dāng)前內(nèi)存供下一次錄音使用,因此需要一些備用的內(nèi)存塊以備不時之需。
調(diào)用waveinaddbuffer為錄音設(shè)備配置數(shù)據(jù)塊,而在此之前需調(diào)用waveinprepareheader準(zhǔn)備數(shù)據(jù)塊。
waveinprepareheader的參數(shù)有三個,就是錄音設(shè)備句柄、一個描述數(shù)據(jù)塊的結(jié)構(gòu)wavehdr和該結(jié)構(gòu)的大小。
wavehdr結(jié)構(gòu)是錄音編程的常用數(shù)據(jù)結(jié)構(gòu),不同的使用環(huán)境設(shè)置不同的參數(shù),具體內(nèi)容參見msdn。
下面的示例為錄音設(shè)備添加4個大小為4096字節(jié)的數(shù)據(jù)塊:
for( i=0; i<4; i++ )
{
pwh = (wavehdr*)malloc( 4096 + sizeof(wavehdr) )
zeromemory( pwh, sizeof(wavehdr) );
pwh->dwbufferlength = 4096;
pwh->lpdata = (lpstr)(pwh + 1);
waveinprepareheader( hwavein, pwh, sizeof(wavehdr) );
waveinaddbuffer( hwavein, pwh, sizeof(wavehdr) );
}
每次添加數(shù)據(jù)塊都需一個wavehdr(其他操作也如此),能否讓這些數(shù)據(jù)塊共用一個wavehdr或使用局部變量以減少內(nèi)存消耗呢?最好不要這么做,因為你是在與實際設(shè)備打交道。很可能當(dāng)調(diào)用返回時,系統(tǒng)內(nèi)部卻還在使用wavehdr結(jié)構(gòu)。如果共用或使用局部變量將導(dǎo)致信息混亂或非法內(nèi)存操作。這一點msdn沒有強調(diào),我花了一星期才得到如此教訓(xùn)。其他錄音操作也類似于此,切記!
配置好內(nèi)存以后就可啟動錄音了:
waveinstart( hwavein );
如果所有操作均正確,很快你將得到第一塊音頻數(shù)據(jù)。
2.3 處理音頻數(shù)據(jù)塊
獲取音頻數(shù)據(jù)塊的方式有多種(詳見msdn),處理方式大同小異,本文以回調(diào)函數(shù)方式為例說明處理過程。如上所述,waveinproc在waveinopen調(diào)用中被設(shè)置為回調(diào)函數(shù)。
waveinproc通過處理wim_open(打開設(shè)備)、wim_data(音頻數(shù)據(jù))、wim_close(設(shè)備關(guān)閉)消息的方式與系統(tǒng)交互,通常只需處理wim_data消息,其他兩條消息可以忽略。
當(dāng)系統(tǒng)返回一個音頻數(shù)據(jù)塊時,系統(tǒng)調(diào)用回調(diào)函數(shù)。此時,回調(diào)函數(shù)第一個參數(shù)被設(shè)置為錄音設(shè)備句柄;第二個參數(shù)是消息,即wim_data;第三個參數(shù)則是在waveinopen第五個參數(shù)中給出的實例數(shù)據(jù);其他參數(shù)是與消息有關(guān)的參數(shù),參見msdn。
處理wim_data的任務(wù)由成員函數(shù)onwaveindata完成,因此waveinproc及其簡單:
cwaverecord *pobject = (cwaverecord *)dwinstance;
if( umsg == wim_data )
pobject->onwaveindata( (wavehdr*)dwparam1 );
在onwaveindata中需要完成兩件事:一是保存系統(tǒng)返回的音頻數(shù)據(jù),二是給錄音設(shè)備添加數(shù)據(jù)塊以繼續(xù)錄音,否則會因數(shù)據(jù)塊耗盡而停止錄音。
在保存系統(tǒng)返回的音頻數(shù)據(jù)塊之前,需要調(diào)用waveinunprepareheader發(fā)送一個通知告訴設(shè)備驅(qū)動程序該數(shù)據(jù)塊已不能用于錄音。保存音頻數(shù)據(jù)塊是個單純的數(shù)據(jù)保存問題,不需多說,只是有三個要點需要特別關(guān)注:
一是msdn中注明在回調(diào)函數(shù)中嚴(yán)格限制系統(tǒng)調(diào)用,因此你不能隨心所欲地設(shè)計方案,比如說直接將音頻數(shù)據(jù)寫入文件是不允許的。通常,你需要設(shè)計一個數(shù)據(jù)塊鏈表結(jié)構(gòu)進(jìn)行緩沖,這多半涉及多線程編程。特別是如果你使用mfc中的csocket類,那么必須清楚csocket依賴一個名為csocketwnd的內(nèi)部類,而該類與tls(線程局部存貯)有關(guān),這意味著csocket類是不支持多線程共享的。然而,win32的socket句柄是全局性。因此,獲取csocketwnd類對象的窗口句柄之后,我通過直接調(diào)用win32 apis使用csocket::m_hsocket避開這個由mfc封裝引起的問題。
二是系統(tǒng)是在另一個線程(不是你自己創(chuàng)建的線程)中調(diào)用回調(diào)函數(shù),這會給使用tls機制的程序帶來一些微妙的影響。如果你使用mfc,afxgetapp之類的函數(shù)返回值不正確。
三是盡可能地快速。
至于給錄音設(shè)備添加數(shù)據(jù)塊以繼續(xù)錄音,其步驟與初始化過程一樣:
waveinprepareheader( hwavein, pwh, sizeof(wavehdr));
waveinaddbuffer( hwavein, pwh, sizeof(wavehdr) );
2.4 關(guān)閉設(shè)備釋放資源
簡單的調(diào)用waveinclose即可關(guān)閉設(shè)備。不過,在關(guān)閉時必須保證所有錄音數(shù)據(jù)塊已全部返回,這有點麻煩。我的解決方法是設(shè)置一個停止錄音標(biāo)志和一個錄音數(shù)據(jù)塊計數(shù)。當(dāng)用戶發(fā)出停止錄音的命令后,該標(biāo)志置為true,而onwaveindata(在回調(diào)函數(shù)中被調(diào)用)檢查該標(biāo)志,在標(biāo)志置位的情況下,onwaveindata不添加錄音數(shù)據(jù)塊。每添加一個錄音數(shù)據(jù)塊計數(shù)增加,每返回一個則減少(均在onwaveindata中實現(xiàn))。執(zhí)行停止錄音命令時,先將停止標(biāo)志值位,等待計數(shù)變?yōu)榱,然后才調(diào)用waveinclose。需要注意的是,計數(shù)涉及多線程數(shù)據(jù)共享,應(yīng)使用線程互斥機制,在單cpu的機子上使用interlockedincrement和interlockeddecrement是個簡單的選擇。
3 編程技巧
3.1 簡單的聲音檢測
如果你通過網(wǎng)絡(luò)發(fā)送音頻數(shù)據(jù),自然需要進(jìn)行音頻處理。專業(yè)級的處理難度相當(dāng)高,如果你是在一個局域網(wǎng)上進(jìn)行通話,那么無需進(jìn)行音頻壓縮也可以將就使用(性能自然不高)。但如果你不說話,程序還不停地發(fā)送音頻數(shù)據(jù)(靜音或噪音),那就太說不過去了。
你可以定義一個靜音區(qū)間,一旦音頻強度進(jìn)入該區(qū)間就意味著沒有聲音。統(tǒng)計音頻數(shù)據(jù)塊中不是靜音采樣點的個數(shù),當(dāng)總數(shù)超過預(yù)設(shè)的限制時,才發(fā)送該數(shù)據(jù)塊。
對于8位采樣,127是靜音點。簡單的代碼如下:
for( i=0; i<nmax; i++ )
{
uvalue = (uchar)data[i];
if( uvalue<=125 || uvalue>=129 )
uhascnt++;
if( uhascnt >= ulestnum )
return true;
}
return false;
3.2 創(chuàng)建音頻文件
創(chuàng)建任意格式的音頻文件在編程上不是一件輕松的事,但對于8位單聲道音頻數(shù)據(jù)而言較容易。定義文件信息頭結(jié)構(gòu):
// wav文件頭結(jié)構(gòu), 對齊方式為1字節(jié)
typedef struct
{
char riffid[4]; // "riff"
dword dwfiledatasize; // file size - 8
char waveid[4]; // "wave"
char fmtid[4]; // "fmt "
dword dwfmtsize; // 16
word wformattag; // wave_format_pcm
word wchannels; // 1
dword dwsamplespersec; // 11025
dword dwavgbytespersec; // 11025
word wblockalign; // 1
word wbitspersample; // 8
char dataid[4]; // "data"
dword dwdatasize;
} wavefilehdr, *pwavefilehdr;
打開文件時,預(yù)留該結(jié)構(gòu)大小的空間:
setfilepointer(hfile,sizeof(wavefilehdr), null, file_begin );
然后就如寫一般文件一樣將接收的音頻數(shù)據(jù)寫入文件:
writefile(hfile,pwh->lpdata,pwh->dwbytesrecorded,&n, null );
m_dwsizewrite += dwsize;
最后一行代碼是將寫入的數(shù)據(jù)量統(tǒng)計下來用于信息頭填寫。
在關(guān)閉音頻文件時填寫信息頭:
wavefilehdr wfd =
{
'r', 'i', 'f', 'f', 0,
'w', 'a', 'v', 'e',
'f', 'm', 't', ' ', 16,
wave_format_pcm, 1, 11025, 11025, 1, 8,
'd', 'a', 't', 'a', 0
};
wfd.dwfiledatasize = dwbytes + sizeof(wavefilehdr)- 8;
wfd.dwdatasize = dwbytes;
::setfilepointer( hfile, 0, null, file_begin );
::writefile( hfile, &wfd, sizeof(wfd), &dwsize, null );
參考文獻(xiàn)
【打印此頁】【返回首頁】 |