2006年10月アーカイブ

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

このアーカイブについて

このページには、2006年10月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2006年9月です。

次のアーカイブは2006年12月です。

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