ホーム < ゲームつくろー! < Ogg Vorbis入門編
その5 メモリにあるOggファイルを再生する
ファイルを直接読みながら再生する仕組み及びその仕組みを使ったプレイヤークラスを前章までで作成してきました。この章ではメモリ上にあるOggファイルを再生する仕組みについて見ていく事にします。
@ ov_open_callbacks関数がポイント
前章まで見てきたov_fopen関数は、最終的にOggVorbis_File構造体を作ってくれました。これができれば再生までの道は全く一緒です。つまり、この構造体がメモリ上にあるOggファイルから作れればお話は終わりです。
Ogg Vorbisはオープンソースなのでソースの中身を全部見ることができます。もちろんov_fopen関数もです:
ov_fopen関数の中身(libvorbis/lib/vorbisfile.c) int ov_fopen(char *path,OggVorbis_File *vf) {
int ret;
FILE *f = fopen(path,"rb");
if(!f)
return -1;
ret = ov_open(f,vf,NULL,0);
if(ret) fclose(f);
return ret;
}
これを見ると、何をしているかと思えばfopenでファイルを開き、得たファイルハンドルをov_open関数に渡しているだけです。ov_open関数はこうなっています:
ov_open関数の中身(libvorbis/lib/vorbisfile.c) int ov_open(FILE *f,OggVorbis_File *vf,char *initial,long ibytes) {
ov_callbacks callbacks = {
(size_t (*)(void *, size_t, size_t, void *)) fread,
(int (*)(void *, ogg_int64_t, int)) _fseek64_wrap,
(int (*)(void *)) fclose,
(long (*)(void *)) ftell
};
return ov_open_callbacks((void *)f, vf, initial, ibytes, callbacks);
}
短い中にポイントが凝縮しています。全体では「ov_open_callbacks関数」を呼ぶが目的になっています。ov_fopen内で呼ばれていたov_openの第3及び第4引数は0(NULL)です。それより、ov_open_callbacksに渡しているのは「f(ファイルハンドル)」「vf(OggVorbis_file構造体)」そして「callbacks(ov_callbacks構造体)」です。
ov_callbacks構造体(libvorbis/include/vorbis/vorbisfile.h) typedef struct {
size_t (*read_func) (void *ptr, size_t size, size_t nmemb, void *datasource);
int (*seek_func) (void *datasource, ogg_int64_t offset, int whence);
int (*close_func) (void *datasource);
long (*tell_func) (void *datasource);
} ov_callbacks;
何だかとてつもなくごちゃごちゃしていますが、これは4つの「関数ポインタ」が定義された構造体です。
実はov_open_callbacks関数はマニュアルでちゃんとその使用方法が定義されています。この関数の定義は次のようになっています:
ov_open_callbacks関数の定義 int ov_open_callbacks(
void* datasource,
OggVorbis_File* vf,
char* initial,
long ibytes,
ov_callbacks callbacks
);
datasourceは「データ構造」へのポインタとなっています。ここがファイルハンドル(FILE)になっていないのがポイントです。
vfには作成されたOggファイル構造体が返ります。空っぽを渡せば何かが返ります。
initialはNULLセットが通常との事でそうしましょう。
ibitesはinitialのサイズですが、これも0で構いません。
callbacksは「カスタムファイル操作ルーチンが完全に入ったov_callbacks構造体」とあります。
簡単に言えば、Ogg Vorbisライブラリはov_callbacks構造体にあるコールバック関数を用いてファイルを開いたり読み込んだりシークしたり閉じたりしてデータを読み取っています。そして、マニュアルにも記載されているように、このコールバック関数は「ユーザが定義できる」んです。
A ov_callbacks構造体のコールバックをカスタマイズ
ov_callbacks構造体に定義されている4つのコールバック関数(read_func、seek_func、close_funcそしてtell_func)をオリジナルに作成してov_open_callbacks関数に渡すと、メモリにあるOggファイルを扱うことができるようになります。
とは言っても、これらのコールバック関数はいったい何のこっちゃいとなるわけです。そう言えば、つい先ほど見たov_open関数内でOggライブラリが持っているこれらのデフォルト定義関数が登録されていましたので、それを見てみます:
ov_open関数の中身(libvorbis/lib/vorbisfile.c) int ov_open(FILE *f,OggVorbis_File *vf,char *initial,long ibytes) {
ov_callbacks callbacks = {
(size_t (*)(void *, size_t, size_t, void *)) fread,
(int (*)(void *, ogg_int64_t, int)) _fseek64_wrap,
(int (*)(void *)) fclose,
(long (*)(void *)) ftell
};
return ov_open_callbacks((void *)f, vf, initial, ibytes, callbacks);
}
このfread、fcloseそしてftellというのはOggの関数ではありません。これはstdio.hに定義されている純粋なC言語の関数です。そして、_fseek64_wrap関数の中身は、
_fseek64_wrap関数(libvorbis/lib/vorbisfile.c) static int _ov_header_fseek_wrap( FILE *f,ogg_int64_t off,int whence ) {
if ( f == NULL )
return( -1 );
return fseek( f, off, whence );
}
と大変短くなっていて、なんだ、なんてことは無い単なるfseek関数(これもstdio.hにあるC言語の関数)のラップ関数だったというわけです。つまり、Oggのファイルオープンやファイル解析は、C言語のファイル操作関数を駆使していただけなんです。よって、それらのファイル操作関数と同じ振る舞いをする独自の関数を作ればOKです。
まずは各ファイル操作関数の振る舞いを軽くおさらいしておきます。
○ fread関数
fread関数の定義 size_t fread(
void* buffer,
size_t size,
size_t count,
FILE* stream
);
fread関数はstreamというファイルハンドルからsizeバイトを単位として最大count回データを読み取りbufferにコピーします。戻り値は実際に完全に読み取った「回数」です。読み込みが成功したらファイルポインタを読み込んだ分だけ進ませます。
これとそっくりの動作をメモリに展開したOggファイルに対して行ってあげれば良いわけです。
○ fseek関数
fseek関数の定義 int fseek(
FILE* stream,
long offset,
int origin
);
fseek関数はstreamファイルハンドルに対してある基点(origin)の位置からoffsetだけファイルポインタを移動させます。基点には次の3つのフラグがあります:
・ SEEK_CUR 現在の位置
・ SEEK_END 一番後ろ
・ SEEK_SET 先頭
originにはこのいずれかが来るので、それに対応してメモリ(バッファ)のポインタを移動すればよいだけです。正しくポインタが移動できたら0を、変な位置になったら-1を返します。
○ fclose関数
fcloase関数の定義 int fclose(
FILE *stream
);
fclose関数は非常に単純で、streamをきれいさっぱり無くせば良いだけです。メモリで言えばもちろんdeleteです。正常終了は0、不正の場合はEOFを返します。
○ ftell関数
fcloase関数の定義 long ftell(
FILE *stream
);
ftell関数もとても単純な関数で、streamの現在のファイルポインタ位置を返します。メモリで言えば現在のポインタ位置です。どえらく簡単です。不正な場合は-1を返す決まりになっています。
という事で、これら4つの関数を真似するコールバック関数を次に作るのですが、今回は前章で構築したOggDecoderクラスのインターフェイスを踏襲しようと思います。そうすればPCMPlayerに渡してすぐに再生ができます。
B OggDecoderInMemoryクラス
前章でOggDecodreというOggファイルを開いてプレイヤーにデータを渡すクラスを作りました。OggDecoderの仕事は内部でOggVorbis_File構造体を作ってしかるべき情報を揃えるだけです。この解析はファイル上だろうとメモリ上だろうと初期化の段階で終わってしまいますので、その部分だけが異なるクラスを作れば終わりです。そこでOggDecoderクラスを継承したOggDecodeInMemoryクラスを新設します。
先の4つの関数ポインタはOggDecoderInMemoryクラスの静的メンバ関数で代用します。検討した結果インターフェイスは次のようになりました:
OggDecoderInMemoryクラス(OggDecoderInMemory.h) #ifndef IKD_DIX_OGGDECODERINMEMORY_H
#define IKD_DIX_OGGDECODERINMEMORY_H
#include "OggDecoder.h"
namespace Dix {
class OggDecoderInMemory : public OggDecoder {
public:
OggDecoderInMemory();
OggDecoderInMemory( const char* filePath );
virtual ~OggDecoderInMemory();
//! クリア
virtual void clear();
//! サウンドをセット
virtual bool setSound( const char* fileName );
protected:
//! ストリームのバッファからオブジェクトポインタを取得
static OggDecoderInMemory* getMyPtr( void* stream );
//! メモリ読み込み
static size_t read( void* buffer, size_t size, size_t maxCount, void* stream );
//! メモリシーク
static int seek( void* buffer, ogg_int64_t offset, int flag );
//! メモリクローズ
static int close( void* buffer );
//! メモリ位置通達
static long tell( void* buffer );
protected:
char* buffer_; // Oggファイルバッファ
int size_; // バッファサイズ
long curPos_; // 現在の位置
};
}
#endif
protected宣言された静的メソッドが5つあります。「え、5つ?」ですね。getMyPtrメソッドが余分です。でも、これが今回の実装のポイントです。他の静的メンバは先に説明しましたov_callbacks構造体に登録する関数になります。
メンバ変数を次に見て下さい。ここにはbuffer_(Oggファイルが展開されるバッファ)とsize_(バッファサイズ)そして現在のシーク位置を表すcurPos_が定義されています。curPos_は静的メソッドによってぐりぐりと動かされますし、buffer_はもちろんそのメソッド内で参照できなければなりません。
どうして赤文字でわざわざ強調しているのか?良く考えてみて下さい。静的なメソッドの中でローカルな変数は参照できません。つまり、seekメソッドの中でcurPos_を参照するとコンパイルエラーになってしまうんです。かと言ってメンバ変数をstaticにはできません。そんな事をしたらOggを1つしか再生できなくなります。
でもそうなると困りました。seekメソッドにどうやってローカルのcurPos_を渡せば良いのでしょうか?そこで考えました。buffer_はすべての静的メソッドに渡ります。この中にOggDecoderInMemoryオブジェクトのポインタを渡してしまいます。そして、各静的メソッド内でそれを復帰させればローカル情報を操作できます!実はgetMyPtrメソッドはbuffer_内からそのポインタを抽出するためのメソッドなんです。
buffer_の内部フォーマットを次のように定義します:
項目 OggDecoderInMemoryオブジェクトポインタ Oggファイル サイズ(byte) 4 n 内容例 0x00424f5a ---
つまり、先頭4バイトにポインタを格納して、その後にOggファイルを続けます。ここから元のポインタを取り出すgetMyPtrメソッドは次のようになります:
getMyPtrメソッド //! ストリームのバッファからオブジェクトポインタを取得
OggDecoderInMemory* OggDecoderInMemory::getMyPtr( void* stream ) {
OggDecoderInMemory *p = 0;
memcpy( &p, stream, sizeof( OggDecoderInMemory* ) );
return p;
}
memcpyによる代入が楽です。各静的メソッドはgetMyPtrメソッドを最初に呼び出してローカルへのアクセスを確保します。
○ readメソッド
最初に一番メンドクサイ所から片付けます。readメソッドはOggバッファから指定サイズだけコピーするメソッドです。先に実装をご覧下さい:
readメソッド //! メモリ読み込み
size_t OggDecoderInMemory::read( void* buffer, size_t size, size_t maxCount, void* stream ) {
if ( buffer == 0 ) {
return 0;
}
// ストリームからオブジェクトのポインタを取得
OggDecoderInMemory *p = getMyPtr( stream );
// 取得可能カウント数を算出
int resSize = p->size_ - p->curPos_;
size_t count = resSize / size;
if ( count > maxCount ) {
count = maxCount;
}
memcpy( buffer, (char*)stream + sizeof( OggDecoderInMemory* ) + p->curPos_, size * count );
// ポインタ位置を移動
p->curPos_ += size * count;
return count;
};
引数のsizeには単位サイズが、maxCountには最大取得回数が入ってきます。ローカルな情報には現在のポインタ位置(シーク位置)とOggファイルのサイズが入っています。ここから最初に実際に取得できる回数(count)を算出します。上の太文字のように残りの読み込みサイズを単位であるsizeで割れば回数が出ますね。これがmaxCountより大きければ指定サイズを全部読めますのでcountをmaxCountにしています。
memcpyの所がポイントです。streamにはローカルのバッファが入ってきますが、これは「先頭から4バイトにポインタ情報があるバッファ」です。実際にコピーしたいのはOggファイルの情報ですから、4バイト分オフセットする必要があります。sizeofでポインタ分足しているのはそういう理由です。
コピー後にポインタ位置を更新して、このメソッドは終わります。
○ seekメソッド
seekメソッドの役目はポインタ位置を指定の位置に移動させる事です。これはcurPos_変数を操作するだけなので結構簡単です:
seekメソッド //! メモリシーク
int OggDecoderInMemory::seek( void* buffer, ogg_int64_t offset, int flag ) {
// ストリームからオブジェクトのポインタを取得
OggDecoderInMemory *p = getMyPtr( buffer );
switch( flag ) {
case SEEK_CUR:
p->curPos_ += offset;
break;
case SEEK_END:
p->curPos_ = p->size_ + offset;
break;
case SEEK_SET:
p->curPos_ = offset;
break;
default:
return -1;
}
if ( p->curPos_ > p->size_ ) {
p->curPos_ = p->size_;
return -1;
} else if ( p->curPos_ < 0 ) {
p->curPos_ = 0;
return -1;
}
return 0;
}
flagにはstdio.hに定義されているSEEK_CURなどの基点位置を指定するフラグが入ってきます。それを判断しているだけです。SEEK_CURは現在位置からオフセットさせるので足し算、SEEK_SETは最初からなのでオフセット位置を代入するだけ。ちょっと気をつけたいのがSEET_ENDで、ポインタ位置の基点は「バッファの最後尾の次」になります。バッファが10バイトあったとしたら、buffer_[9]の位置ではなくてbuffer_[10]の位置です。SEEK_ENDは最後尾から指定バイトだけ戻すという使い方をする前提になっているため、ちょっとドキッとする位置に合わせる必要があります。
残りの処理は特に問題ありません。バッファオーバーランしている場合にエラーを返しているだけです。
○ tellメソッド、closeメソッド
tellメソッドは現在のポインタ位置を返します。そしてcloseメソッドはバッファを削除します。双方の実装はこちら:
tellメソッド、closeメソッド //! メモリ位置通達
long OggDecoderInMemory::tell( void* buffer ) {
return getMyPtr( buffer )->curPos_;
}
//! メモリクローズ
int OggDecoderInMemory::close( void* buffer ) {
return 0;
}
tellメソッドは問題ありませんね。注目はcloseメソッドです。この中身、何もしていません。どうしてか?バッファの本体はOggDecoderInMemoryオブジェクトが所有しています。と言うことは、わざわざOgg側で消さなくとも、オブジェクトが無くなればバッファもきれいさっぱり無くなるんです。よってこのメソッドでバッファを削除する必要はありません(むしろ、削除したらバグになります)。
これで4関数が全部揃いました。最後にsetSoundメソッド(ファイルを指定してOggVorbis_File構造体を作るメソッド)は次の通りです:
setSoundメソッド //! サウンドをセット
bool OggDecoderInMemory::setSound( const char* fileName ) {
clear();
// ファイルをバッファにコピー
FILE *f = fopen( fileName, "rb" );
if ( f == 0 ) {
return false; // オープン失敗
}
fseek( f, 0, SEEK_END );
size_ = ftell( f );
fseek( f, 0, SEEK_SET );
buffer_ = new char[ size_ + sizeof( OggDecoderInMemory* ) ];
OggDecoderInMemory* p = this;
memcpy( buffer_, &p, sizeof( OggDecoderInMemory* ) );
size_t readSize = fread( buffer_ + sizeof( OggDecoderInMemory* ), size_, 1, f );
if ( readSize != 1 ) {
// 何か変です
clear();
return false;
}
// コールバック登録
ov_callbacks callbacks = {
&OggDecoderInMemory::read,
&OggDecoderInMemory::seek,
&OggDecoderInMemory::close,
&OggDecoderInMemory::tell
};
// Oggオープン
if ( ov_open_callbacks( buffer_, &ovf_ , 0, 0, callbacks ) != 0 ) {
clear();
return false;
}
setFilename( fileName );
// Oggから基本情報を格納
vorbis_info *info = ov_info( &ovf_, -1 );
setChannelNum( info->channels );
setBitRate( 16 );
setSamplingRate( info->rate );
setReady( true );
return true;
}
引数のファイルをバッファ(buffer_)にコピーするのですが、最初の4バイトにオブジェクトへのポインタを格納しています。後はその後ろにOggファイルをそのまま展開します。続いてコールバック関数を登録するためov_callbacks構造体を作成しています。そしてov_open_callbakcs関数にそれを渡してエラーが返ってこなければ、めでたくOggVorbis_File構造体が出来上がりです。
この章では、メモリ上にあるOggファイルを読み込んでOggVorbis_File構造体をライブラリに作ってもらう方法を見てきました。実際上のOggDecoderInMemoryオブジェクトをPCMPlayerクラスに渡すと音が鳴ります(サンプルプログラム参照)。もちろん、同じようなコールバック関数を作れば、皆さん独自のクラスでも同様の事ができます。ただ、今回実際に作ってみて前々から思っていた不満がくすぶってきました。上のクラスは1つのオンメモリOggファイルを1回鳴らすのはできますが、「1つのオンメモリOggファイルを複数回同時に鳴らす」という事ができません。元々BGMを鳴らす目的で作ったためそういう仕様になったわけですが、ゲームで使う時に効果音などが重なる場面があるわけです。その時にまさか「効果音の数だけメモリに展開する」わけにはいきません。
そこで、次の章ではここまで作ってきたクラスの設計をちょっと変更・拡充して、1つのOggファイルを複数回同時に鳴らせるようにしてみます。