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