XAudio2を使ってみる。その4-ストリーミング再生-

ゲームなどのBGMでWAVファイルを使用する場合、数十Mバイトのバッファを用意するのは現実的ではないので、数秒程度のバッファを使って少しずつ再生していきます。まずはイベント通知型ではなく、ポーリング型で実装してみたいと思います。

解説

SubmitSourceBuffer()によって内部の再生キューにバッファが追加されます。



再生中、常にバッファの再生キューの状態を監視して、2つ以上のバッファが追加されている状態にします。



再生キューの先頭にあるバッファの再生が終わったら、空になったバッファに新しいデータを書き込んで、再生キューに追加します。


以降、同様に再生中のバッファと再生待ちのバッファが存在する状態を保ちつつ、先頭のバッファの再生が終わったら新しいデータを書き込んで、再生キューに追加します。書き込みカーソル(write)がWAVEデータの終端に達したら再生終了です。

実装

実装方法はいろいろあると思いますが、今回はプライマリ(再生中バッファ)、セカンダリ(再生待ち)の2つのバッファを交互に入れ替えながら再生してみます。

//プライマリとセカンダリの2つのバッファを用意する。
//プライマリは現在再生中のバッファ
//セカンダリは新しいデータの書き込み可能なバッファ
//プライマリの再生が終わったらプライマリとセカンダリを入れ替えて、
//常にプライマリが再生状態になるようにする
std::vector< BYTE > data[ 2 ];
int primary = 0;
int secondary = 1;
DWORD write = 0;                            //書き込みカーソル
手順
  • WAVファイルオープン
  • SourceVoice作成
  • 1秒のバッファをを2つ作成
  • セカンダリにデータを書き込んで、再生キューに追加しておく
  • 再生
  • ポーリングして、キュー内のバッファが1つになったら新しいデータを追加
    //
    //  WAVファイルを開く
    //
    CWaveFile wav;
    if( FAILED( wav.Open( "WAVファイル名を指定" ) ) )
        throw "Open";

    //
    //  WAVファイルのWAVEFORMATEXを使ってSourceVoiceを作成
    //
    WAVEFORMATEX* format = wav.GetFormat();
    if( FAILED( xaudio->CreateSourceVoice( &source_voice, format ) ) )
        throw "CreateSourceVoice";

    //
    //  1秒のバッファを2つ用意
    //
    for( int i = 0; i < 2; i ++ )
        data[ i ].resize( format->nAvgBytesPerSec * 1 );

    //
    //  あらかじめバッファを追加しておく
    //
    add_next_buffer( wav );

    //
    //  再生
    //
    source_voice->Start();

    //メッセージループ、またはゲームループ
    while( bool b = true )
    {
        //とりあえず、ESCキーで終了
        if( GetAsyncKeyState( VK_ESCAPE ) & 0x8000 )break;

        //再生状態を常に監視する
        polling( wav );

        Sleep( 10 );
    }
//セカンダリに新しいデータを書き込んで、再生キューに追加する
void add_next_buffer( CWaveFile& wav )
{
    std::vector< BYTE >& data_ = data[ secondary ];
    //
    //  secndaryにデータを書き込んで、書き込みカーソルを進める
    //
    DWORD read;     //実際にReadしたバイト数
    if( FAILED( wav.Read( &data_[ 0 ], data_.size(), &read ) ) )
        throw "Read";
    write += read;

    //
    //  SourceVoiceにデータを送信
    //
    XAUDIO2_BUFFER buffer = { 0 };
    buffer.AudioBytes = read;                       //バッファのバイト数
    buffer.pAudioData = &data_[ 0 ];                //バッファの先頭アドレス
    if( wav.GetSize() <= write )
        buffer.Flags = XAUDIO2_END_OF_STREAM;
    source_voice->SubmitSourceBuffer( &buffer );

    //
    //primaryとsecondaryの入れ替え
    //
    flip();
}
void polling( CWaveFile& wav )
{
    //状態を取得
    XAUDIO2_VOICE_STATE state;
    source_voice->GetState( &state );

    //再生キューに常に2つのバッファを溜めておく
    if( state.BuffersQueued < 2 )
    {
        //バッファにデータを書き込んで、再生キューに追加
        add_next_buffer( wav );
    }
}
//プライマリとセカンダリのフリップ
void flip()
{
    std::swap( primary, secondary );
}

再生の終了とループ再生

以上でも再生可能ですが、ファイルの終端まで再生すると例外が発生してプログラムが停止します。

ファイルの終端に達した場合、再生キューにこれ以上追加しないよう適当にフラグを追加しておきます。ファイルの終端チェックは書き込みカーソル(write)がWAVファイルのサイズを超えているかどうかでチェックします。

void polling( CWaveFile& wav )
{
    //状態の取得
    XAUDIO2_VOICE_STATE state;
    source_voice->GetState( &state );

    //再生キューに常に2つのバッファを溜めておく
    if( !eof && state.BuffersQueued < 2 )
    {
        if( write >= wav.GetSize() )
        {
            eof = true;
        }
        else
        {
            //バッファにデータを書き込んで、再生キューに追加
            add_next_buffer( wav );
        }
    }
}

ループ再生、特にイントロのスキップが可能なループ再生を行う場合はCWaveFileクラスにSeekメソッドを追加する必要があります。

class CWaveFile
{
public:
    HRESULT Seek( int offset, int origin )
    {
        // Seek to the data
        if( -1 == mmioSeek( m_hmmio, offset, origin ) )
            return DXTRACE_ERR( "mmioSeek", E_FAIL );

        m_ck.cksize -= offset;

        return S_OK;
    }
};

CWaveFileクラスには、再生位置を先頭に戻すResetFileメソッドが用意されているので、それも併用します。

void polling( CWaveFile& wav )
{
    //状態の取得
    XAUDIO2_VOICE_STATE state;
    source_voice->GetState( &state );

    //再生キューに常に2つのバッファを溜めておく
    if( !eof && state.BuffersQueued < 2 )
    {
        //書き込みカーソルがWAVファイルのサイズを超えている場合、
        //  ループ再生を行う場合、ファイルをループ位置までシーク
        //  ループ再生しない場合、再生終了
        if( write >= wav.GetSize() )
        {
            if( loop )
            {
                //dataチャンクの先頭に戻す
                wav.ResetFile();
                //dataチャンクの先頭からloop_pointまでシーク
                wav.Seek( loop_point, SEEK_CUR );
                //書き込みカーソルをloop_pointまで移動
                write = loop_point;
            }
            else
                eof = true;
        }

        if( !eof )
        {
            //バッファにデータを書き込んで、再生キューに追加
            add_next_buffer( wav );
        }
    }
}

サンプル

  • xaudio2_05.zip 直
  • VC++2008EE用
  • DirectXSDK(Nobember2008)
  • CWaveFileクラスの不要なメンバ、メソッドは削除しています。
  • WAVファイルは各自で用意してください。