というわけで、予告していたとおりXAudio2です。今回はMediaFoundationの時とは違い前後編で終わりです。
DirectSoundの時とは違い再生するだけならとても簡単(下手をすると同期などを考える必要がない)なので。
また、今となっては古い話ですが、コードとしてはXbox系と共用できるよう設計されているのでちょっとおもしろいです。
なお、今回のコードは基本的に上から下に続いている(切れ目が関数ごとではない)のでくっつけるとサンプルコードになります。
基本的にmainだけで完結するようにしています。
DirectSoundに対するXAudio2の優位性について
ちなみにDirectSoundよりかなり組みやすいと思います。
というのもDirectSoundに比べてサウンドバッファの転送がかなりやりやすいからです。
DirectSoundの場合はループバッファと呼ばれるバッファを「サウンドバッファ内に」作成してその場所に「直接メモリを書き込み」ます。
そのため、必要なタイミングで自分でバッファカーソル位置を調べて書き込みができるか確認、その後必要な領域まで書き込みを行います。
ループバッファなので領域が二ヶ所になってしまうこともあるためかなり面倒でした。
XAudio2の場合、サウンドバッファは一応「実装側が」作成して保持し、XAudio2が持つサウンドキューに「再生情報だけを」入れます。
そして必要なタイミングでXAudio2がサウンドキューからデータを取得して再生する、という状態になります。
そのため、サウンドバッファは実装側が再生が完了するまで保持することを条件としてバッファ転送がただのキューに入れる処理になるので
見た目が非常にわかりやすいです。しかもキュー処理完了の通知もXAudio2側から通知を受け取ることも自分で調べることもできるので楽です。
この辺はDirectSoundをやってから見るとちょっとうらやましいです・・・。
グローバル部分を提示
#define _WIN32_WINNT 0x0510 #include <windows.h> #include <mmsystem.h> #include <xaudio2.h> #pragma comment(lib,"winmm.lib") //#pragma comment(lib,"xmcore.lib") //#pragma comment(lib,"xaudio2.lib") #define _USE_VOICECALLBACK_ HANDLE g_hEvent = NULL; HMMIO g_hmmio = NULL; MMIOINFO g_mmioinfo; MMCKINFO g_riffchunkinfo; MMCKINFO g_datachunkinfo; WAVEFORMATEX g_wfx; IXAudio2 *g_lpXAudio = NULL; IXAudio2MasteringVoice *g_lpMasterVoice = NULL; IXAudio2SourceVoice *g_lpSourceVoice = NULL; #define BUFFERQUEUE_MAX 4 #define BUFFERQUEUE_ALLOC (BUFFERQUEUE_MAX + 1)
ライブラリの連結指示が抜けているのは必要なかったからです。DirectXのヘルプ(June 2010)には必要とか何とか書いてあったのですが・・・。
途中にある_USE_VOICECALLBACK_はコールバックを使うパターンと使わないパターンを分けるためのものです。
どちらでも普通に動きます。が、自分でゲームライブラリなどとして実装するときはコールバックパターンを使う方がよいと思います。
また、今回サウンドのロードは面倒なのでmmio系を使います。自前処理を使うときは適当にアレンジを。
今回、XAudio2を使うときに使うインターフェイスは以下になります。
インターフェイス | 説明 |
---|---|
IXAudio2 | XAudio2の管理インターフェイス。DirectSoundのIDirectSound(8)に相当 |
IXAudio2MasteringVoice | XAudioが再生時にミキシングした後の状態を管理するインターフェイス。マスターボリュームなどの設定をここで行う |
IXAudio2SourceVoice | XAudioのサウンドバッファ単位。DirectSoundのIDirectSoundBuffer(8)に相当 |
サウンドコールバック処理
class CSoundCallback : public IXAudio2VoiceCallback{ public: CSoundCallback(){ } ~CSoundCallback(){ } void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32 BytesRequired){ } void STDMETHODCALLTYPE OnVoiceProcessingPassEnd(void){ } void STDMETHODCALLTYPE OnStreamEnd(void){ SetEvent(g_hEvent); } void STDMETHODCALLTYPE OnBufferStart(void *pBufferContext){ SetEvent(g_hEvent); } void STDMETHODCALLTYPE OnBufferEnd(void *pBufferContext){ } void STDMETHODCALLTYPE OnLoopEnd(void *pBufferContext){ } void STDMETHODCALLTYPE OnVoiceError(void *pBufferContext,HRESULT Error){ } };
コールバックと言うよりリスナパターンです。ほぼそのままです。
XAudio2では再生中のコールバックはIXAudio2VoiceCallbackを実装することで行います。
が、XAudio2に存在するだいたいのインターフェイスはCOMではありません。(マスターであるIXAudio2が例外の一つですが)
そのためCOMの流儀に従うことなく組むことができます。AddRefやQueryInterfaceを実装しなくても良いわけですね。
このインターフェイスで実装されるものでわかりやすいのはOnStreamEnd、OnLoopEnd、OnVoiceErrorです。ほぼそのままです。
ちなみにOnLoopEndはループが発生するたびに呼ばれますので注意です。
残りの4つは連動して呼び出されます。通常は
- バッファキューに再生データを転送通知
- 再生開始(XAudio内の再生バッファは空、バッファキュー内のデータは未使用とする)
- XAudioが再生バッファを埋めようとする
- OnVoiceProcessingPassStartが呼び出される。BytesRequiredは通常読み込むべきバイト数となる
- バッファキュー内のデータを参照
- OnBufferStartが呼び出される。pBufferContextはバッファ通知時に与えられるContext値
- データを指定バイト数だけ転送
- OnVoiceProcessingPassEndが呼び出される。
- 転送した分をサウンドデバイスより再生
- OnVoiceProcessingPassStartが呼び出される
- 残っているバッファキュー内のデータを参照。まだ残っているときはそのデータを転送
- OnVoiceProcessingPassEndが呼び出される。
・・・
- OnVoiceProcessingPassStartが呼び出される。
- 残っているバッファキュー内のデータを参照。まだ残っているときはそのデータを転送。この段階で対象としていたバッファキューデータが終了する
- OnBufferEndが呼び出される。pBufferContextはバッファ通知時に与えられるContext値
- OnVoiceProcessingPassEndが呼び出される。
- OnVoiceProcessingPassStartが呼び出される。
- バッファキュー内のデータを参照
- OnBufferStartが呼び出される。
・・・
と言う流れで呼び出されます。キューに入れたデータが余りに細かすぎるとこうはなりませんが・・・
Waveファイルを開く処理
bool LoadSound(const TCHAR *lpFileName) { MMCKINFO mmckinfo; PCMWAVEFORMAT pcmwf; MMRESULT mmret; memset(&g_mmioinfo,0x00,sizeof(g_mmioinfo)); g_hmmio = mmioOpen(const_cast<TCHAR *>(lpFileName),&g_mmioinfo,MMIO_READ); if(g_hmmio == NULL) return false; memset(&g_riffchunkinfo,0x00,sizeof(g_riffchunkinfo)); g_riffchunkinfo.fccType = mmioFOURCC('W','A','V','E'); mmret = mmioDescend(g_hmmio,&g_riffchunkinfo,NULL,MMIO_FINDRIFF); if(mmret != MMSYSERR_NOERROR) return false; memset(&mmckinfo,0x00,sizeof(mmckinfo)); mmckinfo.ckid = mmioFOURCC('f','m','t',' '); mmret = mmioDescend(g_hmmio,&mmckinfo,&g_riffchunkinfo,MMIO_FINDCHUNK); if(mmret != MMSYSERR_NOERROR) return false; if(mmckinfo.cksize >= sizeof(WAVEFORMATEX)){ mmioRead(g_hmmio,(HPSTR)&g_wfx,sizeof(g_wfx)); } else{ mmioRead(g_hmmio,(HPSTR)&pcmwf,sizeof(pcmwf)); memset(&g_wfx,0x00,sizeof(g_wfx)); memcpy(&g_wfx,&pcmwf,sizeof(pcmwf)); g_wfx.cbSize = 0; } mmioAscend(g_hmmio,&mmckinfo,0); memset(&g_datachunkinfo,0x00,sizeof(g_datachunkinfo)); g_datachunkinfo.ckid = mmioFOURCC('d','a','t','a'); mmret = mmioDescend(g_hmmio,&g_datachunkinfo,&g_riffchunkinfo,MMIO_FINDCHUNK); if(mmret != MMSYSERR_NOERROR) return false; return true; }
MMIO系を使って簡単に実装したものです。RIFFの構造やらWAVEの形式についてはここでは説明しません。
とりあえずいえるのはこの処理が完了すれば以降のmmioReadによって読み込まれるのはサウンドのサンプルデータそのものになります。
後編はmain処理を
一応コールバックの説明はしましたが本体の処理の流れについては書いていません。
後編はその辺をある程度詳しく書いてみたいと思います。