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

その4 Ogg Vorbisプレイヤークラス


 前章までで、Ogg Vorbisファイルを読み込んで再生する、またストリーム再生する仕組みを見てきました。ただ、テストプログラムとして実装してきたので、このままでは再利用性がありません。そこで本章では前章までの基礎知識を踏まえて、Ogg Vorbisファイルを読み込んで再生できるプレイヤークラスを作ってみることにします。これがあれば、ゲーム中の任意のタイミングでBGMを鳴らせます!

 尚、この章で作成するOgg Vorbis再生クラスの完全版はサンプルプログラム及びツール編に公開致します。以下はそのクラス作成の概略の説明になっています。



@ ポーリング型かコールバック型か…

 この手のクラスを作る時に「ポーリング型」か「コールバック型」のどちらにするか迷います。ポーリング型というのは、例えばupdateメソッドを設けて毎回クラスの状態をチェックするタイプです。今再生しているのか、どこまで進んでいるのかはupdateで更新した時にわかります。一方のコールバック型というのは、クラスの内部で何かが勝手に進んでいて、あるタイミングが来た時に特定のメソッドが呼ばれるタイプです。毎回クラスの状態を監視して更新する必要がありませんが、自動的に何かが動いている分制御が難しくなります。

 BGMはゲームのあるシーンが始まった時とか、特定の場面が来た時に流れます。それは部屋の中でプレイヤーの再生ボタンを押したらスピーカーから音が勝手に流れ続けるような状態です。そういう状態を考えると、毎回更新するポーリング型はちょっと気にし過ぎな感じがします。コールバック型ならば曲が終わった時とか特定のポジションに来た時だけ気にすれば良いので気楽かもしれません。よって今回は、ちょっと面倒はありますが、コールバック型のプレイヤーを作ってみることにします。



A PCM作成クラスとPCM再生クラスとの分離

 ここまでOgg Vorbisライブラリを扱ってきてわかったのが、BGMの再生には「何らかのフォーマット形式の音声ファイルからPCMを抽出する」という作業と「抽出されたPCM音声を再生する」という作業がある事です。この2つの作業はかなりぱっきりと分離できます。こういうのはクラスも分離しておいた方が柔軟です。

 そこで、今回はあるフォーマット形式の音声データをPCM音声にデコードするクラスであるPCMDecoderクラスと、そこからPCM音声をもらって再生するPCMPlayerクラスの2つを作りたいと思います。PCMPlayerクラスはPCMDecorderクラスを良く知っていますが、PCMDecorderクラスはPCMPlayerクラスの事は知りません。

 まずはPCMDecoderクラスから作ります。



B PCMDecoderクラス

 PCMDecorderクラスは特定の音楽フォーマットデータをPCM音声に変換して提供するクラスです。Ogg Vorvisを扱うデコーダクラス(OggDecoderクラス)はこの基本クラスから派生されることになります。

 このクラスがすべき一番の仕事は、外部のクラス(PCMPlayerクラス)が「音声を頂戴」と命じた時にそれを渡す事です。この実装はすでにその3の「指定の大きさのPCMバッファを埋めてもらう」にあります。クラスのメンバを用いるように少し改良が必要ですが、外部が用意したバッファ分のPCM音声を埋めるPCMDecoder::getSegmentメソッドの実装はこういう感じになります:

PCMDecoder::getSegmentメソッド
//! セグメント取得
bool OggDecoder::getSegment( char* buffer, unsigned int size, unsigned int* writeSize, bool* isEnd ) {
   if ( isReady() == false ) {
      return false;
   }

   if ( buffer == 0 ) {
      if ( isEnd ) *isEnd = true;
      if ( writeSize ) *writeSize = 0;
      return false;
   }

   if ( isEnd ) *isEnd = false;
   memset( buffer, 0, size );

   unsigned int requestSize = requestSize_g;   // 読み込み基本単位
   int bitstream = 0;
   int readSize = 0;
   unsigned int comSize = 0;
   bool isAdjust = false;           // 調整段階中?

   if ( size < requestSize ) {
      requestSize = size;
      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;
            if ( writeSize ) *writeSize = comSize;
            return true;
         }
      }

      comSize += readSize;

      _ASSERT( comSize <= size ); // バッファオーバー

      if ( comSize >= size ) {
      // バッファを埋め尽くしたので終了
      if ( writeSize ) *writeSize = comSize;
         return true;
      }

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

   if ( writeSize ) *writeSize = 0;
   return false; // 良くわからないエラー
}

 細かく色々なのですが、肝はwhile分のov_read関数です。これはOgg Vorbisライブラリが提供している関数で、現在オープンしているOgg Vorbisファイルから指定のサイズ(requestSize)分だけ音声をPCMに変換し格納してくれます。ただ、要求した大きさと実際に書き込まれる大きさは食い違います。関数の戻り値に実際に書き込まれた量が返りますので、それを見てどこまでバッファを埋めたかをチェックしています。

 Ogg Vorbisライブラリがデコード部分を全部やってくれますので、OggDecoderクラスの実装はかなりシンプルになります。完全な実装はサンプルプログラムにありますのでご覧下さい。

 本番はプレイヤーの方にありますので、そちらを中心に見ていきます。



C PCMPlayerクラス

 プレイヤークラス(PCMPlayerクラス)はいわゆる音楽プレイヤーのような操作で音声を再生するクラスです。PCMデコーダークラスのオブジェクトを受け取って再生(play)、一時停止(pause)そして停止(stop)を行います。このクラスの肝は沢山あって細かく説明しきれないのですが、一番のポイントはストリーム再生を行っている最中に動くスレッドです。これにより、一度音声を再生したらupdate等の更新メソッドを呼ばなくても音声が流れ続けます。

 まず、PCMデコーダオブジェクトを受け取ったら、そこからサウンドバッファであるIDirectSoundBuffer8インターフェイスを作成します。そして、デコーダにそのバッファを埋めてもらい、ストリーム再生用のスレッドを動作させます:

PCMPlayer::setDecoderメソッド
//! PCMデコーダを設定
bool PCMPlayer::setDecoder( sp< PCMDecoder > pcmDecoder ) {
   if ( cpDS8_.GetPtr() == 0 || pcmDecoder.GetPtr() == 0 || pcmDecoder->isReady() == false ) {
   isReady_ = false;
      return false;
   }

   state_ = STATE_STOP;

   if ( !pcmDecoder->getWaveFormatEx( waveFormat_ ) ) {
      return false;
   }

   DSBufferDesc_.dwSize = sizeof( DSBUFFERDESC );
   DSBufferDesc_.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_CTRLFREQUENCY | DSBCAPS_GLOBALFOCUS;
   DSBufferDesc_.dwBufferBytes = waveFormat_.nAvgBytesPerSec * playTime_g;
   DSBufferDesc_.dwReserved = 0;
   DSBufferDesc_.lpwfxFormat = &waveFormat_;
   DSBufferDesc_.guid3DAlgorithm = GUID_NULL;

   spPCMDecoder_ = pcmDecoder;

   // セカンダリバッファがまだ無い場合は作成
   if ( cpDSBuffer_.GetPtr() == 0 ) {
      IDirectSoundBuffer* ptmpBuf = 0;
      if ( SUCCEEDED( cpDS8_->CreateSoundBuffer( &DSBufferDesc_, &ptmpBuf, NULL ) ) ) {
         ptmpBuf->QueryInterface( IID_IDirectSoundBuffer8 , (void**)cpDSBuffer_.ToCreator() );
      }
      else {
         clear();
         return false;
      }
      ptmpBuf->Release();
   }

   // バッファを初期化
   if ( initializeBuffer() == false ) {
      return false;
   }

   // バッファコピースレッド生成
   if ( threadHandle_ == 0 ) {
      threadHandle_ = (unsigned int)_beginthread( PCMPlayer::streamThread, 0, (void*)this );
   }


   isReady_ = true;

   return true;
}

 ソースの細かい部分は前章のテストプログラムで出てきています。セカンダリバッファを作成するまではそれと同じです。バッファサイズはちょうどplayTime_g分作成しています。

 作成したセカンダリバッファはまだ空っぽなので、これを渡されているPCMDecoderオブジェクトに初期化してもらいます。それを行っているのがinitializeBifferメソッドです:

PCMPlayer::initializeBufferメソッド
//! バッファを初期化する
bool PCMPlayer::initializeBuffer() {
   if ( spPCMDecoder_.GetPtr() == 0 ) {
      return false;
   }

   spPCMDecoder_->setHead(); // 頭出し
   cpDSBuffer_->SetCurrentPosition( 0 );

   // バッファをロックして初期データ書き込み
   void* AP1 = 0, *AP2 = 0;
   DWORD AB1 = 0, AB2 = 0;
   if ( SUCCEEDED( cpDSBuffer_->Lock( 0, 0, &AP1, &AB1, &AP2, &AB2, DSBLOCK_ENTIREBUFFER ) ) ) {
      spPCMDecoder_->getSegment( (char*)AP1, AB1, 0, 0 );
      cpDSBuffer_->Unlock( AP1, AB1, AP2, AB2 );
   }
   else {
      clear();
      return false;
   }

   return true;
}

既存のセカンダリバッファの全部をロックし、太文字部分でそのバッファに指定サイズ分だけバッファを埋めてもらいます。ここがプレイヤーとデコーダの接点です。これで最初の数秒間の音声の準備ができました。



D ストリーム再生スレッド

 バッファの初期化が終わったら、ストリーム再生を監視するスレッドを生成します。このスレッドが動いている間は音を鳴らす事が可能です。スレッドは再生を直接監視して、適切なタイミングでバッファを書き換える作業を行います。スレッドの生成部分は次の通りです:

PCMPlayer::setDecoderメソッド内スレッド生成部分
// バッファコピースレッド生成
threadHandle_ = (unsigned int)_beginthread( PCMPlayer::streamThread, 0, (void*)this );

 スレッド関数としてクラスメソッドのstreamThreadを充てます。もちろんstaticメソッドです。再生や停止の権限はPCMPlayerにあります。よってスレッドは常にPCMPlayerの状態を見る必要があります。そのため、スレッドにはPCMPlayer自身のポインタを与えます。

 streamThreadメソッドの実装はこのような感じです:

PCMPlayer::streamThreadメソッド
void PCMPlayer::streamThread( void* playerPtr ) {

   PCMPlayer* player = (PCMPlayer*)playerPtr;

   unsigned int size = player->DSBufferDesc_.dwBufferBytes / 2;
   unsigned int flag = 0;
   DWORD point = 0;
   void* AP1 = 0, *AP2 = 0;
   DWORD AB1 = 0, AB2 = 0;

   while( player->isTerminate_ == false ) {
      switch ( player->getState() ) {
      case STATE_PLAY: // 再生中
         // ストリーム再生
         // 現在位置をチェック
         player->cpDSBuffer_->GetCurrentPosition( &point, 0 );
         if ( flag == 0 && point >= size ) {
            // 前半に書き込み
            if ( SUCCEEDED( player->cpDSBuffer_->Lock( 0, size, &AP1, &AB1, &AP2, &AB2, 0 ) ) ) {
               player->spPCMDecoder_->getSegment( (char*)AP1, AB1, 0, 0 );
               player->cpDSBuffer_->Unlock( AP1, AB1, AP2, AB2 );
               flag = 1;
            }
         }
         else if ( flag == 1 && point < size ) {
         // 後半に書き込み
         if ( SUCCEEDED( player->cpDSBuffer_->Lock( size, size * 2, &AP1, &AB1, &AP2, &AB2, 0 ) ) ) {
            player->spPCMDecoder_->getSegment( (char*)AP1, AB1, 0, 0 );
            player->cpDSBuffer_->Unlock( AP1, AB1, AP2, AB2 );
            flag = 0;
         }
      }
      break;

      case STATE_STOP:
         flag = 0; // 止めると前半書き込みから始まるため
         break;

      case STATE_PAUSE:
         break;

      default:
         break;
      }

      Sleep( 100 );
   }
}

この実装の内容も、前章のテスト環境に実装したストリーム再生とほぼ同じです。スレッド自体は生成と同時に走り出します。そこで現在のPCMPlayerの状態を2段階で監視しています。最初にwhile文の判定にあるplayer->isTerminate_は、PCMPlayer自体が無くなる時にtureとなります。その場合while文から抜けてスレッドはすぐに終了します。スレッドが存在して良い状況で、次に「player->getState()」でPCLPlayerの状態を取得しています。判断状態は3つ「STATE_PLAY」「STATE_STOP」そして「STATE_PAUSE」です。

 再生中(STATE_PLAY)にはストリーム再生を行います。現在の再生位置をチェックして、前半部であれば後半部に新しい音声データを、後半部であれば前半に音声データを書き込んでいます。再生中はこれがひっきりなしに動き続けます。

 停止時(STATE_STOP)には再生中の曲を止め、曲の最初に戻ります。そのため、flagの値を「0」にしなければなりません。これは、停止した段階でセカンダリバッファが初期状態に戻されるためです(バッファを書き直すのはPCMPlayerの仕事)。次に再生が始まった時、再生位置は0になるため、flagも初期化しておかないとまずいわけです。

 一時停止(STATE_PAUSE)は一番簡単ですね。何もしなければいいんです。

 このループを延々と回しますが、この時Sleepを入れるのが大切です。ストリーム再生スレッドの役目は再生位置を見てバッファを書き換えるだけです。この作業頻度は非常に低く、書き換えていない時間は空回りしているだけです。もしSleepを入れないと、この空回りに物凄いCPUパワーが取られてしまいます。Sleepを入れるとその間CPUは他の部分にその力を回すことができます。上では0.1秒に1回何かするようにしていますが、これはセカンダリバッファのサイズによって調節できる部分です。



E 再生・一時停止・停止

 ここまで作ると、後はDirectSoundの機能を使うだけです。再生、一時停止そして停止部分の実装をずらっと見てみます:

再生・一時停止・停止部分の実装
//! 再生
bool PCMPlayer::play( bool isLoop ) {
   if ( isReady() == false ) {
      return false;
   }
   isLoop_ = isLoop;
   spPCMDecoder_->setLoop( isLoop );
   cpDSBuffer_->Play( 0, 0, DSBPLAY_LOOPING );
   state_ = STATE_PLAY;
   return true;
}

//! 一時停止
void PCMPlayer::pause() {
   if ( state_ == STATE_PLAY ) {
      // 動いていたら止める
      cpDSBuffer_->Stop();
      state_ = STATE_PAUSE;
   }
   else {
      // 止まっていたら再生
      play( isLoop_ );
   }
}

//! 停止
void PCMPlayer::stop() {
   if ( isReady() == false ) {
      return;
   }
   state_ = STATE_STOP;
   cpDSBuffer_->Stop();

   // バッファの頭出し
   initializeBuffer();
}

 再生(playメソッド)は再生可能状態ならIDirectSoundBuffer8::Playメソッドを実行します。ストリーム再生なのでもちろんループさせます。一時停止(pauseメソッド)メソッドもIDirectSoundBuffer8::Stopメソッドを呼ぶだけです。再開はplayメソッドを内部で呼んでいます。ちょっと面倒なのが停止(stopメソッド)です。このメソッドが呼ばれたら曲を停止させ、曲を最初から再生できるようにする必要があります。そこで、停止させた後にinitializeBufferメソッドを再度呼び出して再初期化をしています。

 後はボリュームやパン、そして同じオブジェクトを再利用して別の曲(デコードオブジェクト)を再生するための処理などが追加されています。この章のメインはストリーム再生のスレッド生成やPCMDecoderとPCMPlayerクラスの関係などにあると思いますので、その辺りの細かな部分は割愛致します。完全な実装はサンプルプログラム及びツール編にある双方のクラスにありますので、興味のある方はご覧下さい。