Windows錄音API學習筆記
結構體和函數信息
結構體
WAVEINCAPS
該結構描述了一個波形音頻輸入設備的能力。
typedef struct {
WORD wMid; 用於波形音頻輸入設備的設備驅動程序制造商標識符。
WORD wPid; 聲音輸入設備的產品識別碼。
MMVERSION vDriverVersion; 用於波形音頻輸入設備的設備驅動程序的版本號。高位字節是主版本號,低字節是次版本號。
CHAR szPname[MAXPNAMELEN]; 設備名稱
DWORD dwFormats; 所支持的標准格式。可以是以下組合:
WORD wChannels; 數值指定設備是否支持單(1)或立體聲(2)的輸入
WORD wReserved1; 填充
} WAVEINCAPS;
HWAVEIN 目前推測是打開聲音設備后獲取的句柄(現在看就是句柄)
WAVEHDR
此結構定義用於標識一個波形音頻緩沖器中的報頭。
typedef struct {
LPSTR lpData; 指向波形緩沖區。
DWORD dwBufferLength; 緩沖區長度
DWORD dwBytesRecorded; 被用於輸入時緩沖區的數據長度
DWORD dwUser; 用戶數據
DWORD dwFlags; 標志提供有關緩沖區的信息。詳見MSDN
DWORD dwLoops; 循環播放的次數,僅用於輸出緩沖器
struct wavehdr_tag * lpNext; 保留
DWORD reserved; 保留
} WAVEHDR;
WAVEFORMATEX
該結構定義的波形的音頻數據的格式。只有共同所有波形的音頻數據格式的格式信息被包括在這種結構。對於需要更多的信息格式,該結構被包括在另一種結構第一部件,伴隨着的附加信息。
typedef struct {
WORD wFormatTag; 波形音頻格式類型。格式標記注冊的微軟公司的很多壓縮算法。格式標簽的完整列表可以在MMREG.H頭文件中找到。
WORD nChannels; 在波形的音頻數據的通道數。單聲道的數據使用一個通道,立體聲數據使用兩個通道。
DWORD nSamplesPerSec; 采樣速率,以每秒(赫茲)的樣品,每個通道應被播放或錄制。如果wFormatTag是WAVE_FORMAT_PCM,然后nSamplesPerSec共同的價值觀是8.0千赫,11.025千赫,22.05 kHz和44.1 kHz的。對於非PCM格式,這件必須根據制造商的格式標記的規范來計算。
DWORD nAvgBytesPerSec; 所需的平均數據傳輸速率,以每秒字節的格式標記。如果wFormatTag是WAVE_FORMAT_PCM,nAvgBytesPerSec應等於nSamplesPerSec和nBlockAlign的乘積。對於非PCM格式,這件必須根據制造商的格式標記的規范來計算。
播放和錄制軟件,可以通過使用nAvgBytesPerSec成員估計緩沖區大小。
WORD nBlockAlign;
塊對齊,以字節為單位。塊對齊用於wFormatTag格式類型數據的最小基本單位。如果wFormatTag是WAVE_FORMAT_PCM,nBlockAlign應等於nChannels和wBitsPerSample的乘積/8(每字節比特)。對於非PCM格式,這件必須根據制造商的格式標記的規范來計算。
播放和錄制軟件必須處理的數據nBlockAlign字節的倍數的時間。書面和從設備讀取數據必須開始於一個塊的開始。例如,它是非法的開始播放在樣品的中間PCM數據(也就是,在一個非塊對齊的邊界)。
WORD wBitsPerSample;
采樣位數用於wFormatTag格式類型。如果wFormatTag是WAVE_FORMAT_PCM,然后wBitsPerSample應等於8或16。對於非PCM格式,這件必須根據制造商的格式標記的規范來設置。請注意,某些壓縮方案不能定義為wBitsPerSample一個值,因此該構件可以是零。
WORD cbSize;
大小以字節為單位的額外格式信息追加到WAVEFORMATEX結構的末端。此信息可用於通過非PCM格式來存儲額外用於wFormatTag屬性。如果沒有額外的信息所必需的wFormatTag,這個部件必須被設置為零。請注意,對於WAVE_FORMAT_PCM格式(只有WAVE_FORMAT_PCM格式),這個成員被忽略。
} WAVEFORMATEX;
函數信息
//這里獲取到聲音設備句柄使用的是waveInOpen函數,在那里進行的初始化。
返回裝置的數量。返回的零值表示沒有設備存在或發生了錯誤。
UINT waveInGetNumDevs(VOID);
該函數檢索一個給定的波形音頻輸入設備的能力。
MMRESULT waveInGetDevCaps(
UINTuDeviceID, 標識符波形音頻輸出的裝置。它可以是一個設備標識符或一個開放波形音頻輸入設備的一個句柄。
LPWAVEINCAPSpwic, 指向一個WAVEINCAPS結構填充有關器件的信息的功能
UINTcbwic WAVEINCAPS結構體的字節數
);
MMRESULT waveInOpen(
LPHWAVEINphwi,指向打開的聲音設備的標識句柄。在fdwOpen參數指定為WAVE_FORMAT_QUERY時這個參數可以為空
UINTuDeviceID, 要打開設備的標識符
LPWAVEFORMATEXpwfx, 指向一個WAVEFORMATEX結構,它標識用於記錄波形音頻數據所需的格式。您可以waveInOpen返回后立即釋放此結構。
DWORDdwCallback, 指針指向一個固定的回調函數,事件句柄,句柄到窗口或線程波形音頻錄制過程中被調用來處理與記錄的進度消息的標識符。如果沒有回調功能是必需的,則該值可以是零。
DWORDdwCallbackInstance,傳給回調機制的數據類型,此參數並不用於窗口回調
DWORDfdwOpenz指明dwCallBack傳入的數據類型,比如時間句柄或者函數指針
);
該函數為音頻輸入設備提供緩沖區
MMRESULT waveInPrepareHeader(
HWAVEINhwi, 聲音輸入設備句柄
LPWAVEHDRpwh, 指向WAVEHDR結構,標識要准備的緩沖區。
UINTcbwhWAVEHDR結構的大小
);
該函數發送一個輸入緩沖區給定的波形音頻輸入設備。當緩沖區被填滿后,通知應用程序。
MMRESULT waveInAddBuffer(
HWAVEINhwi, 聲音輸入設備句柄
LPWAVEHDRpwh, 指向WAVEHDR結構,標識要准備的緩沖區。
UINTcbwhWAVEHDR結構的大小
);
該函數啟動指定設備的輸入
MMRESULT waveInStart(
HWAVEINhwi 設備句柄
);
基於Windows API的錄音分析
昨天看了一天的Audio API和windows的Wav文件相關資料,漸漸的理清了一點思路,所以在此總結一下,待本文已完成之后,應該就能繼續下一步了。
1 首先要了解的是計算機是如何表示聲音文件的。
我們熟知的音頻格式有:MP3,WMA,FLAC,以及WAV。這里我暫時只關注WAV。要知道的是,WAV其實就是WAVE,意思為波形。真實世界中的聲音都是連續的,因為是模擬信號,但是在計算機中存儲的信息都是數字信號。所以在將聲音存儲到計算機之前,就必須要進行聲音的數字化,轉換成計算機能夠存儲的形式。
學過信號與系統的應該都知道,模擬信號轉換為數字信號,一種比較通用的方法就是進行等間隔采樣。根據奈奎斯特定理,采樣頻率至少為信號頻率的2倍,才能無失真的保存原有的音頻信號。因此采樣頻率的高低決定了數字信號的保真度,自然是越高越好。打個比方,一個周期為1ms的正弦信號,采兩個點和采100個點的信號在還原成模擬信號的時候,肯定是采100個點信號的還原效果更好。
在對模擬信號完成采樣后,得到的是一系列的離散電壓信號。因此在將數據存儲至計算機前,還需要對這些模擬信號進行量化。這里所謂量化,就是用二進制數據來表示電平的大小。一般采用8位(256級)或者16位(65536級)的數據來表示,在硬件級的設計中,需要根據ADC的具體情況來決定。而在Windows中,可以使用waveInGetDevCaps函數獲取聲卡信息,以判斷使用8位或者16位的量化采樣位數。在其參數的dwFormats 成員中,包含了相應的信息,具體如下:
假設0-5V電平的正弦信號,在采用8位的量化下,就將0-5V分成256個梯度,每個梯度的電壓差值為0.0195V。一個4V的采樣值對應的數值就是205(204.8)。
在經過上面兩步的處理之后,模擬聲音信號就轉化為了具有固定采樣頻率和相應量化標准的數字信號。所以數字聲音信號最主要的兩個參數就是采樣位寬和采樣頻率。
另外一個需要注意的就是聲道。我們平時聽到的音樂都是立體聲,也就是雙聲道的。這個在WAV文件格式中有專門的介紹,不過我沒細看……記得是交替出現的。
現在回過頭來看一下上面的表,聲卡在提供信號的時候,也就上面這三個主要因素:采樣頻率,聲道,量化位寬。
我根據WAV文件頭的資料寫了一份代碼,打開了並查看了一個我計算機上的WAV格式的音樂文件,以前在CD上拷的。
運行結果如下:
不過這個就是看一看,暫時還用不到,有了上面的基礎,接下來要做的事分析API的使用。
第一個要調用的函數:
UINT waveInGetNumDevs(VOID);
這個函數的僅僅是用來查看計算機上是否有聲卡設備,如果返回值是0,那么表明沒有聲卡。不過一般這種情況不會發生,畢竟現在的計算機主板都是集成聲卡的。
第二個要調用的函數:
MMRESULT waveInGetDevCaps(
UINTuDeviceID,
LPWAVEINCAPSpwic,
UINTcbwic);
這個函數的第一個參數是設備ID,不過我們現在並不知道設備ID,但是沒關系,只要我們知道有設備存在就可以了。不過在MSDN上有這么一句讓我比較費解的話:
“Use this function to determine the number of waveform-audio input devices present in the system.”
中文意思就是:使用這個函數來決定系統中聲音輸入設備的數量。我看這個函數名感覺更像是獲取設備能力信息,而且第二個參數更是直接點出了其目的,“聲音輸入設備的能力”。
回來接着說第一個參數,MSDN在Remarks說明了這個參數可以是從0值到設備數量中的任何一個數,或者使用WAVE_MAPPER。我估計這個WAVE_MAPPER就是0值,因為傳入的參數要求是個指針,所以,你懂的。因此只要填個0就可以了。不過至於多聲卡設備具體怎么玩,我還真的不太清楚。
第二個參數是一個指向WAVEINCAPS結構的指針,在這把這個結構好好剖析一下:
typedef struct {
WORD wMid;
WORD wPid;
MMVERSION vDriverVersion;
CHAR szPname[MAXPNAMELEN];
DWORD dwFormats;
WORD wChannels;
WORD wReserved1;
} WAVEINCAPS;
前兩個結構體成員分別是設備制造商信息和產品標示符,我點擊去看了下,發現一堆的設備制造商信息的宏定義……不用太關心。
第三個成員是MMVERSION,其本質就是一個UINT,驅動設備版本號,也不用太關心。
第四個成員是設備名稱,就是一個以’\0’結尾的字符串,同樣不用管太多。
第五個成員很重要,因為指明了支持音頻信號標准的組合,具體看這個結構體的MSDN翻譯。
第六個是聲道支持的判斷。其實這個參數在第五個參數的分析中就能看出來的。
第七個是保留參數,不過至今還沒用上。
第三個參數僅僅是告知第二個參數對應結構體的大小。
根據程序的調試結果看,這個函數的用途更多是初始化WAVEINCAPS。根據下面的圖可以看到,在該函數調用完畢后,結構體被初始化。
第三個調用的函數:
MMRESULT waveInOpen(
LPHWAVEINphwi,
UINTuDeviceID,
LPWAVEFORMATEXpwfx,
DWORDdwCallback,
DWORDdwCallbackInstance,
DWORDfdwOpen
);
這個函數很有意思,同時也相對比較復雜。這里的第一個參數是設備句柄,不過在這里的這個參數是在這里被初始化的,傳進來就可以了。第二個參數還是設備ID,不過這個設備ID好像也是只傳個0就行了。第三個參數需要好好研究一下,也就是這個結構,WAVEFORMATEX。我現在覺得是用來初始化設備的。
先查看一下MSDN中對其的說明:
typedef struct {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
} WAVEFORMATEX;
第一個成員是格式類型參數,在第二個函數中,也就是waveInGetDevCaps函數的第二個參數的子成員Format,其組成基本是一致的。如果在這個參數中使用上面的宏定義指定,那么后面的如nChannel參數函數不用處理了。不過一種比較通用的方式還是將wFormatTag修改為WAVE_FORMAT_PCM,並依此初始化后面的參數。
這第三個參數可以根據需求進行相應的初始化,比如這形式的:
WaveInitFormat(
WORD nCh,
DWORD nSampleRate,
WORD BitsPerSample)
{
m_WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
m_WaveFormat.nChannels = nCh;
m_WaveFormat.nSamplesPerSec = nSampleRate;
m_WaveFormat.nAvgBytesPerSec = nSampleRate * nCh * BitsPerSample/8;
m_WaveFormat.nBlockAlign = m_WaveFormat.nChannels * BitsPerSample/8;
m_WaveFormat.wBitsPerSample = BitsPerSample;
m_WaveFormat.cbSize = 0;
}
在這個初始化函數中,第一個是WAVE_FORMAT_PCM,第二個就是聲道數的選項了,第三個是采樣頻率,第四個參數是根據采樣頻率,聲道,每個采樣點字節數的乘積計算的。第五個參數則是塊對齊大小,也就是每個獨立的聲音節點信息的大小,其計算公式就是聲道數*每個采樣點的字節數。第六個參數就是采樣點的位數,指定了聲音的量化水平。第七個參數有點說法,不過在wFormatTag被指定為WAVE_FORMAT_PCM的時候,這個參數會被忽略。
第四參數是回調處理參數,比如,一個回調函數,或者事件句柄,或者線程句柄。用於通知應用程序處理音頻數據的一個參數。等操作系統的音頻信號接收完畢,就是通過這個參數來決定以什么方式來把數據傳遞給應用程序。
第五個參數是傳給回調機制的數據類型,並不用語窗口回調。這個參數我不太懂,但是使用的時候,傳入NULL就可以了。
第六個參數是打開設備的一個通知信息,主要用途就是告訴函數上面第四個參數是一個回調函數還是一個事件句柄。就這么簡單。
第四個調用的函數是waveInPrepareHeader。這個函數的功能就是在初始化好后,為聲音輸入設備准備一塊存放聲音數據的緩沖區。
MMRESULT waveInPrepareHeader(
HWAVEINhwi,
LPWAVEHDRpwh,
UINTcbwh
);
第一個參數就是設備句柄,不多說。
第二個參數需要好好的關注一下,這一個結構體。
typedef struct {
LPSTR lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD dwUser;
DWORD dwFlags;
DWORD dwLoops;
struct wavehdr_tag * lpNext;
DWORD reserved;
} WAVEHDR;
LpData就是一個緩沖區,指向你所提供的內存空間。至於具體大小,好像沒有什么限制,因為填滿了會通知你,到時候用專門的函數去處理就OK了。
DwBufferLength參數就是緩沖區長度,不多說。
DwByteRecorded實際記錄的字節數,因為未必每次都會用掉全部的緩沖區。
DwUser用戶數據,具體干啥,MSDN沒說,有可能是到時候傳給回調函數的信息,我猜的。
DwFlags對於buffer的附加信息,根據MSDN的要求,在使用這個函數waveInPrepareHeader的時候,這個參數必須為0。
DwLoops循環播放的次數,只用於輸出使用。這里用不到。
LpNext 這個雖然是指針,但是實際確實一個保留參數。
reserved最后一個和上面一樣。
這樣這個結構體算是完事了。就是提供一個頭部輸入的緩沖區。
第三個參數就是上面那個結構的字節數。一個sizeof萬事大吉。
這個函數的用途就是為聲音輸入設備准備一個緩沖區,到時候音頻信息就會被寫進這個內存區域內。
第五個函數就是waveInAddBuffer,這個函數的用途就是把函數waveInPrepareHeader准備好的那個緩沖區發送給聲音輸入設備。
MMRESULT waveInAddBuffer(
HWAVEINhwi,
LPWAVEHDRpwh,
UINTcbwh
);
和上一個函數的原型是一致的,三個參數和waveInPrepareHeader函數使用過的參數必須一致。而且在本函數調用前,pwh參數的Buffer必須經過waveInPrepareHeader函數的處理。當緩沖區被填滿,那么會通知應用程序。詳見MSDN。
第六個函數是waveInStart,這個函數僅用於開啟錄音功能,最簡單。
Windows的錄音流程大致如下:
1 先查看本地機器是否擁有聲音輸入設備。
2 獲取聲音輸入設備的信息
一般上面的兩部不是很必要,畢竟現在的電腦基本都擁有集成聲卡。不過從穩定性和通用性看,還是很必要的。
3 打開設備,獲取設備句柄,傳入對應的事件句柄。
4 准備一個異步線程專門用於錄音完成后的處理工作,並等待事件。
5 通過設備句柄為其准備緩沖區
6 將准備好的緩沖區通過句柄添加到設備中
接下來屬於系統的工作,正常情況下,在緩沖區被填滿后,將會觸發事件,來通知異步線程進行處理。獲取聲音信息后,要再次添加緩沖區,才能繼續錄音。
20140118 補充:
有時在緩沖區的建立上,一般會采取棧分配或者堆分配的方式。棧分配內存的析構處理通過退棧完成,用戶不用手動處理。但是堆分配的時候就會遇到麻煩,具體如下:
在准備緩沖區的時候會調用waveInPrepareHeader函數,這個函數調用后,為其分配的內存就無法通過delete或者free來釋放了,因為在該函數調用后這塊內存區域被鎖定了。此時必須調用waveInUnprepareHeader函數才能解鎖定,然后才能釋放。
但是在調用waveInPrepareHeader函數后,再接着調用了waveInAddBuffer函數,在該緩沖區未被填滿的時候,嘗試使用waveInUnprepareHeader函數解鎖定,就會返回失敗碼33。這里的解決方案就是在決定釋放空間前,首先調用一個函數:waveInReset。這個函數調用后,就可以將內存從waveInAddBuffer函數的限定中釋放,然后再常規使用waveInUnprepareHeader釋放,最后調用delete或者free釋放內存空間。