ホーム < ゲームつくろー! < Ogg Vorbis入門編
その6 OggDecoderオブジェクトを複製して同時に鳴らす
前章までで、Oggファイルのストリーミング再生、またメモリに展開したOggファイルの再生ができるようになりました。そこでじわっと出てきた不満が「1つのOggファイルを複数同時に鳴らせない」という問題です。
その4で作成したOggDecoderクラスの内部には1つのOggVorbis_File構造体が保持されています。この構造体にOggファイルのファイルハンドルやファイルポインタが格納されてしまっています。こうなると、1つのOggVorbis_File構造体に対して複数の人がアクセスするとファイルポインタの位置がめちゃくちゃになってしまいます。また、OggVorbis_File構造体をov_close関数で削除すると、ファイルハンドルそのものがクローズしてしまいます。それを知らずに他の人がアクセスしたら、あっという間にクラッシュです。
どうやら1つのOggファイルに対して複数同時再生を実現するには、1つのOggファイルに対する複数のOggVorbis_File構造体を作らなければならなさそうです。この章ではこの前提の下に、1つのOggファイルを複数同時に鳴らすための工夫を試行錯誤してみたいと思います。
@ Oggのクローン
Oggファイルのデータは前章まで作成してきたOggDecoderクラスが持っています。このクラスのsetSoundメソッドにOggファイル名を渡すと内部でOggVorbis_File構造体が1つ作られます。そして、このクラスのオブジェクトをPCMPlayerクラスに渡すとさくっと再生してくれます。
OggDecoderオブジェクトのハードコピーができれば事は簡単なのですが、冒頭でも述べました通りOggVorbis_File構造体内にはファイルポインタ(ファイルハンドル)があるため、OggVorbis_File構造体を単純にmemcpyしてもうまくいきません。
そこで、親クラスであるPCMDecoderクラスにcloneメソッドを追加して、この中で真面目にOggVorbis_File構造体を複製する事にします。
PCMDecoder::cloneメソッドは、PCMDecoderクラスの派生オブジェクトの情報を丸々コピーした新しいオブジェクトを生成して返してくれるメソッドにしましょう。こうすれば、1つのオブジェクトから複製を作ってプレイヤーに渡し、同時再生することが可能になります。メソッドイメージはこんな感じです:
PCMDecoder::cloneメソッド(PCMDecoder.h) sp< PCMDecoder > PCMDecoder::clone() = 0;
複製方法は親クラスではわかりませんので子クラス(OggDecoderクラス及びOggDecoderInMemoryクラス)に一任します。では、まずOggDecoderクラスのcloneメソッドの実装です。
A OggDecoder::Cloneメソッド
OggDecoderクラスはファイルから直接OggVorbis_Fileオブジェクトを生成しています。これを複製するにはそのファイルをさらにオープンして別のファイルハンドルを取得し、OggVorbis_File構造体にその情報を格納するしかなさそうです。逆に言えばそれだけですから、簡単と言えば簡単ですね:
OggDecoder::cloneメソッド(OggDecoder.cpp) //! 複製
sp< PCMDecoder > OggDecoder::clone() {
if ( isReady() == false ) {
// 何も設定されていない!
return 0;
}
sp< OggDecoder > spCloneObj( new OggDecoder( getFileName() ) );
return spCloneObj;
}
ちなみに、○×謹製スマートポインタはアップキャストをサポートしていますので、戻り値が親クラスならば直接代入できます。
B こっちが大問題!OggDecoderInMemory::Cloneメソッド
大変なのはこちらでした。OggDecoderInMemoryクラスはメモリに展開されたOggファイルを再生するクラスです。このクラスについて詳しくは前章の「メモリにあるOggファイルを再生する」をご覧下さい。
何が大変かと言うと、Oggライブラリに渡していたバッファの使いまわしができない事です。OggDecoderInMemoryクラスは、静的なコールバック関数で自身を操作してもらうために、次のようなポインタ入りのバッファを作っていました:
項目 OggDecoderInMemoryオブジェクトポインタ Oggファイル サイズ(byte) 4 n 内容例 0x00424f5a ---
0x00420f5aというのがオブジェクトポインタの例です。バッファの先頭にこの4バイトがくっついていて、各コールバック関数内で先頭4バイトに格納されているポインタをキャストすることで、クラスのメンバ変数にアクセスできていたのでした。
しかし、これが「複製」となると大問題をはらみます。複製元のバッファは参照して使いたいわけです。しかしながら、参照するバッファの先頭4バイトにはすでにコピー元のローカルポインタがしっかり陣取っています。このため、静的なコールバック関数内からは常に複製元しか見えないんです。かと言って、コールバックに情報を伝えるにはこの方法しかありません。
これはしばらく悩みました。悩んだ挙句、巧妙に解決する策を見つけました。
良く考えてみれば、「静的なコールバック関数内からローカルクラスにさえアクセスできれば何でもできる」んです。ということは、大胆にも上のバッファからOggファイル部分をごっそりと削り取ってしまい、オブジェクトポインタのみをライブラリに伝え、ポインタを通して間接的にメモリ展開したOggバッファにアクセスすれば、すべてが解決します!
図で描いても微妙に分かりにくい部分ではありますが、上の方が問題のある仕様、下の方がOggライブラリにポインタだけを渡すという大胆な仕様です。下の方はポインタのみがコールバック関数に渡っていて、OggDataもcurPos_もそのポインタを通して処理できています。こうすれば、OggDecoderInMemoryオブジェクトを複製した時にもPointerを自分のにして渡せばいいだけです。
実際のコードはこんな感じになります:
OggDecoderInMemory::cloneメソッド(OggDecoderInMemory.cpp) //! 複製
sp< PCMDecoder > OggDecoderInMemory::clone() {
// バッファは共通参照可能
// 他の変数はハードコピー
if ( isReady() == false ) {
return 0;
}
OggDecoderInMemory* cloneObj = new OggDecoderInMemory();
// ハードコピー
*cloneObj = *this;
cloneObj->curPos_ = 0;
memset( &cloneObj->ovf_, 0, sizeof( cloneObj->ovf_ ) );
/* OggVorbis_Fileを作り直す */
// コールバック登録
ov_callbacks callbacks = {
&OggDecoderInMemory::read,
&OggDecoderInMemory::seek,
&OggDecoderInMemory::close,
&OggDecoderInMemory::tell
};
// Oggオープン
if ( ov_open_callbacks( cloneObj, &cloneObj->ovf_ , 0, 0, callbacks ) != 0 ) {
cloneObj->clear();
return false;
}
cloneObj->setFilename( getFileName() );
cloneObj->setReady( true );
sp< OggDecoderInMemory > spCloneObj( cloneObj );
return spCloneObj;
}
ハードコピーというコメントの所で変数を一時全部コピーします。ただそのままだとOggVorbis_File構造体やシーク位置なども複製してしまうので、それらは再度初期化します。続いて、ov_open_callbacks関数の第1引数に大胆にもオブジェクトポインタのみを渡します(赤文字)。
後はコールバック関数側の処理を変えます。前回まではバッファにポインタ+Oggデータが付いてきましたが、新しい設計ではポインタしか入ってきません。そこで、ポインタを通してOggバッファにアクセスするようコールバックの中身を改良します。readメソッドだけ示しますとこういう感じに変わります:
OggDecoderInMemory::cloneメソッド(OggDecoderInMemory.cpp) //! メモリ読み込み
size_t OggDecoderInMemory::read( void* buffer, size_t size, size_t maxCount, void* stream ) {
if ( buffer == 0 ) {
return 0;
}
// ストリームをオブジェクトのポインタに大胆にも変換
OggDecoderInMemory *p = (OggDecoderInMemory*)stream;
// 取得可能カウント数を算出
int resSize = p->size_ - p->curPos_;
size_t count = resSize / size;
if ( count > maxCount ) {
count = maxCount;
}
memcpy( buffer, p->buffer_ + p->curPos_, size * count );
// ポインタ位置を移動
p->curPos_ += size * count;
return count;
};
赤文字の部分でポインタからOggバッファを引っ張ってきています。実際バッファを直に触るのはここだけです。修正が楽で助かりました。
さて、これでうまくいく・・・というのは甘い考えでした(この段階で実行して「んが!」っと固まりました(^-^;)。そう言えばもう1つ「終了処理」が残っていました。
OggDecoderInMemoryクラスは自分がいなくなる際にOggバッファをデストラクタで削除します。しかし、複製した段階で参照するバッファは1つ、それを削除する人は複数人です。そうです、一人目がバッファをさっくりと消した後に二人目が同じバッファを削除しようとしたら、もうバッファ先には何も無いのでメモリ保護違反で止まってしまうわけです。
解決は幸運にも極めて簡単です。共有するメモリの削除問題を解決する・・・これはもうスマートポインタのお家芸です。しかも大変嬉しい事に、○×謹製スマートポイントは配列に対応しておりました!(この前拡張しておいて良かった・・・)。裸で持っていたOggデータを格納するバッファをスマートポインタでくるみます。こうすればハードコピーした際に内部で参照カウンタが立つので、最後の一人が責任を持って配列削除してくれます。ありがとう、スマートポインタ!
OggDecoderInMemory::cloneメソッド(OggDecoderInMemory.h) class OggDecoderInMemory : public OggDecoder {
protected:
sp< char > spBuffer_; // Oggファイルバッファ
int size_; // バッファサイズ
long curPos_; // 現在の位置
...
};
という事で、OggDecoderInMemoryオブジェクトのクローンも無事に解決し、ここに1つのバッファを共有してデコーダオブジェクトをガンガン複製する仕組みが整いました。新しいバージョンのクラス群、及び典型的な使い方はサンプルプログラムに公開致します。
これができると、例えばSTGで爆発音をクローンで複製して同時に鳴らしたり、予めクローンを沢山用意しておいて各キャラクタに持たせてそれぞれが勝手に音を再生するようにしたりと、音再生に対する制約がぐっと減ります。BGMも音も、同じ仕組みで簡単にならせるようになってきた気がします(^-^)