頑張ってストリーミング再生させてみる - XAudio2

ストリーミング再生となるとサブスレッドを用意する必要があります。

ただ、_beginthreadexだとか、_beginthreadだとか、BeginThreadだとか面倒です。

もっと手軽にかけないの?ということで今回はboost.threadを使いましょう。

boost.threadで関数オブジェクトを渡してストリーミングスレッドとします。

XAudio2のボイスは処理をキュー形式で行っているので、私達がやることは単純で

  1. 再生データ用バッファを用意する
  2. バッファへ再生するブロックデータを読み取ってボイスへ送信
  3. バッファ1つが再生を終了するまで待機、バッファがまだバッファの数-1以上あるのであれば継続して待機する

これだけです。

待機、というのがちょっと分かりづらいかも知れないのですが、ボイスに対してコールバックを設定することができます。

IXAudio2VoiceCallBackインターフェイスがまさにそうなのですが、これを私達の方で実装する必要があります。

IXAudio2VoiceCallback

今回は、キュー1つの再生が終わるまで待機したいのでOnBufferEndメソッドに処理を記述し、それ以外の純粋仮想メソッドは空実装にします。

// voice_callback.h
#include <xaudio2.h>

namespace my_audio
{

class voice_callback : public IXAudio2VoiceCallback
{
public:
    voice_callback() : BufferEndEvent_( CreateEvent( NULL, FALSE, FALSE, NULL ) ){}
    virtual ~voice_callback(){ CloseHandle( BufferEndEvent_ ); }

    HANDLE operator()() const { return BufferEndEvent_; };
    void __stdcall OnBufferEnd(void*){ SetEvent( BufferEndEvent_ ); }

    // 空実装(未使用)
    // -------------------------------------------------------------------------
    void __stdcall OnVoiceProcessingPassStart(UINT32){}
    void __stdcall OnVoiceProcessingPassEnd(){}
    void __stdcall OnStreamEnd(){}
    void __stdcall OnBufferStart(void*){}
    void __stdcall OnLoopEnd(void*){}
    void __stdcall OnVoiceError(void*,HRESULT){}

private:
    HANDLE   BufferEndEvent_;
};

};

ボイスはある操作を完了したときにこれらのメソッドの中のどれかを呼び出すようになっているので、今回はイベントハンドルを使って待機させます。

イベントが発生(バッファ1つの再生が終了)するまで待機し、発生したら待機を続けるか否かを判断します。

ちょっと今回は長くなりそうなので、例外処理並びにエラーチェックについては全て省略します

#include <xaudio2.h>
#include <boost/thread.hpp>
#include <boost/cstdint.hpp>
#include "xaudio2_singleton.h"
#include "voice_callback.h"
#include "pcm_loader.h"

using my_audio::voice_callback;
using my_audio::pcm_loader;
using std::string;
using boost::thread;
using boost::uint32_t;
using boost::uint8_t;

// ストリーミング再生クラス
class streaming_player
{
public:
    explicit streaming_player( const string& file_name )
        : Loader_( pcm_loader::create_instance( file_name ) )
        , VoiceCallBack_()
        , Source_Voice_()
        , CurrentBuff_( 0 )
    {
        xaudio2_singleton& xaudio2_ = xaudio2_singleton::get_instance();

        // ソースボイスの作成
        WAVEFORMATEX wfx;
        Loader_->get_wfx( wfx );
        xaudio2_->get_engine()->CreateSourceVoice( &Source_Voice_
                                                 , &wfx
                                                 , 0
                                                 , XAUDIO2_DEFAULT_FREQ_RATIO
                                                 , &VoiceCallBack );
        Source_Voice_->Start();
    }
    ~streaming_player()
    {
        // ソースボイスの削除
        Source_Voice_->DestroyVoice();
    }

    void operator()()
    {
        XAUDIO2_VOICE_STATE state  = { 0 };
        XAUDIO2_BUFFER      buffer = { 0 };
        buffer.Flags               = XAUDIO2_END_OF_STREAM;

        while( true )
        {
            // バッファへ読み取り、読み取ったサイズが0なら(ファイル終端なら)ループ終了
            buffer.AudioBytes = Loader_.get_segment( Buffer_[ CurrentBuff_ ], BUFFER_SIZE );
            if( buffer.AudioBytes <= 0 ) { break; };

            // ボイスの情報を取得し、キューが指定の数以上の間ループ
            while( Source_Voice_->GetState( &state ), state.BuffersQueued >= BUFFER_COUNT - 1 )
            {
                // コールバックでイベントが発生するまで待機
                WaitForSingleObject( VoiceCallBack_(), INFINITE );
            }

            // 今回読み取ったバッファをボイスへ送信
            buffer.pAudioData = Buffer_[ CurrentBuff_ ];
            Source_Voice_->SubmitSourceBuffer( &buffer );

            // 次の格納先バッファを設定
            ++CurrentBuff_ %= BUFFER_COUNT;
        }

        // キューを全て処理
        while( Source_Voice_->GetState( &state ), state.BuffersQueued >= BUFFER_COUNT - 1 )
        {
            WaitForSingleObject( VoiceCallBack_(), INFINITE );
        }
    }

private:
    static uint8_t          BUFFER_COUNT = 3;
    static uint32_t         BUFFER_SIZE  = 32768;

    IXAudio2SourceVoice*    Source_Voice_;
    voice_callback          VoiceCallBack_;
    pcm_loader::value_type  Loader_;

    unsigned char           Buffer_[ BUFFER_COUNT ][ BUFFER_SIZE ];
    uint8_t                 CurrentBuff_;
};

int main()
{
    // 初期化
    xaudio2_singleton& xaudio2_ = xaudio2_singleton::get_instance();
    if( FAILED(xaudio2_.initialize()) ){ return -1; };

    {
        // インスタンス生成
        streaming_player stream( "test.wav" );

        // スレッドを作成し、再生が終わるまで待機
        thread thr( stream );
        thr.join();
    }

    // 終了
    xaudio2_.release();
}

ちょっと長くなりましたが…これでもエラーチェックと例外について処理は外したのでご勘弁を。

IXAudio2SourceVoice::GetStateメソッドを使ってソースボイスの状態を取得します。

それを使ってキューの数をチェックし、バッファの数-1(今回の例では2個)以上であれば待機ループを続行します。

それだけ分かれば多分、上のソースは読めるんじゃないかなと思います。

ストリーミングをただ行なうだけだったら簡単でした。ここからゲーム用にするのがめんどくさい。

boost.mutexなどを用いて再生しないと、ただ単にストリーミング再生して、再生が終わるまで待機しかできませんからね。

boost.threadのインスタンスをストリーミング再生クラスが持ったりとかしてればもうちょっと簡単にゲーム用にできるかも。