音頻處理(二) 音頻輸出

Windows下的音頻輸出經常使用的3種方法:c++

1. PlaySound:使用最簡單直接,可是不夠靈活,功能也很是單一,沒法混音;算法

2. WaveOut:早期的Windows系統中普遍應用的音頻輸出程序接口,功能比PlaySound較完善(WaveIn用於音頻輸入);緩存

3. DirectSound:如今Windows中主流的應用於音頻輸入輸出的API,支持混音、獨立音量控制、硬件加速、硬件仿真等強大的功能;多線程

 

PlaySound併發

   PlaySound的使用很是簡單,下面是一個示例 (vs2013 vc++ Project):app

#include "stdafx.h"
#include <Windows.h>
#include <mmsystem.h>

#pragma comment(lib, "winmm.lib")

const char fPath1[] = "C:/Windows/Media/Ring09.wav\0\0";

// 同步播放
void PlaySync()
{
    printf("Sync Start...\n");
    PlaySoundA((char*)fPath1, NULL, SND_FILENAME|SND_SYNC);
    printf("Sync Complete!\n");
}

// 異步播放
void PlayAsync()
{
    printf("Async Start...\n");
    PlaySoundA((char*)fPath1, NULL, SND_FILENAME|SND_ASYNC);
    printf("Async Complete!\n");
}

int main(int argc, char* argv[])
{
    PlaySync();
    //PlayAsync();

    Sleep(3000);
    return 0;
}

    使用PlaySound方法以前須添加mmsystem.h和Windows.h兩個頭文件,並將winmm.lib連接庫添加到工程,這裏是用#pragma添加的,上面使用了同步和異步兩種方式播放一段wav音頻;異步

    它們的區別是,同步方式下,PlaySound方法調用時會阻塞程序,直到這段音頻播放結束,再返回往下繼續執行;而異步方式下,調用PlaySound方法會當即返回,音頻播放的同時,程序依然正常往下執行,這種狀況下,上例若是沒有Sleep方法休眠主線程,那麼程序會直接結束,致使聽不到完整的音樂。ide

    另外,PlaySound沒法同時播放兩個音頻,當重複調用PlaySound方法時,已經在播放的音頻會中斷,轉而播放新的那段音頻;又或者第二次調用PlaySound會失敗返回FALSE,而已經播放的音頻不受影響,這一切要看PlaySound的第三個參數的設定;好比上例中,若是重複調用,已經在播放的音頻會中斷,轉而播放新的音頻;而若是加上SND_NOSTOP,那麼重複調用播放同一段音頻時,以前的音頻不會中斷,而再次調用的PlaySound會失敗返回FALSE;oop

    SND_FILENAME表示採用文件名的方式加載音頻資源,除此以外還有引用內存中已有音頻資源的方式 (詳見官方文檔);ui

WaveOut

     使用WaveOut進行音頻輸出大體分爲幾個步驟:建立緩衝區 — 讀取音頻文件 — 複製數據到緩衝區 — 播放,用一張圖來講明音頻播放的大體流程:

                               

    首先是建立緩存區,這裏要用到官方定義的一個結構 WAVEHDR,這個結構是專用來進行音頻數據緩存塊和設備之間的橋樑,這裏經過它能夠提交音頻數據給設備;

它的結構定義以下:

/* wave data block header */
typedef struct wavehdr_tag {
    LPSTR       lpData;                 /* 指向數據區起始地址 */
    DWORD       dwBufferLength;         /* 數據緩存大小(Byte) */
    DWORD       dwBytesRecorded;        /* used for input only */
    DWORD_PTR   dwUser;                 /* 開發者自由使用 */
    DWORD       dwFlags;                /* 標誌位 */
    DWORD       dwLoops;                /* 循環計數器 */
    struct wavehdr_tag FAR *lpNext;     /* reserved for driver */
    DWORD_PTR   reserved;               /* reserved for driver */
} WAVEHDR, *PWAVEHDR, NEAR *NPWAVEHDR, FAR *LPWAVEHDR;

    通常咱們會建立3個以上的緩存塊,循環地把數據寫入、並按順序提交給設備,那麼設備就會按提交的順序不斷地播放已提交的那些緩存塊音頻數據,如上圖;CPU控制硬盤讀入數據,並複製到緩存區,而後按順序提交一個個緩存塊,播放完了的緩存塊,能夠繼續讀入再提交,循環往復;

之因此選擇3個以上緩存塊,是爲了不播放間隙時間的產生,微小的間隙在音頻播放中,觀感是難以忍受的;

建立代碼:

#define BlockSize   1024*10    // 數據塊緩存大小(這個案例中必須是 BufferSize 的整數倍)
#define BlockCount  12         // 數據塊個數(不限,建議3個以上)

WAVEHDR* Blocks = NULL;

//
// 建立緩存區
// WAVEHDR* CreateBlocks() { unsigned char* buffer; DWORD totalBufferSize = (BlockSize + sizeof(WAVEHDR))*BlockCount; // 申請的內存空間 = 塊結構內存 + 緩存空間 if ((buffer = (UCHAR*)malloc(totalBufferSize)) == NULL) { printf("Memory Malloc Error!\n"); return NULL; } memset(buffer, 0, totalBufferSize); Blocks = (WAVEHDR*)buffer; buffer += sizeof(WAVEHDR) * BlockCount; for (int i = 0; i < BlockCount; i++) { Blocks[i].dwBufferLength = BlockSize; Blocks[i].lpData = (char*)buffer; buffer += BlockSize; } return Blocks; }

緩存塊建立完成,而後能夠讀入wav音頻數據了,首先讀入wav頭結構,分析音頻信息,頭結構的分析直接引用上一篇中ReadHeader()方法;

//
// 讀取Wav文件頭,並驗證文件格式
// 成功返回文件句柄,並重定位文件指針到數據區
// 失敗返回NULL
//
HANDLE ReadHeader(char* path)
{
    HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("Unable to Open File!");
        return NULL;
    }
    char buffer[512];
    DWORD readByte;
    if (ReadFile(hFile, buffer, sizeof(buffer), &readByte, NULL))
    {
        Riff_Header header;
        Fmt_Block fmt;
        Data_Block data;
        memcpy(&header, buffer, sizeof(Riff_Header));
        if (strncmp(header.szRiffId, "RIFF", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&fmt, buffer + sizeof(Riff_Header), sizeof(Fmt_Block));
        if (strncmp(fmt.szFmtId, "fmt ", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&data, buffer + sizeof(Riff_Header)+fmt.dwFmtSize+8, sizeof(Data_Block));
        if (strncmp(data.szDataId, "data", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&wfx, &fmt.wFormatTag, sizeof(WAVEFORMATEX) - 2);
        wfx.cbSize = 0;

        // 重定位文件指針,到數據起始位置
        int headSize = sizeof(Riff_Header) + fmt.dwFmtSize + 8 + sizeof(Data_Block);
        headSize = (headSize / 8 + (headSize % 8 ? 1 : 0)) * 8;
        SetFilePointer(hFile, headSize, 0, FILE_BEGIN);

        return hFile;
    }
    return NULL;
}
ReadHeader

 頭文件分析完畢,能夠獲得波形文件信息wfx,如今能夠打開設備,並讀入音頻數據到緩存塊,最後提交,這樣就能夠播放音頻了;

#define BufferSize    1024     // 讀取文件的緩存大小

const char testWave[] = "C:/Windows/Media/Ring02.wav";

WAVEFORMATEX wfx;
CRITICAL_SECTION wcSection;
static volatile int freeCount; // 可用的緩存塊數量,初始爲BlockCount static int curIndex; // 當前要讀入數據的緩存塊的Index

void CALLBACK WaveOutProc(HWAVEOUT, UINT, DWORD, DWORD, DWORD); int PlayWave(HANDLE hFile) { unsigned char buffer[BufferSize]; if (Blocks == NULL || hFile == NULL) return 1; freeCount = BlockCount; curIndex = 0; InitializeCriticalSection(&wcSection); HWAVEOUT hwo;
// 打開設備
if (waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)WaveOutProc, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { printf("Unable to Open Mapper Device!"); return 2; } WAVEHDR* current = &Blocks[curIndex]; printf("Play Wave Start...\n"); while (1) { DWORD readByte; if (!ReadFile(hFile, buffer, BufferSize, &readByte, NULL)) break; if (readByte == 0) break; if (readByte < BufferSize) memset(buffer + readByte, 0, BufferSize - readByte); memcpy(current->lpData + current->dwUser, buffer, BufferSize); current->dwUser += BufferSize; if (current->dwUser < BlockSize) continue; // 必須先填滿當前緩存塊 waveOutPrepareHeader(hwo, current, sizeof(WAVEHDR)); // 準備數據塊 waveOutWrite(hwo, current, sizeof(WAVEHDR)); // 把數據塊提交給設備 (播放),當即返回 EnterCriticalSection(&wcSection); freeCount--; LeaveCriticalSection(&wcSection); while (freeCount == 0) Sleep(10); // 當全部的數據都準備好,且沒有釋放時,等待 curIndex = (curIndex + 1) % BlockCount; current = &Blocks[curIndex]; current->dwUser = 0; } while (freeCount < BlockCount) Sleep(100); // 等待全部數據塊播放完 printf("Finish Play Wave!\n"); DeleteCriticalSection(&wcSection); waveOutClose(hwo); return 0; }
//
// 設備回調方法,三種狀況下調用:
// 當設備開啓、關閉、播放一個緩存塊完成時
//
void CALLBACK WaveOutProc(HWAVEOUT hwo, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
    if (msg != WOM_DONE) return;  // 過濾設備開啓、關閉消息

    WAVEHDR* pwh = (WAVEHDR*)dwParam1;
    waveOutUnprepareHeader(hwo, pwh, sizeof(WAVEHDR));  // 釋放播放完的塊

    EnterCriticalSection(&wcSection);
    freeCount++;
    LeaveCriticalSection(&wcSection);
}

要點說明:

1. 上面的打開設備方法中,參數WaveOutProc是一個回調方法,就是設備的反饋消息,包括設備的打開、關閉、緩存塊播放完成消息;

2. wcSection是一個用於數據同步的信息,經過wcSection中的訪問計數,能夠在多個線程同時訪問EnterCriticalSection和LeaveCriticalSection之間的數據時,避免數據併發衝突;

    如上例中,經過wcSection計數能夠記錄freeCount的訪問個數,避免多線程(WaveOutProc方法)同時修改freeCount致使的錯誤;

3. 有一點須要知道,數據的讀取並複製到緩存區並提交,這一過程的速度遠遠大於音頻播放的速度,全部緩存塊在程序一運行就迅速填充滿了,以後就是等待釋放一個,填充一個,依次進行;

4. 這裏經過freeCount的值來判斷有無數據塊可用,curIndex始終表示最晚提交的緩存塊編號,那麼(curIndex+1)%BlockCount就是最先提交的那個編號,由於全部緩存塊是循環使用的;

只要斷定 freeCount > 0成立,那麼必定是(curIndex+1)%BlockCount播放完成了,能夠繼續複製數據到其中,再次提交;

 

那麼,上面就是數據塊的建立、讀入數據、提交數據的過程,下面是調用它們:

#include "stdafx.h"
#include <Windows.h>
#include <mmsystem.h>
#include "WavStruct.h"

#pragma comment(lib, "winmm.lib")

int
main(int argc, char* argv[]) { HANDLE hFile = ReadHeader((char*)testWave); if (hFile == NULL) return 0; CreateBlocks(); PlayWave(hFile); free(Blocks); CloseHandle(hFile); return 0; }

    運行,播放,無問題; 

 

DirectSound

    WaveOut 原生API沒法直接同時播放多路wav音頻,即沒法進行混音,若要使用WaveOut實現混音功能,就只能在準備數據緩存以前,進行混音算法,手動將多路音頻數據糅合到一塊兒;這樣也能夠達成混音的效果,固然混音質量就取決於所採用的混音算法了;

    而DirectSound也能夠實現混音,並且混音功能封裝在其內部,徹底沒必要由咱們手動進行;

    單從使用DirectSound來看,過程和WaveOut相似,也須要咱們解碼獲得Wave數據,而後把Wave數據複製到緩衝區;區別是,WaveOut只有一個緩衝區(雖然其中分爲若干塊),而DirectSound通常有不少個緩衝區,它們各自是獨立的,只要把它們同時播放,就會自動實現混音,互不干擾!

    DirectSound緩衝區分一主、多副緩衝區,通常咱們不操做主緩衝區,只是用若干個次緩衝區,次緩衝區多個音頻數據會自動混音,最終的音頻數據存放在主緩衝區進行播放,只是混音的過程不需咱們來操做,它是DS內部完成的 (也能夠手動);

    WaveOut的緩衝區由咱們指定大小並建立,而DS中,咱們只是指定大小,由DS內部開闢內存,咱們能夠得到地址;

步驟1. 首先初始化:

//
// 初始化DS,成功則返回 0
//
int DSInit()
{
    // 建立 DirectSound
    if (DirectSoundCreate8(0, &lpds8, 0) != DS_OK)
    {
        printf("Create DirectSound Failed!\n");
        return 1;
    }
    // 設置應用協做級別,要實現混音必須設置爲 Priority
    if (lpds8->SetCooperativeLevel(hwnd, DSSCL_PRIORITY) != DS_OK)
    {
        printf("Set CooperativeLevel Failed!\n");
        return 2;
    }
    return 0;
}

 

步驟2. 讀取音頻數據,爲了方便,這裏依然使用無壓縮的Wav格式,不須要解碼;

讀取Wav文件的方法,沿用以前的ReadHeader()方法,只是稍做修改;

//
// 讀取Wav文件頭,並驗證文件格式
// 成功返回文件句柄,並重定位文件指針到數據區
// 失敗返回NULL
//
HANDLE ReadHeader(char* path, WAVEFORMATEX* pwfx)
{
    HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("Unable to Open File!");
        return NULL;
    }
    char buffer[512];
    DWORD readByte;
    if (ReadFile(hFile, buffer, sizeof(buffer), &readByte, NULL))
    {
        Riff_Header header;
        Fmt_Block fmt;
        Data_Block data;
        memcpy(&header, buffer, sizeof(Riff_Header));
        if (strncmp(header.szRiffId, "RIFF", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&fmt, buffer + sizeof(Riff_Header), sizeof(Fmt_Block));
        if (strncmp(fmt.szFmtId, "fmt ", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&data, buffer + sizeof(Riff_Header) + fmt.dwFmtSize + 8, sizeof(Data_Block));
        if (strncmp(data.szDataId, "data", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(pwfx, &fmt.wFormatTag, sizeof(WAVEFORMATEX) - 2);
        pwfx->cbSize = 0;

        // 重定位文件指針,到數據起始位置
        int headSize = sizeof(Riff_Header) + fmt.dwFmtSize + 8 + sizeof(Data_Block);
        headSize = (headSize / 8 + (headSize % 8 ? 1 : 0)) * 8;
        SetFilePointer(hFile, headSize, 0, FILE_BEGIN);

        return hFile;
    }
    return NULL;
}
ReadHeader

CreateWaveBuffer()方法中,讀取Wav文件成功後,建立了一個緩衝區,將用於存放音頻數據,

//
// 打開Wav文件,讀取文件頭
// 並建立與之對應的 DS 緩存區
//
HANDLE CreateWaveBuffer(char* path, LPDIRECTSOUNDBUFFER* ppdsBuffer)
{
    WAVEFORMATEX wfx;
    HANDLE hFile = ReadHeader((char*)path, &wfx);
    if (hFile == NULL)
    {
        printf("File Read Failed!\n");
        return NULL;
    }
    DSBUFFERDESC dsDesc;
    memset(&dsDesc, 0, sizeof(DSBUFFERDESC));
    dsDesc.dwSize = sizeof(DSBUFFERDESC);
    dsDesc.lpwfxFormat = &wfx;
    dsDesc.dwBufferBytes = BlockSize * MaxAudio;
    dsDesc.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2;

    // 建立副緩存區
    if (lpds8->CreateSoundBuffer(&dsDesc, ppdsBuffer, 0) != DS_OK)
    {
        printf("Create lpdsBuffer01 Failed!\n");
        CloseHandle(hFile);
        return NULL;
    }
    return hFile;
}

 

步驟3. 播放音頻,這是一個建立線程CreateThread()方法的參數,它的調用在後面能夠看到;

//
// 線程:播放音頻
//
DWORD WINAPI PlayWave(void* lpv)
{
    LPDIRECTSOUNDBUFFER lpdsBuffer;
    HANDLE hFile = CreateWaveBuffer((char*)lpv, &lpdsBuffer);
if(hFile == NULL) return 1;
// 建立通知字段,當播放到該位置時,觸發事件通知 DSBPOSITIONNOTIFY dsPosNotify[MaxAudio]; HANDLE hevent[MaxAudio]; for (int i = 0; i < MaxAudio; i++) { hevent[i] = CreateEventA(NULL, FALSE, FALSE, NULL); dsPosNotify[i].dwOffset = (i+1) * BlockSize-1; dsPosNotify[i].hEventNotify = hevent[i]; } LPDIRECTSOUNDNOTIFY lpdsNotify = NULL; if (lpdsBuffer->QueryInterface(IID_IDirectSoundNotify, (void**)&lpdsNotify) != DS_OK) { printf("QueryInterface lpdsNotify Failed!"); return 2; } lpdsNotify->SetNotificationPositions(MaxAudio, dsPosNotify); lpdsNotify->Release(); void* lpBuffer1; void* lpBuffer2; DWORD dwLen1; DWORD dwLen2; // 首次讀入數據 HRESULT hr = lpdsBuffer->Lock(0, BlockSize*MaxAudio, (void**)&lpBuffer1, &dwLen1, &lpBuffer2, &dwLen2, DSBLOCK_ENTIREBUFFER); if (hr >= 0) { ReadFile(hFile, lpBuffer1, dwLen1, 0, NULL); lpdsBuffer->Unlock(lpBuffer1, dwLen1, NULL, 0); } //設置起始位置,播放 lpdsBuffer->SetCurrentPosition(0); lpdsBuffer->Play(0, 0, DSBPLAY_LOOPING); DWORD index; bool flag = true; while (1) { index = WaitForMultipleObjects(MaxAudio, hevent, FALSE, INFINITE); // 有標記的通知事件觸發時,返回一個index,表明hevent的編號 printf("index = %d\n", index); if (index >= 0) flag = FillBuffer(hFile, index, lpdsBuffer); if (!flag) break; } WaitForMultipleObjects(MaxAudio, hevent, TRUE, INFINITE); lpdsBuffer->Stop(); CloseHandle(hFile); return 0; } // // 補充數據到緩衝區的方法 // bool FillBuffer(HANDLE hFile, int index, LPDIRECTSOUNDBUFFER lpdsBuffer01) { bool flag = true; VOID* lockBuffer1 = NULL; VOID* lockBuffer2 = NULL; DWORD dwSize1; DWORD dwSize2; HRESULT hr; hr = lpdsBuffer01->Lock(index*BlockSize, BlockSize, &lockBuffer1, &dwSize1, &lockBuffer2, &dwSize2, 0); if (hr == DSERR_BUFFERLOST) { lpdsBuffer01->Restore(); hr = lpdsBuffer01->Lock(index*BlockSize, BlockSize, &lockBuffer1, &dwSize1, &lockBuffer2, &dwSize2, 0); } //printf("hr = %d, index = %d, address1 = 0x%X, Size = 0x%X, address2 = 0x%X\n\n", hr, index, lockBuffer1, dwSize1, lockBuffer2); DWORD dwReadByte = 0; if (hr >= 0) { if (lockBuffer1 != NULL) { ReadFile(hFile, lockBuffer1, BlockSize, &dwReadByte, NULL); if (dwReadByte < BlockSize) { memset((char*)lockBuffer1 + dwReadByte, 0, BlockSize - dwReadByte); flag = FALSE; } } if (lockBuffer2 != NULL) { ReadFile(hFile, lockBuffer2, BlockSize, &dwReadByte, NULL); if (dwReadByte < BlockSize) { memset((char*)lockBuffer2 + dwReadByte, 0, BlockSize - dwReadByte); flag = FALSE; } } hr = lpdsBuffer01->Unlock(lockBuffer1, dwSize1, lockBuffer2, dwSize2); return flag; } }

以上就是播放單個wav文件的方法主體,如今只要調用它們就好了,能夠傳入多個文件路徑同時調用它們,在此以前有些說明;

說明:

(1) 在PlayWave()方法中,須要設置播放事件觸發通知位置,就是說,在這個緩衝區,設置多個點,播放到這些點的時候,就會觸發一個通知,返回點的編號,從而咱們能夠知道播放的位置,而後從新讀取數據填充到已播放的緩衝區域,這樣緩衝區就能夠不斷循環播放;

        dsPosNotify[i].dwOffset = (i+1) * BlockSize-1;
        dsPosNotify[i].hEventNotify = hevent[i];

     在這個循環中,i = 0時,設置的緩衝偏移是 BlockSize - 1,而整個緩衝區的大小是BlockSize*MaxAudio,這是在CreateWaveBuffer中設定的;就是說,整個緩衝區平均分紅 MaxAudio 個小區,第一個小區的起始offset是0,終點offset是BlockSize-1,第二個、第三個依次類推;

    所以,這裏通知位置的設定爲各個小區的終點offset,不能比它大,大了就是越界了會得不到通知,就不知道播放到了哪裏;能夠比它小,但最好不要過小,以避免一個小區沒播完,就從新寫入;

(2) 設定完播放位置,就能夠開始讀取音頻數據到緩存區,而後播放,Play方法這裏必須設定爲DSBPLAY_LOOPING循環播放緩存區;

(3) WaitForMultipleObjects()方法是一個win32方法,根據以前播放事件通知位置綁定的HANDLE組,觸發一個預設點,就會返回一個Index,它就是HANDLE組的下標,即編號,由此,咱們能夠獲得當前播放位置,須要填充數據的位置就是BlockSize*Index,大小是小區大小BlockSize;

    這個方法第三個參數是bool類型,爲 false 時,表示等待任一位置觸發時返回;爲 true 時,表示等待全部觸發點所有觸發,再返回;注意上面代碼它的兩種調用,因而可知,這也是控制程序執行進度的手段;

                                                           

步驟4. 調用播放方法;

#include "stdafx.h"
#include <dsound.h>
#include <mmsystem.h>
#include "WaveStructs.h"

#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")


#define BlockSize     32*1024   // 緩存區每一個小區的大小(能夠設定)
#define MaxAudio      4         //緩存區的小區個數(能夠設定爲2個以上)

const char testWave2[] = "C:/Windows/Media/Ring02.wav";
const char testWave5[] = "C:/Windows/Media/Ring05.wav";

HWND hwnd;

int main(int argc, char* argv[])
{
    SetConsoleTitleA("DSTest");
    hwnd = FindWindowA(NULL, "DSTest");
    
    if (DSInit() == 0)
    {
        CreateThread(NULL, 0, PlayWave, (void*)testWave2, 0, NULL);
        CreateThread(NULL, 0, PlayWave, (void*)testWave5, 0, NULL);

        getchar();
    }
    return 0;
}

    使用DS必須先添加dsound.h,另外還需mmsystem.h頭文件,添加dsound.lib, dxguid.lib到項目中;

    這裏播放了兩個實例wav文件,還能夠再加幾個,多個音頻能夠同時播放互不影響。

  (完)

相關文章
相關標籤/搜索