DirectSoundでWAVEファイルを再生する

前回はDirectSoundによるOggVorbisファイルの再生について書きましたが、
今回はDirectSoundによるWAVEファイルの再生について書きます。

全体的な処理の流れは以下のようになります。

 WAVEファイルオープン
   ↓
 WAVEファイルのフォーマット・デコード後サイズ取得
   ↓
 DirectSoundの初期化
   ↓
 WAVEファイルのデータをDirectSoundバッファにデコード
   ↓
 バッファの再生
   ↓
 後始末

OggVorbisファイルの時と処理の流れはほぼ一緒です。

以上の流れのソースコードを以下に記します。

#include <windows.h>
#include <stdexcept>
#include <dsound.h>
#include <fstream>

#define RELEASE(obj) if ( obj ) { obj->Release(); obj = NULL; }

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

// 再生用ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch ( uMsg ) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

// メイン関数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    bool isOpened = false;

    HMMIO hMmio = NULL;
    IDirectSound8* pDS = NULL;
    IDirectSoundBuffer *pPrimaryBuffer = NULL;
    IDirectSoundBuffer8* pSecondaryBuffer = NULL;

    try {
        //---------------------------------------------------------
        //  Waveファイルの読み込み
        //---------------------------------------------------------

        // Waveファイルを開く
        LPTSTR waveFile = TEXT("test.wav");
        HMMIO hMmio = mmioOpen(waveFile, NULL, MMIO_READ);
        if ( hMmio == NULL )
            throw std::runtime_error("ファイルオープンに失敗しました。");


        isOpened = true;

        // RIFFチャンクの探索
        MMCKINFO mmckRiff;
        mmckRiff.fccType = mmioFOURCC('W', 'A', 'V', 'E');
        MMRESULT mr = mmioDescend(hMmio, &mmckRiff, NULL, MMIO_FINDRIFF);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("RIFFチャンクの探索に失敗しました。");

        // フォーマットチャンクの探索
        MMCKINFO mmckFormat;
        mmckFormat.ckid = mmioFOURCC('f', 'm', 't', ' ');
        mr = mmioDescend(hMmio, &mmckFormat, &mmckRiff, MMIO_FINDCHUNK);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("フォーマットチャンクの探索に失敗しました。");


        // フォーマットチャンク内のデータの読み込み
        BYTE* pTmpData = new BYTE[mmckFormat.cksize];
        DWORD dwReadSize = mmioRead(
            hMmio,
            reinterpret_cast<hpstr>(pTmpData),
            mmckFormat.cksize
            );

        // 読み込んだサイズが正しいかどうかのチェック
        if ( dwReadSize != mmckFormat.cksize ) {
            delete[] pTmpData;
            throw std::runtime_error("WAVEFORMATの読み込みに失敗しました。");
        }

        WAVEFORMATEX wf = reinterpret_cast<waveformatex &>(*pTmpData);
        delete[] pTmpData;

        // RIFFチャンクに戻る
        mmioAscend(hMmio, &mmckFormat, 0);

        // データチャンクの探索
        MMCKINFO mmckData;
        mmckData.ckid = mmioFOURCC('d', 'a', 't', 'a');
        mr = mmioDescend(hMmio, &mmckData, &mmckRiff, MMIO_FINDCHUNK);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("データチャンクの探索に失敗しました。");


        // デコード後のデータサイズの取得
        DWORD decodeSize = mmckData.cksize;


        //---------------------------------------------------------
        //  再生用ウィンドウの作成
        //---------------------------------------------------------
        WNDCLASS wc;
        wc.style = CS_CLASSDC;
        wc.lpfnWndProc = WndProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = static_cast<hbrush>(GetStockObject(WHITE_BRUSH));
        wc.lpszMenuName = NULL;
        wc.lpszClassName = TEXT("PlayTest");

        if ( !RegisterClass(&wc) )
            throw std::runtime_error("ウィンドウクラスの登録に失敗しました。");

        HWND hWnd = CreateWindow(
            wc.lpszClassName,
            TEXT("Waveファイル再生テスト"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            NULL,
            NULL,
            hInstance,
            NULL
            );

        if ( hWnd == NULL )
            throw std::runtime_error("ウィンドウの作成に失敗しました。");

        // ウィンドウの表示
        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);


        //---------------------------------------------------------
        //  DirectSoundの初期化
        //---------------------------------------------------------

        // DirectSoundの初期化
        if ( FAILED(DirectSoundCreate8(NULL, &pDS, NULL)) )
            throw std::runtime_error("DirectSoundの初期化に失敗しました。");

        // 協調レベルの設定
        if ( FAILED(pDS->SetCooperativeLevel(hWnd, DSSCL_PRIORITY)) )
            throw std::runtime_error("協調レベルの設定に失敗しました。");

        // プライマリバッファの作成
        DSBUFFERDESC desc;
        desc.dwSize = sizeof(DSBUFFERDESC);
        desc.dwFlags = DSBCAPS_GLOBALFOCUS;
        desc.dwBufferBytes = decodeSize;
        desc.dwReserved = 0;
        desc.lpwfxFormat = &wf;
        desc.guid3DAlgorithm = GUID_NULL;

        if ( FAILED(pDS->CreateSoundBuffer(&desc, &pPrimaryBuffer, NULL)) )
            throw std::runtime_error("プライマリバッファの作成に失敗しました。");

        // セカンダリバッファの作成
        if ( FAILED(pPrimaryBuffer->QueryInterface(
            IID_IDirectSoundBuffer8,
            reinterpret_cast<lpvoid *>(&pSecondaryBuffer))) )
            throw std::runtime_error("セカンダリバッファの作成に失敗しました。");


        // プライマリバッファの開放
        RELEASE(pPrimaryBuffer);


        //---------------------------------------------------------
        //  WAVEファイルのデータをバッファにロード
        //---------------------------------------------------------

        // バッファロック
        LPVOID pWrite;
        DWORD lockSize;
        if ( FAILED(pSecondaryBuffer->Lock(
            0,
            0,
            &pWrite,
            &lockSize,
            NULL,
            NULL,
            DSBLOCK_ENTIREBUFFER)) )
            throw std::runtime_error("バッファのロックに失敗しました。");

        DWORD totalReadSize = 0;
        bool result = true;

        // データロード
        mmioRead(hMmio, static_cast<hpstr>(pWrite), lockSize);

        // アンロック
        pSecondaryBuffer->Unlock(pWrite, lockSize, NULL, 0);

        // ファイル読み込みの成否チェック
        if ( !result )
            throw std::runtime_error("Waveファイルの読み込みに失敗しました。");


        // WAVEファイルを閉じる
        mmioClose(hMmio, 0);

        // 再生
        pSecondaryBuffer->Play(0, 0, 0);

        // ウィンドウメッセージループ
        MSG msg;
        while ( GetMessage(&msg, NULL, 0, 0) > 0 ) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // 停止
        pSecondaryBuffer->Stop();


        // バッファ開放
        RELEASE(pDS);


    }
    catch ( std::exception& e ) {
        if ( isOpened )
            mmioClose(hMmio, 0);

        RELEASE(pDS);

        MessageBoxA(NULL, e.what(), NULL, MB_OK | MB_ICONERROR);
        return -1;
    }

    return 0;
}

WAVEファイルはRIFFファイルとして定義されています。
WAVEファイルの読み込みにはmmio~関数を用います。
これはWindowsのマルチメディア操作用関数のことで、RIFFファイルを比較的楽に扱うことが出来ます。

RIFFファイルはWAVEファイルに限らずたとえばMP3などのサウンドデータもサポートします。

これらのAPIを使用するためにはwinmm.libをインポートする必要があります。

WAVEファイルはmmioOpen関数で開きます。

        // Waveファイルを開く
        LPTSTR waveFile = TEXT("test.wav");
        HMMIO hMmio = mmioOpen(waveFile, NULL, MMIO_READ);
        if ( hMmio == NULL )
            throw std::runtime_error("ファイルオープンに失敗しました。");

ファイルを開いたら、WAVEFORMATEX構造体データと実データを読み込みます。
今回は読み込み処理を示すだけにして詳細の説明は割愛させていただきます。

        // RIFFチャンクの探索
        MMCKINFO mmckRiff;
        mmckRiff.fccType = mmioFOURCC('W', 'A', 'V', 'E');
        MMRESULT mr = mmioDescend(hMmio, &mmckRiff, NULL, MMIO_FINDRIFF);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("RIFFチャンクの探索に失敗しました。");

        // フォーマットチャンクの探索
        MMCKINFO mmckFormat;
        mmckFormat.ckid = mmioFOURCC('f', 'm', 't', ' ');
        mr = mmioDescend(hMmio, &mmckFormat, &mmckRiff, MMIO_FINDCHUNK);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("フォーマットチャンクの探索に失敗しました。");


        // フォーマットチャンク内のデータの読み込み
        BYTE* pTmpData = new BYTE[mmckFormat.cksize];
        DWORD dwReadSize = mmioRead(
            hMmio,
            reinterpret_cast<hpstr>(pTmpData),
            mmckFormat.cksize
            );

        // 読み込んだサイズが正しいかどうかのチェック
        if ( dwReadSize != mmckFormat.cksize ) {
            delete[] pTmpData;
            throw std::runtime_error("WAVEFORMATの読み込みに失敗しました。");
        }

        WAVEFORMATEX wf = reinterpret_cast<waveformatex &>(*pTmpData);
        delete[] pTmpData;

        // RIFFチャンクに戻る
        mmioAscend(hMmio, &mmckFormat, 0);

        // データチャンクの探索
        MMCKINFO mmckData;
        mmckData.ckid = mmioFOURCC('d', 'a', 't', 'a');
        mr = mmioDescend(hMmio, &mmckData, &mmckRiff, MMIO_FINDCHUNK);
        if ( mr != MMSYSERR_NOERROR )
            throw std::runtime_error("データチャンクの探索に失敗しました。");


        // デコード後のデータサイズの取得
        DWORD decodeSize = mmckData.cksize;

ここまで出来たら、後はDirectSoundバッファにデータを読み込んで再生できるようになります。

        // データロード
        mmioRead(hMmio, static_cast<hpstr>(pWrite), lockSize);

OggVorbisファイルの再生同様にワンパターンな処理なので、関数化やクラス化して使ったほうが良いでしょう。

■参考サイト
RIFFファイルフォーマット
WAVEの再生