前回は、OggVorbis
ネームスペースを作り、その中でプラットフォーム呼び出し用の外部関数を定義して、必要なクラスや構造体も用意しました。今回は、実際にこれらのものを使ってOgg Vorbisのファイルをデコードしてみます。
Vorbisfile API Referenceによれば、一つのOgg Vorbisファイルを扱うのに、FILE構造体と同じような感覚でOggVorbis_File構造体を使う必要があります。これをラップするクラスとして、OggVorbis.OggVorbisDecoder
クラスを作ることにします。
// OggVorbisDecoder.cs
using System;
using System.IO;
namespace OggVorbis {
// ……
// ……Functions、OggVorbisFileクラスなど
// ……
public class OggVorbisDecoder {
#region コンストラクタ/デストラクタ
public OggVorbisDecoder( Stream source ) {
this.stream = source;
this.binaryReader = new BinaryReader( source );
this.OpenOggVorbis();
return;
}
#endregion
#region デコード
private Stream source;
private BinaryReader binaryReader;
private OggVorbisFile oggVorbisFile;
#endregion
#region Ogg Vorbisのオープン
private void OpenOggVorbis() {
return;
}
#endregion
}
}
上のソースではまだreturn文しか書かれていないOggVorbis.OggVorbisDecoder.OpenOggVorbis
関数の中で、ファイルを開く処理をしましょう。その為に、とりあえずstream、binaryReader、oggVorbisFileの3つのフィールドを用意しました。
Vorbisfile APIには、ファイルを開く為の関数としてov_openとov_open_callbacksがありますが、使うのは当然前回エクスポートしたov_open_callbacksの方です。これはOggVorbis.Functions.OpenCallbacks
として使えるようになっています。この関数は、こう宣言されていました:
[DllImport( "OggVorbis.dll", EntryPoint = "ov_open_callbacks" )]
public static extern int OpenCallbacks( IntPtr dataSource,
[In, Out] OggVorbisFile oggVorbisFile,
IntPtr initial, int initialBytes, OggVorbisCallbacks callbacks );
パラメータの詳しい説明はVorbisfile APIのリファレンスを見てもらうこととして、第1引数のdataSourceで、ソースを渡したいのですが……。
private void OpenOggVorbis() {
OggVorbisCallbacks callbacks = new OggVorbis.OggVorbisCallbacks();
callbacks.readFunction = Read;
// callbacks.readFunction = new ReadFunction( Read ); と等価
// このRead、Seek、Close、Tellはあとで定義する
callbacks.seekFunction = Seek;
callbacks.closeFunctrion = Close;
callbacks.tellFunction = Tell;
if ( Functions.OpenCallbacks( /* データソースを渡したいが…… */,
this.oggVorbisFile, IntPtr.Zero, 0, callbacks ) < 0 )
throw new InvalidDataException();
return;
}
第1引数はポインタです。this.streamかthis.binaryReaderを渡したいところですが、前述の通り参照はプラットフォーム呼び出しのときにマーシャラによってポインタに変換されます。しかも、呼び出しの間は固定されています。それはいいのですが、この関数の呼び出しが終わったら、またガベージコレクタに移動させられる可能性があります。この為、パラメータとして渡す前に、マネージドオブジェクトを固定するか、ハンドルのようなものが必要になります。実はこれにはうってつけのものがあります。System.Runtime.InteropServices.GCHandle
構造体がそれです。
GCHandleがどんなものかはリファレンスを見ればわかると思うので、使い方だけ紹介します。
using System.Runtime.InteropServices;
public OggVorbisDecoder {
// ……
private GCHandle binaryReaderHandle;
// ……
private void OpenOggVorbis() {
OggVorbisCallbacks callbacks = new OggVorbis.OggVorbisCallbacks();
this.binaryReaderHandle = GCHandle.Alloc( this.binaryReader );
callbacks.readFunction = Read;
callbacks.seekFunction = Seek;
callbacks.closeFunctrion = Close;
callbacks.tellFunction = Tell;
if ( Functions.OpenCallbacks( (IntPtr)this.binaryReaderHandle,
this.oggVorbisFile, IntPtr.Zero, 0, callbacks ) < 0 )
throw new InvalidDataException();
return;
}
}
このように、GCHandle.Alloc
でハンドルを取得して、そのハンドルをSystem.IntPtr
にキャストして渡します。こうすると、あとでthis.binaryReaderを(スコープは違いますが)取得することができます。ファイルを開く流れはこんな感じです。
まぁ、ReadやSeekなどを実装していないのでこれだけではまだ動きませんが、こっちの方が簡単なので先にファイルを閉じる処理をしてしまいます。アンマネージドリソースを使っている場合はSystem.IDisposable
を実装するのがいいでしょうから、Disposeの中でov_clearを呼び出すことにします。
public class OggVorbisDecoder : IDisposable {
// ……
private bool disposed = false;
public void Dispose() {
if ( this.disposed )
return;
Functions.Clear( this.oggVorbisFile );
this.binaryReaderHandle.Free();
this.binaryReader.Close();
this.disposed = true;
GC.SuppressFinalize( this );
return;
}
// ……
}
Functions.Clear
の呼び出しは見ての通りですが、GCHandle.Allocで取得したハンドルは忘れずにGCHandle.Freeで解放しましょう。this.binaryReaderを閉じるのも忘れずに。
さて、まだファイルのオープンとクローズしかできませんが、とりあえず動くようにしましょう。その為に、先送りにしたReed、Seek、Close、Tellの4関数をOggVorbisDecoderに追加します。これらの関数は、Vorbisfile - Callbacks and non-stdio I/Oで説明されているような処理を行わなければなりません。色々書いてありますが、大体CRTのfread、fseek、fclose、ftellと同じものです。
まずはOggVorbisDecoder.Readから。
private static int Read( IntPtr destination, int size, int count, IntPtr dataSource ) {
byte[] data = null;
try {
GCHandle binaryReaderHandle = (GCHandle)dataSource;
BinaryReader binaryReader = (BinaryReader)binaryReaderHandle.Target;
data = binaryReader.ReadBytes( size * count );
if ( data.Length != 0 )
Marshal.Copy( data, 0, destination, data.Length );
}
catch {
Functions.SetError( 1 );
return 0;
}
Functions.SetError( 0 );
return data.Length;
}
size * countバイト読んで、destinationの指すアドレスに書き込み、実際に読み取られたバイト数(<= size * count)を返すものです。Functions.OpenCallbacksで渡したdataSourceは、この関数の第4引数として渡されます。まぁ、見ての通りですが、IntPtrをGCHandleにキャストして、Targetを取ればthis.binaryReaderが得られます。アンマネージド配列へのコピーはMarshal.Copyが速いです。
ちなみに、一時変数dataの存在を嫌ってMarshal.WriteByteなどでコピーすると劇的に遅くなります。加えて、.NETのガベージコレクタは一時変数をうまく回収できるようにチューニングされているらしく、デコード中にメモリ使用量がどんどん増えていくというようなことも(少なくともこの環境では)起こりませんでした。素直に中間配列を使うのがいいでしょう。
さて、コードばかり長いですが、次はSeekです。
private static int Seek( IntPtr dataSource, long offset, int whence ) {
GCHandle binaryReaderHandle = (GCHandle)dataSource;
Stream stream =
( (BinaryReader)binaryReaderHandle.Target ).BaseStream;
switch ( whence ) {
case 0: // SEEK_SET(ストリームの先頭)
stream.Position = offset;
break;
case 1: // SEEK_CUR(現在位置)
stream.Position += offset;
break;
case 2: // SEEK_END(ストリームの末尾)
stream.Position = stream.Length;
break;
default:
return -1; // エラー通知
}
return 0;
}
これはBinaryReaderではなくStreamを扱わなければならないので、streamの取得がえらく長くなっていますが、勿論複文にしても同じです。あとは特に触れるべきこともないでしょう。
次は、Close。
private static int Close( IntPtr dataSource ) {
return 0;
}
……って、何もしなくていいんですよね。というのは、dataSourceであるthis.binaryReaderは、Disposeで閉じているからです。このCloseはov_clearの中からのみ呼ばれるので、わざわざここでやる必要は無い、ということです。勿論Disposeの中のthis.binaryReader.Close();
をここでやってもいいですが。
最後はTell。
public static int Tell( IntPtr dataSource ) {
GCHandle binaryReaderHandle = (GCHandle)dataSource;
Stream stream =
( (BinaryReader)binaryReaderHandle.Target ).BaseStream;
return (int)stream.Position;
}
バイトオフセットを返せばいいので、これもStreamを扱います。やってることも大したことではないし、これで終了。
これらの関数をOggVorbis.OggVorbisDecoder
クラスに実装すれば、晴れてOgg Vorbisのファイルを開くことができる訳です。……再生は、次の次で。次回は位置取得、フォーマット取得などを行います。