C#の最近のブログ記事

概要: 複数のスレッドが動いていると、どのような順番でそれぞれの処理が実行されるかという事は、予測が困難なものですね。 先日、DirectSound周りの処理を改良していた時、スレッドを二つ作り、メインスレッ...

複数のスレッドが動いていると、どのような順番でそれぞれの処理が実行されるかという事は、予測が困難なものですね。

先日、DirectSound周りの処理を改良していた時、スレッドを二つ作り、メインスレッドでは次のような処理を行わせていました(話を単純にする為にある程度簡略化しています)。

// メインスレッド(通常の優先度)で
private void PlayButton_Click( object sender, EventArgs e ) {
  // ……
  // OggVorbisDecoderは、ISoundDecoderを実装するクラス
  this.sound = new StreamingSound(
    new OggVorbisDecoder( new FileStream( this.sounds[ 0 ] ) )
  );
  for ( int i = 1 ; i < sounds ; i++ )
    // StreamingSound.AddFollowingDecoderは、
    // 部分ループ再生の為に後続のデコーダを追加するメソッド
    this.sound.AddFollowingDecoder(
      new OggVorbisDecoder( new FileStream( this.sounds[ i ] ) )
    );
  // それなりに長い処理(空行抜きで15行分程度)
  this.sound.Play();
  // ……
}

// メインスレッドから呼び出される、StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// メインスレッドから呼び出される、StreamingSoundのメソッド
public void Play() {
  // this.bufferは、Microsoft.DirectX.DirectSound.SecondaryBuffer
  if ( this.buffer.Status.Playing )
    return;

  // this.decoderLockは、単なるobjectのインスタンス
  lock ( this.decoderLock )
    this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

そして、セカンダリバッファの内容を更新するRefreshingThreadでは、次のようにしていました。

// バックグラウンドスレッド(通常の優先度)で
private void RefreshingThread() {
  // 変数宣言(代入含めて、5つ程)……
  // まだPlayが呼び出されては困る(ゴミが再生される恐れがある)
  lock ( this.decoderLock )
    // バッファの前半にPCMデータを書き込む
    this.WriteHalfBuffer( 0, 0 );
  // 準備完了、もうPlayを呼び出しても大丈夫
  // ……
}

lock ( this.decoderLock )というのは、クリティカルセクションを作る為に導入したものです。lock文のブロックの中に何れかのスレッドが進入すると、他のスレッドでは、現在クリティカルセクションに進入しているスレッドがそのオブジェクトの所有権を手放さない限り(つまりlock文を抜けない限り)、同じオブジェクトを使ってlock文の中に入る事ができなくなります。ここでは僕は、二つのスレッドの優先度が同じで、RefreshingThreadをthis.refreshingThread.Start()で開始させた時からの作業量はどう見てもバックグラウンドスレッドの方が少ないので、WriteHalfBufferが先に呼ばれるものと思っていました。実際Debugビルドではそうだったようで、再生してみても、サウンドの頭から綺麗に再生されていました。

ところが、Releaseビルドで作られた実行ファイルでは、どうも初回の再生だけ汚くなるのです。ノイズが聞こえる訳ではないですが、どうも頭の部分が切れている。初回の再生(初めて「再生」ボタンを押した時)と二回目以降の再生で違うのは、StreamingSoundを作り直すかどうかだけ。つまり、上記のソースコードで一番上の辺りの処理が怪しいのです。色々調べてみましたが、ふと気になって、StreamingSound.Playの中と、RefreshingThreadのWriteHalfBufferの前後で、System.DateTime.Now.Ticksを使って、処理が実行された時間を調べてみました。StreamWriterによって書き出されたログを見てみると……案の定、Playが先に呼ばれています。少し後れてRefreshingThreadのWriteHalfBufferが実行されていたようです。そりゃー、頭が切れる訳ですね。再生開始時点では何もバッファに書き込まれていない訳ですから。雑音が聞こえなかったのは運が良かったのでしょう。

さて、ではどう解消すべきか。lock文は、クリティカルセクションを作る事はできますが、スレッドの処理の実行順序を支配する事はできません。ミューテックス(System.Threading.Mutex)も同様ですし、モニタ(System.Threading.Monitor)はlock文の実体なので、書き換えた所で意味がありません。また、Releaseビルドでは問題が起こるけれどもDebugビルドでは起こらないから、といってReleaseビルドの実行ファイルを使わないようにするのも面白くありません。それに、Debugビルドだって、きちんと動いているように見えるだけで、その動作は「たまたま」上手く行っているのでしょう。確実に実行順序を支配する機構が欲しい所です。

これに打って付けなのは、イベントでしょう。と言ってもC#の言語機構のeventキーワードやdelegateを使う方ではなく、Win32 APIにもあるような、スレッドの同期を実現するイベントの方です。.NET Frameworkでは、System.Threading.AutoResetEventやSystem.Threading.ManualResetEventがそれに当たります。今回は特にマニュアルリセットが必要とされるものでもないので、AutoResetEventを使って同期してみました。主な変更は次の3箇所です。

// StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  // 最初は非シグナル状態にするので、falseを渡す
  this.decoderReady = new AutoResetEvent( false );
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// StreamingSound.RefreshingThread
private void RefreshingThread() {
  // 変数宣言……
  // バッファの前半にPCMデータを書き込む
  this.WriteHalfBuffer( 0, 0 );
  this.decoderReady.Set();
  // ……
}

// StreamingSound.Play
public void Play() {
  if ( this.buffer.Status.Playing )
    return;

  this.decoderReady.WaitOne();
  this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

イベントには、ミューテックスなどと同じように、シグナル状態と非シグナル状態という二つの状態があります。シグナル状態ではWaitOneなどのメソッドは成功し、次に進む事ができます。非シグナル状態ではWaitOneなどのメソッドは成功する事は無く、次に進む事はできません(無理矢理進んだりしても、同期に失敗しているのですから望む結果は得られないでしょう)。シグナル状態には、コンストラクタでtrueを渡す、Setメソッドを呼び出す、などの操作をする事で変化させる事ができます。非シグナル状態には、コンストラクタでfalseを渡す、Resetメソッドを呼び出す、WaitOneなどの待機関数を成功させる、などの操作で変化させられます。

StreamingSound.Playで呼び出しているWaitOne関数は引数がありません。引数無しでこの関数を呼び出すと、そのイベントがシグナル状態になるまで無限に待機します。コンストラクタではこのイベントは非シグナル状態にされているので、バックグラウンドスレッドのRefreshingThreadがSetしない限り、メインスレッドはサウンドの再生を開始しないという訳です。

……久し振りにプログラミングに関する覚え書きを書きました。

概要: あーやだやだ、なんでどれもこれも横幅もフォントサイズもピクセル固定のスタイルばかりなんでしょうかMovable TypeのStyle Catcherで取得できるスタイルは。Styleとしては合格かも知...

あーやだやだ、なんでどれもこれも横幅もフォントサイズもピクセル固定のスタイルばかりなんでしょうかMovable TypeのStyle Catcherで取得できるスタイルは。Styleとしては合格かも知れませんが、Designとしては不合格じゃないでしょうか。適当にいじって横幅もフォントサイズも可変にしてみましたが、こんなんじゃ気軽にスタイル変えられないですね。はぁ、これでまた一つ消滅事由が増えた。

愚痴はさておき、FLAC(Free Lossless Audio Codec)のバージョンが1.1.4になっていたので、今まで使っていたFLAC用のDLLもバージョンアップしました。以前のDLLはlibflac-1.1.2を使っていたので、結構違いがありました。と言っても1.1.4はプログラミングインターフェースには大した差は無くて、大きな変更があったのは1.1.3なんですが、

  • file_decoder、seekable_stream_decoderはstream_decoderに統合
  • libOggFLAC、libOggFLAC++はそれぞれlibFLAC、libFLAC++に統合
  • stream_decoderの初期化方法の変更(但し、その他の面では統一的に扱えるようになった)
  • 他、ステータスを表す列挙型のメンバの変更

……などなど、まぁ変えなければならない箇所がわんさか。おまけにlibflacは、libvorbisのものよりもソースに手を加えなければならない箇所が多い(28箇所、コールバック関数の呼び出し規約を__stdcallに変えなければならない)ので、ちょっとうんざりしました。nasmからエクスポートされる関数が__cdeclなもんで、既定値を__stdcallにすると余計面倒くさくなりそう(未確認)なんですよねぇ。まぁ、スマートになったから良しとしますか。

ちなみにFLAC 1.1.4の最大の魅力はエンコード速度・デコード速度の劇的な上昇で、-8オプション(最高圧縮)のエンコードスピードは約2倍になったとか(体感的にもそのくらいです)。デコードの方は調べていませんが、最近作っていた曲のループ境界探索ツールで実験した所、同じサウンドデータから作ったRIFF Wave(生のPCM)オーディオ、Ogg Vorbisオーディオ、Native FLACオーディオ、Ogg FLACオーディオに対して同じ条件で探索してみても、かかる時間は全て約5分で、有意な差は殆ど感じられませんでした。

ここ一週間、libflac-1.1.4の他にも、DirectSound周りのライブラリやそのテスト用ツールを改良していました。大分満足できるできになりましたが、Direct3D周りにはまだまだやる事が残っているのでした。

概要: OggVorbis_Fileって、ヒープ中で完全に位置が固定されている事を仮定しているんでしょうか? 今作っている曲のループ境界探索ツール(?)でAccessViolation問題が再発したので、試し...

OggVorbis_Fileって、ヒープ中で完全に位置が固定されている事を仮定しているんでしょうか? 今作っている曲のループ境界探索ツール(?)でAccessViolation問題が再発したので、試しに次のようにしてみたら、ぴたりと止みました(また気のせいかも知れませんが)。

public class OggVorbisFile : IDisposable {
  #region コンストラクタ/ファイナライザ
  public OggVorbisFile() {
    this.data = new byte[ 720 ];
    this.gchandle = GCHandle.Alloc( this.data,
      GCHandleType.Pinned );
    this.address = Marshal.UnsafeAddrOfPinnedArrayElement
      ( this.data, 0 );
    return;
  }
  ~OggVorbisFile() {
    this.Dispose();
 
    return;
  }
  private bool disposed = false;
  public void Dispose() {
    if ( this.disposed )
      return;
 
    this.gchandle.Free();
 
    this.disposed = true;
    GC.SuppressFinalize( this );
 
    return;
  }
  #endregion
  #region 実体
  byte[] data;
  GCHandle gchandle;
  IntPtr address;
  #endregion
  #region 型変換
  public static implicit operator IntPtr
    ( OggVorbisFile oggVorbisFile ) {
    return oggVorbisFile.address;
  }
  #endregion
}

OggVorbis_Fileは720バイトだという事を既知として(無論、libvorbisのバージョンアップによって変わる可能性はあります)、720バイトのbyte配列を作り、GCHandleでpinする、つまりヒープ中でのアドレスを固定するというものです。以前のOggVorbisFileと違う所は、ヒープ中で固定されているかいないかだけ(のはず)なのですが、何故なんででしょうねぇ(libvorbisのソースを見ていないので、これで解決できているのかどうかは断言できませんが、OggVorbis_Fileのメンバにアクセスしないのであればこれが一番簡単なCヒープの模倣手段でしょう)。

ついでに、セカンダリバッファを更新するスレッド「だけ」で、ファイルのov_open_callbacksからov_readまでの処理をするようにしていましたが、ov_open_callbacksをメインスレッドで、ov_readを別スレッドで呼び出すようにしても、AccessViolationが出なくなりました。これでdeferOpeningとかいう、あまり外部に見せたくない引数を消す事ができました。

2007-03-09追記: この方法を使う時は、アンマネージド関数のプロトタイプ宣言で[In, Out] OggVorbisFile oggVorbisFileとしていた所は、IntPtr oggVorbisFileに変えておいてください。暗黙の型変換が行われるので、労力は小さくて済むでしょう。

概要: 最近XHTML 1.1 Referenceの方もForms Moduleのところで時間かかってるし、こっちも暫く放っておいたなぁと反省して、ちょこっと小ネタでも。以前やっていたDirect Sound...

最近XHTML 1.1 Referenceの方もForms Moduleのところで時間かかってるし、こっちも暫く放っておいたなぁと反省して、ちょこっと小ネタでも。以前やっていたDirect Sound周りが一段落したので(Ogg Vorbisに加えてFLACまであまり必要でもないのにやってしまった)、この前までDirect3D関係を実装していました。その辺りで身を以て思い知らされた事などを少々。

LoaderLock が検出されました

Visual Studio 2005とManaged DirectX 1.1を使っていると、デバッグ時に「ローダーロックが検出された」というエラーが出てきて、「DLL '...\Microsoft.DirectX.Direct3D.dll' は、OS ローダー ロック内でマネージ実行を試行しています。DllMain またはイメージ初期化関数内でマネージ コードを実行しないでください。この動作は、アプリケーションをハングさせる原因になる可能性があります。」と言われます。どうも.NET Framework 2.0とMDX 1.1は折りが合わないようで、そのままの設定ではうまくデバッグが出来ないようです。難しい内部事情は知りませんが、手っ取り早い解決策があります

  1. メインメニューで「デバッグ」「例外」を選び、「例外」ダイアログを開きます。
  2. 「Managed Debugging Assistants」の中の、「LoaderLock」のチェックを外します
  3. 「OK」ボタンで確定します。

Managed Debugging AddistantsのLoader Lockのチェックを外す

この設定はプロジェクト固有なので、新しいプロジェクトを作った場合には毎回設定しなければなりませんが、VS2003とCLR 1.1に戻るくらいならこっちの方がいいでしょう。

参考 : Why do I get a 'LoaderLock' Error when debugging my Managed DirectX application

Microsoft.DirectX.Direct3D.Spriteの有用性

下手な自前のコードよりは速く動作するでしょう。ただ、アルファブレンドが自由に行えないなどの機能的欠点や、バッファリングして描画した時のDrawIndexedPrimitivesには敵わないなどのパフォーマンス的欠点は持ちます。

しかし、デバッグ時にちょっとテクスチャを貼り付けて表示したいとか、普通のアルファブレンドしか使わず、スプライトの数も多くないといった場合には十分有用でしょう。

大量のプリミティブの高速描画

D3DXのSpriteは確かに数が増えてくるとシーン時間が長くなってきますが、DrawPrimitivesやDrawIndexedPrimitivesを使ってベタに組んでも、大した差は出ません。ベタに組む、というのは、スプライトを描画する度に、頂点4つの頂点バッファとインデックス4つのインデックスバッファを使ってTriangleStripを2つ、DrawIndexedPrimitivesで描画するというものです。DirectXでは、DrawUserPrimitivesなども含めて、Draw系の関数の呼び出しは出来るだけ数を減らす方がいいようです

これを実現するには、描画するスプライトの情報をバッファリングして、あとでまとめて頂点バッファに書き出して描画、ということが必要になります。シーンの中では、頂点バッファとインデックスバッファをセットして(ステートブロックを使うと良い)、TriangleListでDrawIndexedPrimitivesを呼び出すだけです。スプライト毎にワールド変換が異なるので、頂点変換はCPUで事前にやっておかなければなりませんが、GPUがせっせとレンダリングしてる裏で処理できるのでさほど問題ではないでしょう。

恐らく、一番のネックは頂点バッファへの書き込みとなります。C++ではポインタが何にでもキャストできる為気になりませんが、C#では、CustomVertex.PositionColoredTextured配列を書き込もうとするとキャストがうまく出来ない為につっかかります。かといって配列を返すバージョンのMicrosoft.DirectX.Direct3D.VertexBuffer.Lockではパフォーマンス的に不利です。GraphicsStreamを返すLockはbyte配列なら書き込めますが、頂点変換の時に一々頂点の構造体のデータの並びをbyte配列に手で直して代入するのも面倒です。

これには抜け道があります。アンセーフコードを書いてポインタにするのではありません、StructLayoutを使えば一発です。

using Microsoft.DirectX.Direct3D;
using System.InteropServices.Runtime;
 
[StructLayout( LayoutKind.Explicit )]
public struct VertexArray {
  public VertexArray( int length ) {
    this.VerticesAsByteArray = null;
    this.Vertices = new CustomVertex.PositionColoredTextured
      [ length ];
 
    return;
  }
  [FieldOffset( 0 )]
  public CustomVertex.PositionColoredTextured[] Vertices;
  [FieldOffset( 0 )]
  public byte[] VerticesAsByteArray;
}

何をやりたいか、見たら判ると思います。C#における共用体です(詳しくはUnions のサンプルを参照)。上のコードは、C++で書いたら次のようなものです。

struct PositionColoredTextured;
 
union VertexArray {
  PositionColoredTextured* Vertices;
  unsigned char* VerticesAsByteArray;
};

頂点変換の時はVerticesを、GraphicsStream.Writeに渡す時はVerticesAsByteArrayを使えばいい訳ですね。なんでこれくらいのことがC#では手軽に出来ないんでしょう。配列をbyte配列として見るかint配列としてみるかとかの変換くらいはキャストだけで出来てもいいような気がしますが……。僕が知らないだけですか? ちなみにオブジェクト配列は無理みたいですがbyte配列とプリミティブ間ならSystem.BitConverterとかいうものが、プリミティブ配列同士ならSystem.Bufferっていうのがあるみたいですね。どっちにしろ無駄なコピーが発生するみたいで残念ですけど。

2007-01-30追記: この時は配列の実体ばかり気にしていて、付随するプロパティに関心がありませんでしたが、問題になることがあるかも知れません。特にSystem.Array.Length。10000バイトのbyte配列を作り、上記の方法でushort配列として扱っても、その擬似ushort配列のLengthプロパティは10000を返します(Lengthプロパティのgetアクセサがプライベートメンバの値をただ返しているのでしょう)。勿論、インデクサはきちんと値を返してくれます(arrayContainer.UInt8Array[ 0 ]が0x34で、arrayContainer.UInt8Array[ 1 ]が0x12の時、arrayContainer.UInt16Array[ 0 ]は0x1234を返すという事)。プログラマは勿論実際の配列の長さを把握しているでしょうが、byte配列を要求する.NET Frameworkの関数(例えばSystem.IO.Stream.Readなど)に渡す時は、最初にbyte配列として作らないといけなくなるでしょう。

概要: Ogg Vorbisデータをデコードすると言う本編(?)は終わりましたが、これをストリーミング再生するときの注意点なんかを挙げておきます。別にマルチスレッドのプロと言う訳ではないので、参考までに。 V...

Ogg Vorbisデータをデコードすると言う本編(?)は終わりましたが、これをストリーミング再生するときの注意点なんかを挙げておきます。別にマルチスレッドのプロと言う訳ではないので、参考までに。

Vorbisfile APIのスレッドセーフティ

Vorbisfile APIは、Vorbisfile - Thread Safetyにあるように、一つのOggVorbis_File構造体を同時に使うことのできるスレッドは一つだけです。これを保障するために、マネージドコード側でも複数のスレッドが同時にReadやPositionを使ったりすることが無いようにする必要があります。具体的には、System.Threading.Mutexを使うか、lock文でクリティカルセクションを作って同期しなければなりません。

Ogg Vorbisの操作は単一スレッドで

VS2003でやっていたときはこんなことが起こらなかったと思うのですが、VS2005+MDX 1.1では、スレッドセーフな扱い方をしていても、oggVorbisFile->vd.viがクリアされることがあるようです。この為、AccessViolationExceptionが投げられます。原因を調べようと思いましたが、やってる内にうんざりしたので、OpenCallbacksを呼ぶのもDecodeを呼ぶスレッドでするようにしたら、改善されました。気のせいかもしれませんが。

複数スレッドからアクセスされるメンバはvolatile

念の為ですが、volatileキーワードを付けておくと、キャッシュが効かなくなるためいつでも正しい値が読み取られます。メインスレッドで位置を取得して、別スレッドでバッファを書き換えるような場合、付けておくと安心です。

概要: 長かった準備もようやく終わり、いよいよデコードする時がやって参りました。とは言え、デコードした後再生するまでにはまた別のコードが必要になりますが、まぁそれはここでは詳しくは触れません。 デコードは、o...

長かった準備もようやく終わり、いよいよデコードする時がやって参りました。とは言え、デコードした後再生するまでにはまた別のコードが必要になりますが、まぁそれはここでは詳しくは触れません。

デコードは、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」シリーズを終わらせることにします。最後まで読みきってくれたあなたに感謝。

概要: 前回、Ogg Vorbisファイル(ストリームになっていれば、ファイルじゃなくてもいいけれど)を開いて、正しく閉じるところまで実装しました。今回は、デコードの前に、他のインフラを整えます。つまり、フォ...

前回、Ogg Vorbisファイル(ストリームになっていれば、ファイルじゃなくてもいいけれど)を開いて、正しく閉じるところまで実装しました。今回は、デコードの前に、他のインフラを整えます。つまり、フォーマットを取得したり、位置を取得・設定できるようにします。基本的な部分は前回実装してしまったので、今回は楽です。書く方にとっても。

まずはフォーマットを取得しましょう。ここでいうフォーマットというのはストリームのデータがOgg Vorbisかどうか、ということではなくて、ウェーブフォーマットのことです。Managed DirectXでは、Microsoft.DirectX.DirectSound.WaveFormat構造体に当たります。この構造体はセカンダリバッファを作るときに必要になります。また、データの長さも必要になるでしょう。

using Microsoft.DirectX.DirectSound;
 
public class OggVorbisDecoder : IDisposable {
  // ……
  private WaveFormat format;
  public WaveFormat Format {
    get {
      return this.format;
    }
  }
  private long dataLength;
  public long Length {
    get {
      return this.dataLength;
    }
  }
  // ……
}

と、外部から見えるようにしておきます。実際にthis.formatやthis.dataLengthの内容を書き換えるのは、OggVorbisDecoder.OpenOggVorbisの中にします。

private void OpenOggVorbis() {
  // ……Functions.OpenCallbacksでファイルを開く処理(前回説明)
 
  IntPtr information;
 
  information = Functions.GetInformation( this.oggVorbisFile, -1 );
  if ( information == IntPtr.Zero )
    throw new InvalidOperationException();
  this.format.FormatTag = WaveFormatTag.Pcm;
  this.format.Channels = (short)Marshal.ReadInt32( information, 4 );
  this.format.BitsPerSample = 16;
  this.format.BlockAlign = (short)( 2 * this.format.Channels );
  this.format.SamplesPerSecond = Marshal.ReadInt32( information, 8 );
  this.format.AverageBytesPerSecond =
    this.format.BlockAlign * this.format.SamplesPerSecond;
 
  this.dataLength = Functions.PCMTotal( this.oggVorbisFile, -1 );
  if ( this.dataLength < 0 )
    throw new InvalidOperationException();
 
  return;
}

フォーマットはov_infoに相当するFunctions.GetInformationで取得できます。ここで失敗するとNULL(=0)を返すので一応例外を投げるようにしておきます。

実は、WaveFormat構造体にセットする必要がある値のうち、vorbis_info構造体には2つしか含まれていません。他の値は自分で適宜設定しなければなりません。

FormatTag
PCMデータであることを示すWaveFormatTag.Pcmを代入します。ov_readもPCMデータを返しますし、DirectSoundではPCM以外扱えません。
Channels
チャネル数を代入します。大抵1(モノラル)か2(ステレオ)です。これは、vorbis_info構造体の2番目のメンバ(int)に入っています。これは先頭から4バイトの位置にあるので、Marshal.ReadInt32( information, 4 )で値を取得してshortにキャストして代入します。
BitsPerSample
サンプリングサイズをビット単位で設定します。Ogg Vorbisの場合、デコード時に決められるので、16にしておきます。
BlockAlign
全てのチャネルを合わせた1サンプル(ブロック)のバイト数を代入します。16ビット(2バイト)にチャネル数をかければいいでしょう。
SamplesPerSecond
サンプリング周波数です。これはvorbis_info構造体の3番目のメンバ(int)に入っているので、informationから8バイトの所にある値を代入します。
AverageBytesPerSecond
PCMの場合、単に一秒当たりのバイト数なので、BlockAlign * SamplesPerSecondと等価です。

フォーマットの設定が終わったら、データの長さも取得しておきます。Functions.PCMTotalはサンプル単位でデータ長を返してくれるので、それをそのままthis.dataLengthに入れます。負の値が返されたときは例によって例外を。

フォーマットとデータ長は用意ができたので、最後に位置の取得と設定を行うプロパティを作ります。

public long Position {
  get {
    long currentSampleIndex;
 
     currentSampleIndex = OggVorbis.PCMTell( this.oggVorbisFile );
    if ( currentSampleIndex < 0 )
      throw new InvalidOperationException();
 
    return currentSampleIndex;
  }
  set {
    if ( value < 0 )
      value = 0;
    else if ( value > this.dataLength )
      value = this.dataLength;
 
     if ( OggVorbis.PCMSeek( this.oggVorbisFile, value ) < 0 )
       throw new InvalidOperationException();
 
    return;
  }
}

データ長もサンプル単位で行ったので、ここでもPCMデータのサンプル単位で処理を行います。エラーが起きると何れも負の値を返すので、例外を投げましょう。

これで準備は終わりです。次は実際にデコードする関数を定義します。

概要: 前回は、OggVorbisネームスペースを作り、その中でプラットフォーム呼び出し用の外部関数を定義して、必要なクラスや構造体も用意しました。今回は、実際にこれらのものを使ってOgg Vorbisのファ...

前回は、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のファイルを開くことができる訳です。……再生は、次の次で。次回は位置取得、フォーマット取得などを行います。

概要: ここで必要になる構造体の最後は、ov_callbacksに相当するOggVorbisCallbacksです。これをどう定義すればいいかを知る為に、include\vorbis\vorbisfile.h...

ここで必要になる構造体の最後は、ov_callbacksに相当するOggVorbisCallbacksです。これをどう定義すればいいかを知る為に、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;

とあります。関数ポインタです。C/C++の関数ポインタに相当するC#の言語機構はデリゲートですから、これらに対応するデリゲートを定義します:

[StructLayout( LayoutKind.Sequential )]
  public struct OggVorbisCallbacks {
    public ReadFunction readFunction;
    public SeekFunction seekFunction;
    public CloseFunction closeFunctrion;
    public TellFunction tellFunction;
  }
}
public delegate int ReadFunction( IntPtr destination, int size,
  int count, IntPtr dataSource );
public delegate int SeekFunction( IntPtr dataSource, long offset,
  int whence );
public delegate int CloseFunction( IntPtr dataSource );
public delegate int TellFunction( IntPtr dataSource );

C/C++のsize_tは常に4バイトではありませんが、64ビット環境で8バイトになる基本型がポインタ系しか無いので、とりあえずintで代用しています。64ビット環境の場合はReadFunctionの戻り値はlongに変えて下さい。或いは、気にならないのであればIntPtrを使えば放っておけます。

デリゲートは関数ポインタに似た機構ですが、インスタンスメソッド(this参照を必要とするメソッド)も使えたり、一回のデリゲート呼び出しで複数のメソッドを順番に呼ぶことも可能です。しかし、OggVorbis.dll内部ではこれを__stdcallの関数として扱う為、そのようなデリゲート特有の機能は使えません。staticなメソッドを一つだけ入れて渡す必要があります。

さて、これで外部関数を可視化するのに必要なものは集まりました。SetError、ov_clear以外の関数のプロトタイプを見てみます。

extern int ov_open_callbacks( void* datasource, OggVorbis_File* vf,
  char* initial, long ibytes, ov_callbacks callbacks);
extern ogg_int64_t ov_pcm_total( OggVorbis_File* vf, int i );
extern int ov_pcm_seek( OggVorbis_File* vf, ogg_int64_t pos );
extern ogg_int64_t ov_pcm_tell( OggVorbis_File* vf );
extern vorbis_info* ov_info( OggVorbis_File* vf, int link );
extern long ov_read( OggVorbis_File* vf, char* buffer, int length,
  int bigendianp, int word, int sgned, int* bitstream );

これを、C#のOggvorbis.Functionsの中で可視化します。前述のSetErrorやov_clearも含めて書くと、Functionsクラスは次のようになります。

public class Functions {
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_open_callbacks" )]
  public static extern int OpenCallbacks( IntPtr dataSource,
    [In, Out] OggVorbisFile oggVorbisFile,
    IntPtr initial, int initialBytes, OggVorbisCallbacks callbacks );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_clear" )]
  public static extern int Clear(
    [In, Out] OggVorbisFile oggVorbisFile );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_pcm_total" )]
  public static extern long PCMTotal(
    [In, Out] OggVorbisFile oggVorbisFile, int bitStream );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_pcm_seek" )]
  public static extern int PCMSeek(
    [In, Out] OggVorbisFile oggVorbisFile, long position );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_pcm_tell" )]
  public static extern long PCMTell(
    [In, Out] OggVorbisFile oggVorbisFile );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_info" )]
  public static extern IntPtr GetInformation(
    [In, Out] OggVorbisFile oggVorbisFile, int bitStream );
  [DllImport( "OggVorbis.dll", EntryPoint = "ov_read" )]
  public static extern int Decode(
    [In, Out] OggVorbisFile oggVorbisFile, IntPtr destination,
    int length, int bigEndian, int wordSize, int signed,
    ref int bitStream );
  [DllImport( "OggVorbis.dll", EntryPoint = "SetError" )]
  public static extern void SetError( int errorOccurred );
}

大体はそのまんまです。ov_infoに相当するGetInformationのみ戻り値の型がIntPtrに変わっていますが、これはクラスへの参照を戻り値として取る関数が定義できなかったからです(知らないだけかも)。勿論、IntPtrを使ってアンマネージドデータの内容を読み取ることは可能ですので、心配する必要はありません。

これでようやく下準備は終わりです。結構長かったですね。次回はこれらの関数を実際に呼び出して使ってみます。

概要: 何故か無性にもっともっとインクレディブルマシーン(The Even More Incredible Machineの日本語版、参考:for all incrayers)がやりたくなったので、ここ暫く猿...

何故か無性にもっともっとインクレディブルマシーン(The Even More Incredible Machineの日本語版、参考:for all incrayers)がやりたくなったので、ここ暫く猿のようにやっていました(ぶっ続けでやっていた訳ではありませんが)。驚いたことに、Windows xpでもまともに動かせるようにしたパッチが作られていたんですね。慣れないWindows Application Compatibility Toolkitを弄くり回しながら、苦難の末折角最適な互換性設定を見つけたというのに……。TIM風に言うなら歯車の再発明というところでしょうか。

それはさておき、今回は前回作ったOggVorbis.dllを.NET環境から呼べるようにします。.NET環境とは言え相変わらずC#だけですが、他の言語でも似たような手法で出来るはずです。

まず、OggVorbis.dllをマネージドアプリケーションから見えるところにコピーしましょう。ユーザが作成したDLLの場合、実行可能ファイルと同じディレクトリに入れておくのが一般的です。クラスライブラリを作ってそこから呼び出す場合でも、クラスライブラリの出力ディレクトリではなく、そのクラスライブラリを使うアプリケーションの実行可能ファイルと同じ階層でなければ駄目です。

では、まず手始めにエラー処理の為に追加したSetError関数を可視化してみましょう。この関数を呼ぶには、次のようなコードが必要になります:

using System.Runtime.InteropServices;
 
namespace OggVorbis {
  public class Functions {
    [DllImport( "OggVorbis.dll", EntryPoint = "SetError" )]
    public static extern void SetError( int errorOccurred );
  }
}

まず、外部の関数はstatic externで修飾しなければなりません。この関数を持つためのラッパクラスを用意して、OggVorbis.Functionsと名付けました。関数のプロトタイプには、普通のマネージド関数と同じように、戻り値と引数の型を指定してやります。今回は戻り値は無く(void)、int型の引数を持っていたので、上のようになります。VC++ 2005では、__w64をつけない限り常にintは4バイトですので、C#でもint(Int32のエイリアス、4バイト)を使います。

プロトタイプには、DllImportAttributeを付けます。必須の引数としてDLL名、他にオプションとしてエントリポイント名、呼び出し規約などを指定できます。エントリポイント名は、上記のようにマネージド関数の名前と完全に一致する場合には付ける必要はありません。

さて、SetErrorの場合は簡単でしたが、ここからは少し骨が折れます。他のエクスポート関数では、include\vorbis\vorbisfile.hのOggVorbis_File構造体が使われているからです。普通にvorbisfile APIを使う分にはそれ程律儀にやらなくてもいいのですが、ここでは敢えて律儀にやります。

手始めに、SetErrorの次に簡単なov_clear関数を可視化します。

namespace OggVorbis {
  public class OggVorbisFile {
    // ……
  }
 
  public class Functions {
    [DllImport( "OggVorbis.dll", EntryPoint = "ov_clear" )]
    public static extern int Clear(
      [In, Out] OggVorbisFile oggVorbisFile );
    // ……
  }
}

ov_clearを呼び出す為の関数として、OggVorbis.Functions.Clearを作りました。ov_clearのプロトタイプはextern int ov_clear( OggVorbis_File* vf );となっているので、OggVorbis_File構造体に相当するものとしてOggVorbisFileクラスを定義します。…が、少しこのプロトタイプは奇妙です。CではOggVorbis_Fileへのポインタでしたが、C#ではクラスの前にInAttributeとOutAttributeがついています。Inは、このパラメータの値(OggvorbisFileへの参照)が外部関数に渡されるべきであることを示し、Outは外部関数からこのパラメータに書き込まれた値をC#側で読み取れるようにすることを指示します。つまり、[In, Out]とやるとC#のrefキーワードに相当しますね。そして、OggVorbisFileへの参照は、ov_clearの呼び出し時にアドレスへと変換されます。マネージドオブジェクトはメモリの中で常に位置がロックされているという保証はありませんが、プラットフォーム呼び出しの間はずっと固定されていることが保証されます。……ちなみに、ov_clear関数がパラメータの値そのものを書き換えることは無いでしょうが、ov_clear( OggVorbis_File* const vf );となっていないので、念の為OutAttributeもつけておきました。

さて、ov_clearを呼び出すClear関数の方は準備できましたが、肝心のOggVorbisFileクラスの方は準備が出来ていません。vorbisfileのOggVorbis_File構造体を受け渡しするのですから、当然それに対応したクラスである必要があります。と思ってvorbisfile.hを見てみると:

typedef struct OggVorbis_File {
  void* datasource;
  int seekable;
  ogg_int64_t offset;
  ogg_int64_t end;
  ogg_sync_state oy;
  int links;
  ogg_int64_t* offsets;
  ogg_int64_t* dataoffsets;
  long* serialnos;
  ogg_int64_t* pcmlengths;
  vorbis_info* vi;
  vorbis_comment* vc;
  ogg_int64_t pcm_offset;
  int ready_state;
  long current_serialno;
  int current_link;
  double bittrack;
  double samptrack;
  ogg_stream_state os;
  vorbis_dsp_state vd;
  vorbis_block vb;
  ov_callbacks callbacks;
} OggVorbis_File;

……とあります。結構量が多いです。しかも、ogg_sync_stateとか、vorbis_dsp_stateとか、構造体がメンバに含まれています。細かいことを捨てて楽をすることも出来るのですが、飽くまでも律儀に模倣することにします。

[StructLayout( LayoutKind.Sequential )]
public class OggVorbisFile {
  IntPtr datasource;
  Int32 seekable;
  Int64 offset;
  Int64 end;
  OggSyncState oy;
  Int32 links;
  IntPtr offsets;
  IntPtr dataoffsets;
  IntPtr serialnos;
  IntPtr pcmlengths;
  IntPtr vi;
  IntPtr vc;
  Int64 pcm_offset;
  Int32 ready_state;
  Int32 current_serialno;
  Int32 current_link;
  Double bittrack;
  Double samptrack;
  OggStreamState os;
  VorbisDSPState vd;
  VorbisBlock vb;
  OggVorbisCallbacks callbacks;
}

まず、ポインタ型は全てIntPtrで置き換えます。64ビットマシンではIntPtrは8バイトになるので、4バイト固定のC#のintなどで置き換えないように。次に、ogg_int64_tは文字通り64ビット符号付き整数なので、Int64を使います。また、VC++のintやlongは4バイトです──C#のlongは8バイトなので、間違えないでください。うっかり勘違いしないように、敢えてエイリアスを使わずビット数が判るようにしてみました。その他、OggSyncStateやOggStreamStateはこの後で定義する構造体です。クラスでは参照にしかならないので、領域をきちんと確保できません。構造体ならC/C++と同じように場所を取ってくれます。

そして、頭のStructLayoutAttribute。このクラスのメンバが書いた通りに並ぶように、LayoutKind.Sequentialを指定しています。これを書かないと、アライメント調整によって生まれるパディングを極力減らす為に、メンバの並び替えを勝手にやってくれる(やってしまう?)ようです。C/C++の配置と全く同じようにする為に、きちんと書いておきます。

ちなみに、普通C/C++コンパイラでは、CPUがデータに効率よくアクセスできるように、2バイトのメンバは2バイト境界に、4バイトなら4バイト境界に、8バイト以上は8バイト境界に配置します。C#のコンパイラも同様のことをします。この為、構造体メンバのアライメント調整をどうするかの設定がずれていると正しくメンバにアクセスできず、パディングを掴まされてしまいます。ただ、今回の場合、OggVorbis.dllをビルドする時に構造体メンバのアライメント(/Zp)を既定値のままにしましたし、.NETでもその既定値は同じ8バイトですので、メンバを書き順通りに並べること以外に気にする必要はありません。詳しく知りたい人は、/Zp (構造体メンバの配置) (C++)align (C++)辺りを読んでみて下さい。

ま、今回の場合アライメントは「ちなみに」でいいです。アンマネージド関数と構造体をやりとりする時にはとりあえずLayoutKind.Sequential、と、それだけ。次行きましょう。

[StructLayout( LayoutKind.Sequential )]
private struct OggSyncState {
  IntPtr data;
  Int32 storage;
  Int32 fill;
  Int32 returned;
  Int32 unsynced;
  Int32 headerbytes;
  Int32 bodybytes;
}

ogg_sync_stateに相当するOggSyncStateです。ちゃんとOggVorbisFileの中に実体を持たなければならないので、structで宣言します。これは場所稼ぎ(領域確保)の為のものでしかないので、構造体ごとprivateでいいでしょう。次はちょっと特殊です。

[StructLayout( LayoutKind.Sequential )]
private struct OggStreamState {
  IntPtr body_data;
  Int32 body_storage;
  Int32 body_fill;
  Int32 body_returned;
  IntPtr lacing_vals;
  IntPtr granule_vals;
  Int32 lacing_storage;
  Int32 lacing_fill;
  Int32 lacing_packet;
  Int32 lacing_returned;
  OggStreamStateHeader header;
  Int32 header_fill;
  Int32 e_o_s;
  Int32 b_o_s;
  Int32 serialno;
  Int32 pageno;
  Int64 packetno;
  Int64 granulepos;
}
[StructLayout( LayoutKind.Sequential, Size = 282 )]
private struct OggStreamStateHeader {
  IntPtr header;
}

ogg_stream_state用のOggStreamStateですが、OggStreamStateHeaderという構造体がおまけで付いています。これを使っているOggStreamStateのメンバheaderは、include\ogg\ogg.hで次のように定義されています:

  // ……
  unsigned char header[282];
  // ……

見ての通りです。C#の配列は、配列コンテナが参照型である為、そのままでは282バイトOggStreamStateの中に埋め込むことが出来ません。そこで、282バイトの大きさを持つOggStreamStateHeader構造体を作り、この構造体をメンバに持つことで282バイトの領域を確保します(地道にlong header1; long header2;……なんてやってられませんしね)。ちなみに、このようにSizeフィールドを使えば一々コピーして行かなくても手っ取り早く領域を確保できますが、個々のメンバにアクセスするのは面倒になりますし、サイズも予めC/C++でsizeof演算子を使って調べておく必要があります。ここまで見てきてもう面倒くさいって人はOggVorbisFileをこの手法で確保してしまえばよいですが、ここに挙げるクラスや構造体をそのままコピー&ペーストするのと手間は大して違わないでしょうか。まぁ、次です。

[StructLayout( LayoutKind.Sequential )]
private struct VorbisDSPState {
  Int32 analysisp;
  IntPtr vi;
  IntPtr pcm;
  IntPtr pcmret;
  Int32 pcm_storage;
  Int32 pcm_current;
  Int32 pcm_returned;
  Int32 preextrapolate;
  Int32 eofflag;
  Int32 lW;
  Int32 W;
  Int32 nW;
  Int32 centerW;
  Int64 granulepos;
  Int64 sequence;
  Int64 glue_bits;
  Int64 time_bits;
  Int64 floor_bits;
  Int64 res_bits;
  IntPtr backend_state;
}

ogg_dsp_state用です。大したことは無いですね。次。

[StructLayout( LayoutKind.Sequential )]
private struct VorbisBlock {
  IntPtr pcm;
  OggPackBuffer opb;
  Int32 lW;
  Int32 W;
  Int32 nW;
  Int32 pcmend;
  Int32 mode;
  Int32 eofflag;
  Int64 granulepos;
  Int64 sequence;
  IntPtr vd;
  IntPtr localstore;
  Int32 localtop;
  Int32 localalloc;
  Int32 totaluse;
  IntPtr reap;
  Int32 glue_bits;
  Int32 time_bits;
  Int32 floor_bits;
  Int32 res_bits;
  IntPtr internal_p;
}
[StructLayout( LayoutKind.Sequential )]
private struct OggPackBuffer {
  Int32 endbyte;
  Int32 endbit;
  IntPtr buffer;
  IntPtr ptr;
  Int32 storage;
}

vorbis_blockです。oggpack_buffer用のOggPackBufferがおまけで付いてきますが、それくらいなもの。これで、構造体の定義は大体終わりました。

このアーカイブについて

このページには、過去に書かれたブログ記事のうちC#カテゴリに属しているものが含まれています。

次のカテゴリはC++/CLIです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。