長かった準備もようやく終わり、いよいよデコードする時がやって参りました。とは言え、デコードした後再生するまでにはまた別のコードが必要になりますが、まぁそれはここでは詳しくは触れません。
デコードは、ov_read、つまりOggVorbis.Functions.Decode
を呼び出すだけです。……と言うと簡単に聞こえますが、それなりに面倒です。何しろov_readは、Functions.Decode( this.oggVorbisFile, data, offset, decodedBytes, 0, 2, 1, ref bitStream )
とやってもdecodedBytesバイトを全部読んでくれることが無いからです(大抵4096バイト読んだら制御を返します)。この為に、OggVorbisDecoderクラスにprivateなDecode関数を用意して、Functions.Decodeをラップします。このラッパ関数は後で定義することとして、とりあえずは簡単なデコードをする関数を作ってみましょう。
public byte[] ReadEntireData() { int totalBytes = this.format.BlockAlign * this.samplesActivated; byte[] data = new byte[ totalBytes ]; GCHandle dataHandle = GCHandle.Alloc( data, GCHandleType.Pinned ); this.Position = 0; this.Decode( data, 0, totalBytes ); dataHandle.Free(); return data; }
データを全てデコードして、byte配列として返す関数です。PCMデータにデコードしたとき、それが全て収まるような配列を確保し、後で定義するthis.Decode関数に、配列dataの先頭からデータを全て読み込ませます。この関数には、あとでSystem.IO.Stream.Read
と似た処理をさせます。これのお陰でReadEntireData関数は簡単ですね。
奇妙なのは、配列dataのGCHandleを取得しているところです。しかも、dataHandle自体は使われていません。実は、これは第2引数のGCHandleType.Pinnedの方が大事で、これをつけてGCHandleを取得すると、マネージドオブジェクトが固定されるのです。こうすることで、プラットフォーム呼び出しでなくても、アドレスが一定になります(ガベージコレクタに再配置されなくなる)。この効果はDecodeの実装の時にもう一度説明しましょう。
では、先程から名前ばかり出ている、OggVorbis.OggVorbisDecoder.Decode
関数を定義します。
private int Decode( byte[] data, int offset, int count ) { int restBytes = count; int bytesToRead; int index = 0; int bitStream = 0; while ( restBytes != 0 ) { bytesToRead = Functions.Decode( this.oggVorbisFile, Marshal.UnsafeAddrOfPinnedArrayElement( data, offset + index ), restBytes, 0, 2, 1, ref bitStream ); if ( bytesToRead < 0 ) throw new InvalidOperationException(); if ( bytesToRead == 0 ) break; restBytes -= bytesToRead; index += bytesToRead; } return index; }
配列dataのoffsetバイトの位置から、countバイト読み取る関数です。Functions.Decodeには色々引数がありますが、気になる場合はov_readのリファレンスで対応しているところを見て下さい。
Functions.Deocdeの所以外は簡単です。まだ続けて読み取る必要がある場合はループを回し、実際に読み取ったバイト数を取得して、残りバイト数を減らしていきます。負の値になったらエラー、0の場合はストリームの終端なので、それぞれ対処します。
ここにも奇妙なコードがあります。即ち、Marshal.UnsafeAddrOfPinnedArrayElement( data, offset + index )
です。これは、配列dataのoffset + indexバイト目のアドレスを取得するものです。MSDN2ライブラリにあるこの関数のリファレンスには微妙な表現で書いてありますが、配列dataは予めGCHandleでpinしておかなければなりません。英語版にはしっかりThe array must be pinned using a GCHandle before it is passed to this method.
と書いてあります。先程書いたように、pinすればマネージドオブジェクトも固定されますから、UnsafeAddrOfPinnedArrayElementにdataを渡してからFunctions.Decodeにそのdataを渡すまでの間に、アドレスが変わってしまうことはなくなります。無論これは非常に短い時間ですが、pinしておかないと、絶対に再配置されないという保証が無いので危険です。
これで、先程のReadEntireData関数が動くようになります。単なるデコーダとして、メモリ残量を気にせずにデコードできる場合はこれでいいですが、実際にはなかなかそんな状況は無いですし、殊に、(忘れられているかもしれませんが、)当初の目的はMDXでOgg Vorbisをストリーミング再生することでした。こういう場合、一定の区切りをつけて、何バイトごとにデコード、と言うことが必要になってくるので、ReadEntireDataだけでは不十分です。そこで、OggVorbis.OggVorbisDecoder.Read
関数を定義します。
public int Read( byte[] data, int offset, int count, bool wrapingAround ) { int restBytes = count; int index = 0; int bytesToRead; int blockAlign = this.format.BlockAlign; if ( restBytes > this.dataLength * blockAlign ) bytesToRead = this.dataLength * blockAlign; else bytesToRead = restBytes; bytesToRead = this.Decode( data, offset, bytesToRead ); restBytes -= bytesToRead; index += bytesToRead; while ( wrapingAround && restBytes != 0 ) { this.Position = 0; if ( restBytes > this.dataLength * blockAlign ) bytesToRead = this.dataLength * blockAlign; else bytesToRead = restBytes; bytesToRead = this.Decode( data, offset + index, bytesToRead ); restBytes -= bytesToRead; index += bytesToRead; } return index; }
byte配列dataのoffsetバイト目からcountバイトに、デコードしたデータを書き込む関数です。配列dataは予め呼び出し側で確保し、pinしていることを期待します。wrapingAroundはラップアラウンドをするかどうかで、trueの場合、ストリームの終端まで読みきっても、また先頭に戻ってデコードを続けるものです。個人的に必要だったのでつけましたが、要らない場合は関連する所をばっさり切れば簡単になります。切ってしまえば、殆どReadEntireDataと変わりませんね。
これでデコードも終わりです。ようやく再生できる訳ですね。最初はこんなに長くするつもりは無かったのですが、見返してみると結構あるものです。では、この辺で「C#でOgg Vorbis」シリーズを終わらせることにします。最後まで読みきってくれたあなたに感謝。
コメントする