使用waveOut接口在Windows中播放聲音(非常實用,轉載)

 Windows waveOut教程
本教程將幫助您瞭解如何使用Windows waveOut接口播放數字音頻。根據經驗,這些接口函數掌握起來有些困難。在本教程中,我們將會建立一個Windows命令行程序來原始數字音頻。注意:本教程假設您熟悉C程序及Windows API的使用。理解數字音頻的相關知識也是有益的,但不是必須的。

教程內容:

·獲取文檔

·什麼是數字音頻

·打開聲音設備

·播放聲音

·播放流式音頻到設備

·緩存機制

·運行程序

·接下來該做什麼?

獲取文檔
首先,您需要有關waveOut接口的相關文檔。如果您有Microsoft Platform SDK或者VisualC++,那麼它們已經提供了相關的信息。如果您還沒有這些,您可以通過MSDN在線查看。(http://msdn.microsoft.com)

什麼是數字音頻
這一部分是爲那些對數字音頻如何存儲完全沒有概念的人準備的。如果您理解有關數字音頻的信息,同時瞭解“樣本(sample)”、“採樣頻率(samplerate)”、“樣本大小(samplesize)”及“聲道(channels)”的概念,您可以跳過此節。
只要把字節碼發送到聲卡就可以播放聲音了,但是這些字節都是什麼意思呢?音頻只是簡單的一系列運動的壓力波。在現實世界中,它們是一些相似的波形,但是在數字世界中,我們必須從這波形中採集一系列的樣本並存儲起來。“樣本”是表現某一時間點上波形振幅的一個值——它僅僅是一個數字。
“採樣頻率”表明我們採集波形樣本的頻率。它的計量單位是赫茲(Hz)或每秒樣本數。顯然,採樣頻率越高,採樣的波形就越接近真實的波形,所以聲音的音質也就越好。
另外一個有助於改善音質的參數是每個樣本的大小。當然,樣本越大音質也就越好。樣本大小用字節位數(bits)來計量。爲何樣本越大音質越好?假設一個8bits的樣本,它有256(2的八次方)個可能的值,這意味着您不能精確地再現波形的振幅。而如果是一個16bits的樣本,它的可能值有65536(2的16次方)個,這樣它就擁有256倍於8bits的樣本的更精確表現波形的能力。
最後是關於聲道。在多數機器上有兩個喇叭(左、右),那是兩個聲道。您需要同時把樣本數據存入左聲道和右聲道。
幸運的是,操作兩個聲道是很容易的(您將在本教程中看到)。樣本總是交錯存儲的,它們將按左、右、左、右……的順序存儲。
CD品質的音頻採樣頻率是44100Hz,樣本大小是16bits,意味着1M的音頻數據只能持續約6秒的時間。

打開聲音設備
打開聲音設備需要使用waveOutOpen函數(可以在您的文檔中查到)。象其它許多Windows對象一樣,您可以簡單地使用一個句柄(Handle)調用該設備。如存儲Windows窗口句柄使用HWND類似,我們可以用HWAVEOUT句柄來調用聲音設備。
下面的代碼段說明了如何打開一個CD標準音質的波形設備,然後關閉它。

#include
#include
#include
int main(int argc, char* argv[])
{

HWAVEOUT hWaveOut;
WAVEFORMATEX wfx;
MMRESULT result;

wfx.nSamplesPerSec = 44100;
wfx.wBitsPerSample = 16;
wfx.nChannels = 2;

wfx.cbSize = 0;
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels;
wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;

if(waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0, 0, CALLBACK_NULL) !=MMSYSERR_NOERROR) {

fprintf(stderr, "unable to openWAVE_MAPPER device\n");
ExitProcess(1);

}

printf("The Wave Mapper device was opened successfully!\n");
waveOutClose(hWaveOut);
return 0;

}

注意:要編譯本程序,您需要添加winmm.lib到您的工程,否則將會鏈接失敗。
好了,我們已經做好了第一步,現在聲音設備已經準備好,我們可以寫音頻數據進去了。

播放聲音
打開和關閉聲音設備挺有意思的,但是上面的代碼並沒有真的做什麼事情。我們想要的是能從設備聽到聲音。在這之前,我們有兩件事要做。

·獲得一個正確格式的原始音頻

·解決如何將數據寫入設備


問題一很好解決,您可以使用Winamp的DiskWriter插件來轉換一個音樂文件爲原始音頻。比如您可以轉換\Windows\Media下的Windows聲音文件(比如Ding.wav)爲原始音頻文件。如果您不能轉換這些文件,那麼直接播放未經轉換的文件也是件很有意思的事。直接播放的話,聽起來會很快,因爲這些文件大部分是用22kHz的採樣頻率存儲的。
問題二就稍微複雜一些了。音頻是以塊(Block)的形式寫入設備的,每個塊都有它自己的頭(Header)。寫入一個塊(Block)是很容易的,但是大部分時候,我們需要建立一個隊列機制並寫入很多的塊(Blocks)。之所以用一個小的文件開始學習,是因爲下面的例子我們將載入整個文件到一個塊中並寫入設備。
首先,我們要寫個函數發送一塊數據到音頻設備中。函數命名爲writeAudioBlock。要寫入音頻數據,我們需要三個接口函數:waveOutPrepareHeader,waveOutWritewaveOutUnprepareHeader,並按這個順序調用它們。您可以在相關文檔中查找到並熟悉這些函數。
下面的代碼是函數writeAudioBlock的初期版本。


void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size)
{

WAVEHDR header;

ZeroMemory(&header, sizeof(WAVEHDR));
header.dwBufferLength = size;
header.lpData = block;

waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));

waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));

Sleep(500);
while(waveOutUnprepareHeader(hWaveOut,&header,sizeof(WAVEHDR)) ==WAVERR_STILLPLAYING)

Sleep(100);

}

現在我們有了一個寫入塊數據的函數。我們還需要一個函數來獲得音頻數據塊。這就是函數loadAudioBlock的任務了。函數loadAudioBlock讀取文件到內存並把指針返回。下面就是loadAudioBlock的代碼:

LPSTR loadAudioBlock(const char*filename, DWORD* blockSize)
{

HANDLE hFile= INVALID_HANDLE_VALUE;
DWORD size = 0;
DWORD readBytes = 0;
void* block = NULL;

if((hFile = CreateFile(

filename,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL
)) == INVALID_HANDLE_VALUE)

return NULL;


do {

if((size = GetFileSize(hFile, NULL)) ==0)

break;

if((block = HeapAlloc(GetProcessHeap(),0, size)) == NULL)

break;

ReadFile(hFile, block, size,&readBytes, NULL);

} while(0);
CloseHandle(hFile);
*blockSize = size;
return (LPSTR)block;

}

這部分的最後,是整個程序調用和main函數。

#include
#include
#include
LPSTR loadAudioBlock(const char* filename, DWORD* blockSize);
void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size);
int main(int argc, char* argv[])
{

HWAVEOUT hWaveOut;
WAVEFORMATEX wfx;
LPSTR block;
DWORD blockSize;

.
. (leave middle section as it was)
.
printf("The Wave Mapper device was opened successfully!\n");

if((block = loadAudioBlock("c:\\temp\\ding.raw", &blockSize)) ==NULL) {

fprintf(stderr, "Unable to loadfile\n");
ExitProcess(1);

}
writeAudioBlock(hWaveOut, block, blockSize);
waveOutClose(hWaveOut);
return 0;

}

將上面的代碼放到一個工程裏進行編譯就可以播放小的聲音文件了。我們實現了類似PlaySound函數的功能。請試着做一些小的試驗:改變播放的採樣頻率(在main函數中)或者改變樣本大小(注意:一定要是8的倍數)看看會發生什麼,甚至可以改變一下聲道的數量。我們會發現改變採樣頻率或者聲道數會加快或減慢播放的速度,而改變樣本大小可能會有毀滅性的影響!

播放流式音頻到設備
您可能會注意到上面的代碼有幾個重要的缺陷(注意這些都是故意的:)),明顯的幾個缺陷如下:

·                             限於載入數據的方式,我們不能播放太大的文件。現在的方法緩存整個文件並一次播放完畢。而音頻本質上是很大的,所以我們需要找一種方法把音頻數據轉換爲流式數據並一個塊(Block)接一個塊的寫入到設備中。

·                             現有的writeAudioBlock函數是同步執行的,所以一個位(bit)一個位地寫入多個塊會在兩個塊輸出之間有個間隔(即不能足夠快速地重新填充緩存(buffer))。微軟建議至少需要兩個buffer的體制,這樣我們可以在播放一個塊(block)的同時填充另一個塊,然後交換播放和填充的塊。實際上這樣也不完全能解決問題。即使交換數據塊播放也會引起一個非常小(但是很煩人)的間隔。


幸運的是數據塊的讀取很簡單,所以暫時不用管它。現在讓我們集中於如何建立一種緩存機制以避免出現音頻設備的聲音間隔吧。
這個塊切換的問題並不象它聽起來那麼嚴重。我們不能無間隔地切換兩個數據塊,但是接口有某種機制可以讓我們避開這個問題。接口管理着一個塊的隊列,我們用waveOutPrepareHeader傳送的每個數據塊都可以通過調用waveOutWrite插入到這個隊列中。這意味着我們可以寫2個(或者更多)的數據塊到設備中,當第一個數據塊播放時填充第三個數據塊,然後當第二個播放的時候再進行切換。這樣我們可以得到無間隔的音頻輸出了。
在說明這個方法之還有最後一個問題,我們如何知道一個數據塊播放完了?前面writeAudioBlock的第一個例子中直到塊完成再調用waveOutUnprepareHeader的方式是非常不好的。我們在實際應用中不能這麼做,因爲我們還要繼續填充新的數據塊到設備中以繼續播放,關於這些,在waveOut接口中提供了更好的方法來實現。
waveOut接口提供了4種回調機制來通知我們數據塊已經播放完成了。它們是:

·事件(Event)——數據塊播放完成的時候會觸發一個事件

·回調函數(CallbackFunction)——數據塊播放完成時會調用一個函數

·線程消息(Thread)——數據塊播放完成時會發送一個線程消息

·窗口消息(Window)——數據塊播放完成時會發送一個窗口消息


要指定使用哪種方式只需要在調用waveOutOpen函數時指定參數dwCallback的值就可以了。在我們下面的例子中將使用回調函數的方式。
所以我們需要一個新的函數:waveOutProc。這個函數如何定義可以在相關文檔中查到。您可以看到,這個函數將在以下三種情況下被調用:

·                             設備打開時(Opened)

·                             設備關閉時(Closed)

·                             數據塊播放完成時


我們感興趣的只是數據塊播放完成時這種情況。

緩存機制
我們將要實現的緩存機制如上面我們提到的一樣運行。它需要一個變量來隨時保存空閒緩存(buffer)的數量(你可能想到了使用信號量(Semaphore)來控制,但是我們不能使用它,後面將解釋原因)。這個變量初始化爲緩存的數量,當數據塊寫入的時候減小並在數據塊完成時增加。如果沒有緩存可用,我們將等待直到該變量計數器爲1以上然後再繼續寫入。這將可以讓我們有效地循環向任何數量的數據塊隊列中寫入數據。我們例子中沒有使用3個數據塊隊列,而是更多,比如20個,這樣每次可以處理大概8KB的數據。
有些事情你可能猜中了:waveOutProc是在不同的線程中被調用的。Windows建立了一個特殊的線程來管理音頻播放。在此回調函數中你可以做的事情有很多限制。讓我們看一下微軟的文檔上是怎麼說的吧:

"Applications should not call anysystem-defined functions from inside a callback function, except forEnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg,OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime,timeGetTime, timeKillEvent, and timeSetEvent.
Calling other wave functions will cause deadlock."
應用程序不能在該回調函數中調用除下列以外的系統函數:EnterCriticalSection,LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString,PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime,timeKillEvent, and timeSetEvent。
調用其它的wave函數可能會引起死鎖。

這解釋了爲什麼我們不能使用信號量(Semaphore)——這將需要調用ReleaseSemaphore系統函數,而這是我們不能去做的。在實際應用中可能會靈活一些——我見過在回調函數中使用信號量的代碼,不過那樣的程序可能在某些版本的Windows上可以執行而不能執行在其它版本的機器上。同樣的,在回調函數中調用waveOut函數也將導致死鎖。實際上我們也將在回調函數中調用waveOutUnprepareHeader,不過我們不能那麼做。(如果你不調用waveOutReset將不會發生死鎖)。
您可能注意到waveOutOpen提供了一個傳遞實例數據到回調函數的方法(一個用戶定義的指針),我們將使用這個方法傳遞我們的計數器變量指針。
另外需要注意的是,既然waveOutProc被在另外的線程中調用,所以會有兩個以上的線程操作此計數器變量。爲了避免線程衝突,我們需要使用Critical Section對象(我們將使用一個靜態變量並命名爲waveCriticalSection)。
下面是waveOutProc函數的代碼:

static void CALLBACK waveOutProc(

HWAVEOUT hWaveOut,
UINT uMsg,
DWORD dwInstance,
DWORD dwParam1,
DWORD dwParam2
)

{


int* freeBlockCounter = (int*)dwInstance;

if(uMsg != WOM_DONE)

return;

EnterCriticalSection(&waveCriticalSection);
(*freeBlockCounter)++;
LeaveCriticalSection(&waveCriticalSection);

}

然後我們需要兩個函數分配和釋放數據塊的內存以及一個命名爲writeAudio的新的writeAudioBlock的實現。下面的兩個函數allocateBlocks和freeBlocks實現了數據塊的分配和釋放。allocateBlocks分配了一組數據塊(Block),每個數據塊的頭(Header)是固定長度的。freeBlocks則釋放了數據塊的內存。如果allocateBlocks失敗將導致程序退出。這意味着我們不需要在main函數中檢查它的返回值。

WAVEHDR* allocateBlocks(int size, intcount)
{

unsigned char* buffer;
int i;
WAVEHDR* blocks;
DWORD totalBufferSize = (size + sizeof(WAVEHDR)) * count;

if((buffer = HeapAlloc(

GetProcessHeap(),
HEAP_ZERO_MEMORY,
totalBufferSize
)) == NULL)

{

fprintf(stderr, "Memory allocationerror\n");
ExitProcess(1);

}

blocks = (WAVEHDR*)buffer;
buffer += sizeof(WAVEHDR) * count;
for(i = 0; i < count; i++) {

blocks[i].dwBufferLength = size;
blocks[i].lpData = buffer;
buffer += size;

}
return blocks;

}
void freeBlocks(WAVEHDR* blockArray)
{


HeapFree(GetProcessHeap(), 0, blockArray);

}

新的writeAudio函數需要能夠把那些必須的數據塊(Block)寫入隊列中。基本的邏輯如下:

While there's data available

If the current free block is prepared

Unprepare it

End If
If there's space in the current free block

Write all the data to the block
Exit the function

Else

Write as much data as is possible tofill the block
Prepare the block
Write it
Decrement the free blocks counter
Subtract however many bytes were written from the data available
Wait for at least one block to become free
Update the current block pointer

End If

End While

這就產生了一個問題:我們如何知道什麼時候一個數據塊(Block)準備好了而什麼時候沒有準備好?
實際上這是一個相當簡單的事情。Windows使用結構體WAVEHDR的dwFlags成員變量來解決這個問題。waveOutPrepareHeader函數的功能中有一項就是設置dwFlags爲WHDR_PREPARED。所以我們需要做的就是檢查dwFlags中的這個標識位。
我們將使用結構體WAVEHDR中的dwUser成員變量來管理數據塊的計數器。下面是writeAudio函數的代碼:

void writeAudio(HWAVEOUT hWaveOut,LPSTR data, int size)
{

WAVEHDR* current;
int remain;
current = &waveBlocks[waveCurrentBlock];
while(size > 0) {


if(current->dwFlags & WHDR_PREPARED)

waveOutUnprepareHeader(hWaveOut,current, sizeof(WAVEHDR));

if(size < (int)(BLOCK_SIZE -current->dwUser)) {

memcpy(current->lpData +current->dwUser, data, size);
current->dwUser += size;
break;

}
remain = BLOCK_SIZE - current->dwUser;
memcpy(current->lpData + current->dwUser, data, remain);
size -= remain;
data += remain;
current->dwBufferLength = BLOCK_SIZE;
waveOutPrepareHeader(hWaveOut, current, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, current, sizeof(WAVEHDR));
EnterCriticalSection(&waveCriticalSection);
waveFreeBlockCount--;
LeaveCriticalSection(&waveCriticalSection);

while(!waveFreeBlockCount)

Sleep(10);


waveCurrentBlock++;
waveCurrentBlock %= BLOCK_COUNT;
current = &waveBlocks[waveCurrentBlock];
current->dwUser = 0;

}

}


現在我們有了寫音頻的新的函數,因爲不會再被用到所以你可以扔掉writeAudioBlock函數了。你也可以扔掉loadAudioBlock函數,因爲下一部分我們將在main函數中實現新的方法不再需要loadAudioBlock函數了。

運行程序
如果您按照本教程進行到這裏,現在應該擁有了一個C文件包含下面的函數:

·                             main

·                             waveOutProc

·                             allocateBlocks

·                             freeBlocks

·                             writeAudio


下面讓我們完成main函數的新版本以實現把硬盤上的文件流式播放到waveOut設備上吧。下面的代碼當然也包括了程序運行所需要的模塊變量的聲明以及我們已經寫出的函數的原型。

#include
#include
#include

#define BLOCK_SIZE 8192
#define BLOCK_COUNT 20

static void CALLBACK waveOutProc(HWAVEOUT, UINT, DWORD, DWORD, DWORD);
static WAVEHDR* allocateBlocks(int size, int count);
static void freeBlocks(WAVEHDR* blockArray);
static void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size);

static CRITICAL_SECTION waveCriticalSection;
static WAVEHDR* waveBlocks;
static volatile int waveFreeBlockCount;
static int waveCurrentBlock;
int main(int argc, char* argv[])
{

HWAVEOUT hWaveOut;
HANDLEhFile;
WAVEFORMATEX wfx;
char buffer[1024];
int i;

if(argc != 2) {

fprintf(stderr, "usage: %s\n", argv[0]);
ExitProcess(1);

}

waveBlocks = allocateBlocks(BLOCK_SIZE, BLOCK_COUNT);
waveFreeBlockCount = BLOCK_COUNT;
waveCurrentBlock= 0;
InitializeCriticalSection(&waveCriticalSection);

if((hFile = CreateFile(

argv[1],
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL
)) == INVALID_HANDLE_VALUE)

{

fprintf(stderr, "%s: unable toopen file '%s'\n", argv[0], argv[1]);
ExitProcess(1);

}

wfx.nSamplesPerSec = 44100;
wfx.wBitsPerSample = 16;
wfx.nChannels= 2;
wfx.cbSize = 0;
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nBlockAlign = (wfx.wBitsPerSample * wfx.nChannels) >> 3;
wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;

if(waveOutOpen(

&hWaveOut,
WAVE_MAPPER,
&wfx,
(DWORD_PTR)waveOutProc,
(DWORD_PTR)&waveFreeBlockCount,
CALLBACK_FUNCTION
) != MMSYSERR_NOERROR)

{

fprintf(stderr, "%s: unable toopen wave mapper device\n", argv[0]);
ExitProcess(1);

}

while(1) {

DWORD readBytes;
if(!ReadFile(hFile, buffer, sizeof(buffer), &readBytes, NULL))

break;

if(readBytes == 0)

break;

if(readBytes < sizeof(buffer)) {

printf("at end of buffer\n");
memset(buffer + readBytes, 0, sizeof(buffer) - readBytes);
printf("after memcpy\n");

}
writeAudio(hWaveOut, buffer, sizeof(buffer));

}

while(waveFreeBlockCount < BLOCK_COUNT)

Sleep(10);


for(i = 0; i < waveFreeBlockCount; i++)

if(waveBlocks[i].dwFlags &WHDR_PREPARED)

waveOutUnprepareHeader(hWaveOut,&waveBlocks[i], sizeof(WAVEHDR));

DeleteCriticalSection(&waveCriticalSection);
freeBlocks(waveBlocks);
waveOutClose(hWaveOut);
CloseHandle(hFile);
return 0;

}

接下來該做什麼?
接下來要做的事就取決於你自己了。我有幾個您可能會有興趣的建議:

·試着修改原始音頻程序讓它可以從標準輸入讀取。您可以直接從命令行控制管道輸出聲音文件。

·重寫讀取部分讓它直接讀取Wave文件(*.wav)而不是RAW文件(原始音頻文件)。您會發現這令人吃驚的簡單。Wave文件使用結構體WAVEFORMATEX來定義數據格式,您可以在打開聲音設備後使用它。關於文件格式的有關信息可以參考WOTSIT's Format網站(http://www.wotsit.org)。

·看看自己能不能創建新的或更好的緩存機制

·試着把這些代碼加入某個開源解碼器中,如Vorbis解碼器或MP3解碼器。這樣您就可以得到一個屬於您自己的媒體播放器了:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章