XAudio2を使ってみる 後編

というわけで後編です。今回は実際に再生させてみるものです。前編はこちらから。

変数宣言、初期化

int main(void)
{
	CSoundCallback callback; XAUDIO2_BUFFER bufinfo; XAUDIO2_VOICE_STATE state;
	unsigned char *buf,*ptr; UINT32 buflen,cnt; LONG readlen; HRESULT hr; DWORD dw;
	CoInitializeEx(NULL,COINIT_MULTITHREADED);
	do{
		buf = NULL; cnt = 0;

本来、C++のコードとして書いているので先頭に変数宣言を固める意味は全くありません。

というより、C++で書く時は変数宣言は必要になるまで遅らせた方がコードの効率としては上になります。

(構造体、クラスをメモリ上に確保するときにコンストラクタを動かすことになるのが主な要因)

が、こういうときには必要な変数をいっぺんに出した方がわかりやすいのでこんな書き方になっています。

XAudio2を使うときには必ずCOMの初期化が必要になります。そのためCoInitialize(Ex)は必須です。

また、HRESULTの処理を行うのでサンプル時はgotoもどきがあった方がわかりやすいのでdo~while(0)をつかって書いています。

サウンドの読み込み

		g_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
		if(g_hEvent == NULL) break;
		if(!LoadSound(TEXT("test.wav"))) break;

浮いているのは前のコードの続きなのでタブが入っているからです。

イベントの作成はコールバックからの同期用です。コールバックを使わなければ必要はないのですが・・・。

XAudio2の作成

		hr = XAudio2Create(&g_lpXAudio,0,XAUDIO2_DEFAULT_PROCESSOR);
		if(FAILED(hr)) break;
		hr = g_lpXAudio->CreateMasteringVoice(&g_lpMasterVoice,XAUDIO2_DEFAULT_CHANNELS,XAUDIO2_DEFAULT_SAMPLERATE,0,0,NULL);
		if(FAILED(hr)) break;
#ifdef _USE_VOICECALLBACK_
		hr = g_lpXAudio->CreateSourceVoice(&g_lpSourceVoice,&g_wfx,XAUDIO2_VOICE_NOPITCH | XAUDIO2_VOICE_MUSIC,XAUDIO2_DEFAULT_FREQ_RATIO,&callback,NULL,NULL);
#else
		hr = g_lpXAudio->CreateSourceVoice(&g_lpSourceVoice,&g_wfx,XAUDIO2_VOICE_NOPITCH | XAUDIO2_VOICE_MUSIC,XAUDIO2_DEFAULT_FREQ_RATIO,NULL,NULL,NULL);
#endif
		if(FAILED(hr)) break;

必要なインターフェイスを順番に作っているだけです。

ちなみにXAudio2Createは関数のように見えますが実際はxaudio2.hにインライン実装(CoCreateInstance+Initialize)してあるだけでした。

MasteringVoiceはDirectSoundで言うプライマリバッファの役割なのでここで最終的に出力されるサウンドの形式を決定します。

通常はデフォルトを使用すれば問題ないですが、自分で設定したいときはここを適当に変更するといいです。

SourceVoiceの作成もほぼそのままです。なお、作成時にサンプルではXAUDIO2_VOICE_NOPITCHとXAUDIO2_VOICE_MUSICを与えてありますが、どちらもいりません。

特にXAUDIO2_VOICE_MUSICは対象のボイスアイテムがMUSIC(BGM)として扱われるというヒントを示しているのでよほどでないと与えるべきではないです。

まあ、サンプルなのとこれ一つしかないのでとりあえず入れてあります。

初期バッファの転送、再生

		buflen = g_wfx.nAvgBytesPerSec / 4;
		buf = new unsigned char[buflen * BUFFERQUEUE_ALLOC];
		ptr = buf; cnt = (cnt + 1) % BUFFERQUEUE_ALLOC;
		readlen = mmioRead(g_hmmio,(HPSTR)ptr,buflen);
		if(readlen <= 0) break;
		memset(&bufinfo,0x00,sizeof(bufinfo));
		bufinfo.Flags = ((UINT32)readlen >= buflen) ? 0 : XAUDIO2_END_OF_STREAM;
		bufinfo.AudioBytes = readlen;
		bufinfo.pAudioData = ptr;
		bufinfo.PlayBegin = 0;
		bufinfo.PlayLength = readlen / g_wfx.nBlockAlign;
		hr = g_lpSourceVoice->SubmitSourceBuffer(&bufinfo,NULL);
		if(FAILED(hr)) break;
		hr = g_lpSourceVoice->Start(0,XAUDIO2_COMMIT_NOW);
		if(FAILED(hr)) break;

前編でも書きましたが、再生が完了するまでこちら側でサウンドデータを保持する必要があります。

そのため、バッファを保持するためのメモリを確保する必要があります。そのメモリ上にサウンドサンプルを読み込んでおくわけですね。

XAUDIO2_BUFFERにはサンプルキューに送るときに必要な情報を与えます。

今回は通常のWAVEデータなのでFlags,AudioBytes,pAudioData,PlayBegin,PlayLengthが対象です。

また、SEのようにループが必要なときはLoopBegin,LoopLength,LoopCountを与えておくとXAudio2側が勝手に対象区間をループ再生します。

pContextは今回は使いませんが、コールバック時に与えられる引数となるので必要であれば使います。

初期バッファが転送されればとりあえず再生命令を書けても大丈夫です。

本来なら必要な分だけキューを充填するのですが、この後にその処理があるので。

バッファの転送ループ

#ifdef _USE_VOICECALLBACK_
		while(dw = WaitForSingleObject(g_hEvent,INFINITE),dw == WAIT_OBJECT_0){
#else
		while(1){
#endif
			g_lpSourceVoice->GetState(&state);
			if(state.BuffersQueued == 0) break;
			while(state.BuffersQueued < BUFFERQUEUE_MAX && g_hmmio != NULL){
				ptr = buf + buflen * cnt; cnt = (cnt + 1) % BUFFERQUEUE_ALLOC;
				readlen = mmioRead(g_hmmio,(HPSTR)ptr,buflen);
				if(readlen <= 0) break;
				bufinfo.Flags = ((UINT32)readlen >= buflen) ? 0 : XAUDIO2_END_OF_STREAM;
				bufinfo.AudioBytes = readlen;
				bufinfo.pAudioData = ptr;
				bufinfo.PlayBegin = 0;
				bufinfo.PlayLength = readlen / g_wfx.nBlockAlign;
				g_lpSourceVoice->SubmitSourceBuffer(&bufinfo,NULL);
				state.BuffersQueued++;
				if(bufinfo.Flags & XAUDIO2_END_OF_STREAM){
					mmioClose(g_hmmio,0);
					g_hmmio = NULL;
				}
			}
#ifndef _USE_VOICECALLBACK_
			Sleep(50);
#endif
		}

IXAudio2VoiceCallbackを使用するときはそちらからバッファ充填が必要なタイミングもしくはバッファが終了したタイミングでイベントが発行されるのでそれを待てばいいです。

使わないときはSleepを使った処理を使ってCPU時間を譲るといいです。

ループそのものは単純です。キューに空きがあるならバッファを充填します。このとき、自分が持っているサウンドバッファが重複しないようにしてください。

そのためにBUFFERQUEUE_ALLOC(確保されているサウンドバッファの個数)はBUFFERQUEUE_MAX(キューに充填するサウンドバッファの数)+1としています。

なお、BUFFERQUEUE_MAXにも制限はありますが、XAUDIO2_MAX_QUEUED_BUFFERS以上(xaudio2.hでは64)にならないようなら大丈夫です。

このサンプルではMMIOのハンドルをファイルが有効かどうかを示すのに使っていますので、これを閉じる=サウンドが終了したという意味にしています。

解放処理

	} while(0);
	if(buf != NULL){ delete[] buf; }
	if(g_lpSourceVoice != NULL){ g_lpSourceVoice->DestroyVoice(); }
	if(g_lpMasterVoice != NULL){ g_lpMasterVoice->DestroyVoice(); }
	if(g_lpXAudio != NULL){ g_lpXAudio->Release(); }
	if(g_hmmio != NULL){ mmioClose(g_hmmio,0); }
	if(g_hEvent != NULL){ CloseHandle(g_hEvent); }
	
	CoUninitialize();
	return 0;
}

どこかで見たことのある解放ルーチンです。

前編でも書きましたが、XAudio2の大半のインターフェイスはCOMではないため、Releaseではなく独自の解放処理を呼び出す必要があります。

IXAudio2MateringVoiceやIXAudio2SourceVoiceはIXAudio2Voiceを継承していて、これの解放にはDestroyVoiceを使う、という規約があるのでこれを解放処理とします。

DirectSoundより見た目簡単ですよね・・・?

サウンドの占有などの概念が無くなったときに作られているのでウィンドウハンドルやらCooperativeやらが一切無いです。

また、サウンドバッファの扱いもキュー処理なので処理が非常にしやすいと思います。ループも回数指定までできますからね。

ちなみに後はこれを管理側と各サウンドバッファ側に分割して・・・というところで実装に迷いました。

それは「誰がサウンドバッファをキューに入れる処理を発行するか」というところです。

DirectSoundの時は各バッファごとにバッファ監視スレッドを作ってそれに操作させていたのですが・・・。

今回の場合はコールバックを確認してキューに送りつけるだけなので「各バッファごとに1スレッド」なんていうもったいないことをしたくないと思いました。

そのあたりの調整で微妙に悩んだ末とりあえず実装してみています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL