ホーム < ゲームつくろー! < Ogg Vorbis入門編

その3 Oggファイルでストリーム再生


 前章までで、Ogg VorbisファイルからPCM音声をデコードする方法を学びました。Vorbisfileを使うと驚くほど簡単に実装が出来ます。本章ではこの基本を踏まえて大きなサイズの音声をストリーム再生してみることにします。



@ Ogg Vorbisのストリーム再生方法

 ストリーム再生というのは、大きなサイズの音声ファイルを小さくちぎりながら読み込み、少ないメモリ領域だけで連続して再生し続ける再生方法の事です。例えばDVDはGBサイズの情報があります。これをすべてメモリに読み込んで再生することはかなり難しい・・・というより現行では無理です。でもDVDはパソコンで簡単に閲覧できます。これは再生すべき箇所をちょっとずつPCに読み込み、メモリに展開して再生するという作業を繰り返しているためです。Ogg Vorbisの再生もこれと同じ事をします。機構はどうしても複雑になってしまいますが、一度作ってしまえばもうどのような巨大なOggファイルでも小さなメモリサイズで再生できるようになります。

 典型的なストリーム再生は下図のような仕組みです:


 サウンドバッファは「ダブルバッファ」という方式をとります。これは実質は連続したメモリブロックなのですが、コピー領域として前後2つ用意されます。最初に音声ファイルの一部分をダブルバッファの両方にコピーします。再生すると再生位置がサウンドバッファの先へ進みだします。再生位置が後ろのバッファに入ったら、音声ファイルの続き部分を前のバッファにコピーします。再生部分はサウンドバッファの最後に到達すると自動的に最初に戻ります(ループ再生)。そしたら後ろのバッファに音声ファイルの一部をまたコピーします。この繰り返しにより少ないメモリで巨大な音声を再生し続けられるわけです。

 さて、Ogg Vorbisの場合ちょっと事情が変わります。Ogg Vorbisファイルからは「不定長の細切れなPCM音声」がバッファにコピーされます。つまり、上のように一度にある程度大きなサイズのバッファをコピーできないんです。そこで、細切れなバッファをどんどん積み重ね、ちょうどダブルバッファの片方分になるまでコピーし続けます:

 Oggは不定長のPCM音声を返しますが、最大取得サイズはこちらで指定できます。ですから、最後の帳尻合わせは可能です。また、ov_readを呼び続けると自動的にループしてくれるので、曲の最後になったら何か特別な事をする必要もありません。

 いきなり多くの事をすると混乱しますので、少しずつ機能を作りこんでいきましょう。



A 指定の大きさのPCMバッファを埋めてもらう

 まずは、あるOggファイルから指定の大きさだけPCMバッファを埋めてもらう関数を作ります。プロトタイプは次のような感じです:

指定の大きさのPCMバッファを埋める関数
unsigned int getPCMBuffer( OggVorbis_File *ovf, char* buffer, unsigned int bufferSize, bool isLoop );

ovfには抽出するOgg Vorvisを指定します。
bufferにはPCMデータをもらうバッファを渡します。
bufferSizeはbufferのバイトサイズです。
isLoopは音声をループで取得するかを指定します。falseにすると曲の最後以降のバッファ部分を0で埋めます。

 ov_read関数は最大読み込みサイズを与えて実際に読み込んだバッファサイズを得ることができます。指定のバッファサイズになるまではマックスに読んでもらい、最後の方の帳尻合わせで最大読み込みサイズを調整します。ぴったり指定のバッファサイズ分だけ読み込めたら関数を抜けるようにします:

指定の大きさのPCMバッファを埋める関数(実装)
// 指定サイズでPCM音声バッファを埋める関数
unsigned int getPCMBuffer( OggVorbis_File *ovf, char* buffer, int bufferSize, bool isLoop, bool* isEnd = 0 ) {
   if ( buffer == 0 ) {
      if ( isEnd ) *isEnd = true;
      return 0;
   }

   if ( isEnd ) *isEnd = false;

   memset( buffer, 0, bufferSize );
   int requestSize = 4096;
   int bitstream = 0;
   int readSize = 0;
   int comSize = 0;
   bool isAdjust = false;

   if ( bufferSize < requestSize ) {
      requestSize = bufferSize;
      isAdjust = true; // 調整段階
   }

   while( 1 ) {
      readSize = ov_read( ovf, (char*)( buffer + comSize ), requestSize, 0, 2, 1, &bitstream );
      if ( readSize == 0 ) {
         // ファイルエンドに達した
         if ( isLoop == true ) {
            // ループする場合読み込み位置を最初に戻す
            ov_time_seek( ovf, 0.0 );
         }
         else {
            // ループしない場合ファイルエンドに達したら終了
         if ( isEnd ) *isEnd = true;
         return comSize;
         }
      }

      comSize += readSize;
 
      if ( comSize >= bufferSize ) {
         // バッファを埋め尽くしたので終了
         return comSize;
      }

      if ( bufferSize - comSize < 4096 ) {
         isAdjust = true; // 調整段階
         requestSize = bufferSize - comSize;
      }
   }

   return 0; // 良くわからないエラー
}


頑張ったのですが少し長くなってしまいました。しかし、これでダブルバッファにPCM音声を書き込む部分はできました。続いて、ストリーム再生自体の仕組みの部分に移ります。



A DirectSoundによるストリーム再生

 ここからはDirectSoundのお話です。DirectX技術編に書いても良いのですが流れ上こちらに書きます。DirectSoundの初期化等についてはDirectX技術編DirectSoundの項をご覧下さい。ここではそれらの知識がある前提で話を進めます。

 DirectSoundでセカンダリバッファーを作成する時にバッファの性質とそのサイズを指定します。バッファの性質はOgg Vorbisファイルから取得できます(ov_info)。サイズはプログラマが適当に決めて良い部分です。今回は44100のサンプリングレートでビットレートは16、ステレオの音質を1秒間格納できるサイズを指定することにします。計算すると、44100 * 2byte * 2channel * 1sec. = 176400byte です。セカンダリバッファ作成部分は例えば次のような実装になります:

セカンダリバッファの作成
// DirectSoundの作成
IDirectSound8 *pDS8;
DirectSoundCreate8( NULL, &pDS8, NULL );
pDS8->SetCooperativeLevel( GetConsoleHwnd(), DSSCL_PRIORITY );

// セカンダリバッファ作成作業
int playTime = 1; // 1秒分

vorbis_info *info = ov_info( &ovf, -1 );

// WAVE情報
WAVEFORMATEX waveFormat;
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nChannels = info->channels;
waveFormat.nSamplesPerSec = 44100;
waveFormat.wBitsPerSample = 16;
waveFormat.nBlockAlign = info->channels * 16 / 8;
waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
waveFormat.cbSize = 0;

// DirectSoundBuffer情報
DSBUFFERDESC DSBufferDesc;
DSBufferDesc.dwSize = sizeof( DSBUFFERDESC );
DSBufferDesc.dwFlags = 0;
DSBufferDesc.dwBufferBytes = waveFormat.nAvgBytesPerSec * playTime;
DSBufferDesc.dwReserved = 0;
DSBufferDesc.lpwfxFormat = &waveFormat;
DSBufferDesc.guid3DAlgorithm = GUID_NULL;

// セカンダリバッファ作成
IDirectSoundBuffer *ptmpBuf = 0;
IDirectSoundBuffer8 *pDSBuffer = 0;
if ( SUCCEEDED( pDS8->CreateSoundBuffer( &DSBufferDesc, &ptmpBuf, NULL ) ) ) {
   ptmpBuf->QueryInterface( IID_IDirectSoundBuffer8 , (void**)&pDSBuffer );
}
ptmpBuf->Release();

 ちょっとややこしいのですが、ここはお決まりの部分がほとんどです。DirectSoundデバイスを作成し、セカンダリバッファの性質を長々と決めていきます。これらの構造体についてもDirectX技術編DirectSoundその1にありますのでご確認ください。確保した1秒分のセカンダリバッファをきっかり真ん中で分割すると考えます。

 セカンダリバッファにPCMデータをコピーするにはIDirectSoundBuffer8::Lockメソッドでメモリをロックします。再生する前であればロックはどの範囲でも有効なので、最初は1秒分(DSBufferDesc.swBufferBytes)書き込みを行います:

1秒分書き込み
void* AP1 = 0, *AP2 = 0;
DWORD AB1 = 0, AB2 = 0;
if ( SUCCEEDED( pDSBuffer->Lock( 0, 0, &AP1, &AB1, &AP2, &AB2, DSBLOCK_ENTIREBUFFER ) ) ) {
   // ロックしたバッファに初期PCM音声データ書き込み
   getPCMBuffer( &ovf, (char*)AP1, AB1, false );
   pDSBuffer->Unlock( AP1, AB1, AP2, AB2 );
}

これでIDirectSoundBuffer8::Playメソッドを実行すると再生が始まります:

再生スタート
pDSBuffer->Play( 0, 0, DSBPLAY_LOOPING );

第3引数がポイントです。ストリーム再生なので短いサウンドバッファをぐるぐるとループし続ける必要があるため、DSBPLAY_LOOPINGフラグを指定します。

 さて、ここからが本番です。再生している最中に再生位置をチェックし、ダブルバッファの片方を通り過ぎたら直ちにそこに続きとなるPCM音声を書き込みます。ここの実装は色々な書き方ができると思いますが、例えばこんな感じでもOKです:

スイッチングでストリーム再生
unsigned int size = DSBufferDesc.dwBufferBytes / 2;
unsigned int flag = 0;
DWORD point = 0;
while( 1 ) {
   Sleep( 16 );
   pDSBuffer->GetCurrentPosition( &point, 0 );
   if ( flag == 0 && point >= size ) {
      // 前半に書き込み
      if ( SUCCEEDED( pDSBuffer->Lock( 0, size, &AP1, &AB1, &AP2, &AB2, 0 ) ) ) {
         getPCMBuffer( &ovf, (char*)AP1, AB1, true );
         pDSBuffer->Unlock( AP1, AB1, AP2, AB2 );
         flag = 1;
      }
   }
   else if ( flag == 1 && point < size ) {
      // 後半に書き込み
      if ( SUCCEEDED( pDSBuffer->Lock( size, size * 2, &AP1, &AB1, &AP2, &AB2, 0 ) ) ) {
         getPCMBuffer( &ovf, (char*)AP1, AB1, true );
         pDSBuffer->Unlock( AP1, AB1, AP2, AB2 );
         flag = 0;
      }
   }

   // Escapeキーを押したら抜ける
   if ( GetAsyncKeyState( VK_ESCAPE ) )
   break;
}

 sizeにはサウンドバッファのちょうど半分のサイズが入っています。flagは「0」の時が前半、「1」の時が後半に書き込みをします。GetCurrectPositionメソッドは現在の再生位置(サンプリング位置)を返してくれます。これでどちらに書き込み可能かを判定できます。判定部分をみれば動きは明らかなのですが、前半書き込みフラグ(flag = 0)が立っていて、さらに再生位置が後半にある場合、前半の書き込みを行います。書き込みは0〜sizeまでの範囲です(Lockメソッドの第1引数と第2引数で範囲指定)。一度書き込んだら次は後半の書き込みなので、flagを「1」に切り替えます。後は後半の書き込みタイミングになるまで待機します。ちなみにSleepを入れているのは特に意味はありません。強いて言えばゲームのフレーム速度を模倣しているだけです。

 これで十分にストリーム再生ができてしまいます。私の環境で、上のSleep間隔だとCPUの付加は1%くらいでした。低スペックマシンでも十分に再生ができるはずです。

 Ogg Vorbisによるストリーム再生の基盤ができました。もちろんこれはテストプログラムなので、再利用性は考えていません。次の章ではいよいよOgg Vorbis形式のファイルを設定してPlayとしてあげるだけでストリーム再生をするクラスを作ります。

 尚、本章の完全なプログラムはサンプルプログラムに挙げましたので、興味のある方は実行してみて下さい。