スレッドを使わないでストリーミング再生 - XAudio2

ついでに書きます。スレッドを何とか使わない方向でストリーミング再生をしたい。
これはIXAudio2VoiceCallback::OnBufferStart/Endを用いるのが楽かも知れません。

IXAudio2VoiceCallback::OnBufferStart
IXAudio2VoiceCallback::OnBufferEnd

2つのメソッドの引数であるpContextはXAUDIO2_BUFFER構造体のメンバに指定できるpContextです。
ここにデコーダのポインタなど持っておけばEndが呼び出されたときに読み出して再生末尾に挿入とかができます。
この方法はnagoya313さんがご紹介されております。

第8回〜oggファイルの読み込みとストリーミング再生〜 - 名古屋313の日記

なので、今回はもう1つ使えそうなメソッドに光を当ててみます。
IXAudio2VoiceCallback::OnVoiceProcessingPassStartメソッドです。

IXAudio2VoiceCallback::OnVoiceProcessingPassStart

このメソッドはボイスがキューデータを読み取る直前に呼び出されます。なのでStartした直後に1度呼び出されるはずです。
これの引数は「いますぐ送信する必要があるバイト数」となっています*1。つまりボイスが最低限要求するバッファのサイズですね。
要求サイズを私達が実値として知る必要は無いのですが(知らなくても処理できるように書けるべき)、調べてみたところどうも通常のPCMの1/10秒分のサイズを要求しています(1764Byte)
引数の説明にも「ジャストインタイムのストリーミング シナリオの実装」と書かれており、スレッドを作らずにストリーミングができる可能性を示唆してそうです。
まあそれだけ持っておけばストリーミング再生が容易にできそうですね。
今回は用心のために3つのバッファを持つクラスを考えてみました。サイズはとりあえず1764Byteにしておきます。
(動作環境、PCMのフォーマットなどによってこの要求サイズが変わる可能性は十分にあります)

class stream_buffer
{
public:
	StreamBuffer() : CurrentBuffer_(0) {};
	void		next()		     { ++CurrentBuffer_ %= buffer_count(); };
	uint8_t*	get()                { return Buffer_[CurrentBuffer_]; };
	size_t		buffer_count() const { return BUFFER_COUNT; };
	size_t		buffer_size()  const { return BUFFER_SIZE; };

private:
	static const size_t BUFFER_COUNT = 3;
	static const size_t BUFFER_SIZE  = 1764;

	uint8_t		Buffer_[BUFFER_COUNT][BUFFER_SIZE];
	uint8_t		CurrentBuffer_;
};

こんな感じで良いでしょう。
で、これとデコーダ、それとソースボイスをメンバとしたIXAudio2VoiceCallbackの派生クラスを書きます。

IXAudio2SourceVoice* CreateSourceVoice( WAVEFORMATEX const& wfx, IXAudio2VoiceCallback& callback )
{
	IXAudio2SourceVoice* pSource;
	const auto hr = xaudio2_manager::get_device()->CreateSourceVoice( &pSource
									, &wfx
									, 0
									, XAUDIO2_DEFAULT_FREQ_RATIO
									, &callback );
	if( FAILED(hr) ){ throw std::runtime_error("ソースボイスの作成に失敗しました"); };
	return pSource;
}

struct voice_deleter
{
	void operator()( IXAudio2Voice* voice ) const { if( voice ){ voice->DestroyVoice(); }; };
};

class stream_player : public IXAudio2VoiceCallback
{
public:
	typedef std::unique_ptr<IXAudio2SourceVoice,voice_deleter> voice_type;

	stream_player( wav_decoder& decoder )
		: Buffer_()
		, Source_()
		, Decoder_( decoder )
	{
		Source_ = voice_type( CreateSourceVoice(decoder.get_format(),*this) );
	}

	void start()
	{
		Source_->Start();
	};
	void stop()
	{
		Source_->Stop();
	};

	void __stdcall OnVoiceProcessingPassStart( UINT32 BytesRequired )
	{
		XAUDIO2_BUFFER buf = { 0 };
		buf.AudioBytes = Decoder_.get_segment( Buffer_.get(), BytesRequired );
		if( buf.AudioBytes <= 0 )
		{
			Decoder_.seek( 0, SEEK_SET );
			OnVoiceProcessingPassStart( BytesRequired );
			return;
		}
		buf.pAudioData = Buffer_.get();
		Source_->SubmitSourceBuffer( &buf );
		Buffer_.next();
	};

private:
	StreamBuffer	Buffer_;
	voice_type	Source_;
	wav_decoder&	Decoder_;

public:
	//! empty
	// -------------------------------------------------------------------------
	void __stdcall OnVoiceProcessingPassEnd(){};
	void __stdcall OnStreamEnd(){};
	void __stdcall OnBufferStart(void* pBufferContext){};
	void __stdcall OnBufferEnd(void* pBufferContext){};
	void __stdcall OnLoopEnd(void* pBufferContext){};
	void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error){};
	// -------------------------------------------------------------------------
};

int main()
{
	xaudio2_manager::get_instance()->initialize();

	{
		wav_decoder decoder( L"test.wav" );

		stream_player player( decoder );
		player.start();
		auto c = getchar();
		player.stop();
	}

	xaudio2_manager::get_instance()->release();
}

ちょっと紹介してないxaudio2_managerクラスとかありますが、メソッド読んで適宜読み替えてみてください。
デバッグエンジンだと起動時にWarningをはかれるかも知れませんが、殆ど問題なく動きました。
ストリーミング再生を実行しているのは実質OnVoiceProcessingPassStartメソッドだけです。IXAudio2SourceVoiceが持つ再生スレッドが勝手にこのメソッドを読んでくれるのでスレッド用関数を書く必要や、処理ループを書く必要がありません。(Mutexなどでしっかり制御する必要はありそうです)
IXAudio2SourceVoice::Startを呼び出すと勝手にこのメソッドがどんどん呼び出され、IXAudio2SourceVoice::Stopが呼び出されれば処理が止まります。
バッファも必要最小限あれば問題ないので、簡単に済ませるならこれが楽だと思います。逆に、凝った処理をしなければならない場合はこの方法は不向きだと思われます。
一番の注意事項として、IXAudio2VoiceCallbackの全ての純粋仮想メンバは全て数ミリ秒以上の遅延を発生させるとオーディオ処理に不具合をもたらします。
なのでコンピュータの処理性能が低かったり、ボイスの数が多かったりした場合にはより不具合を発生させやすいのでこれらの方法は危ないかも知れません。
対処法としては別のスレッドにシグナルを送信して実際の読み出しは他のスレッドで行なったり、イベントドリブンを使うと良いようです。

XAudio2 のコールバック

別のスレッドを使うと今回のテーマに思いっきり反するのであれですが、例えばboost.signal2とかでしょうか。とにかく、今回の方法は簡単に再生する程度でとどめておくべきなのかもしれません。
面白そうなので簡易的ストリーミング再生クラスとして、もうちょっと精錬してから今書いてるライブラリに組み込む予定でいます。

*1:PCMの場合のみで、xWMAなどXAudio2がサポートする他のフォーマットでは常に0らしいです