Unmanaged C++の最近のブログ記事

概要: OpenMPを利用すると、さほど労力をかけずプログラムを並列化できるようです。自分ではまだ試していませんがかなり面白そう。

自分で頑張ってスレッド作って、複数スレッド間で頑張って同期したりしなくても、コード中にヒントを書いておけば勝手に並列化してくれる(と思う)OpenMPなんてのがあるそうです。概要掴むにはWikipediaにあるOpenMPの記事が役に立ちます。

当然ながらアプリケーションの複数スレッド間で資源が競合しないようにする為には元のソースコードに対する深い知識が必要ですが、OpenMPはコンパイラが使うミドルウェアといった感じのようです。コンパイラによるサポートが必要ですが、gccのC++コンパイラやVisual C++のclといった(独断と偏見によれば)メジャーなコンパイラではサポートされているようです。コンパイラは当然元のソースコードをよく知っていますから、うまく動くって訳ですね。ちなみにVisual C++の場合はVisual C++ の OpenMPを読むと良さそう。

こいつを知ったのは、最近OpenCVを入れたのですが、そこでOpenMPが使われていたからです。なるほど画像処理でも局所性が高いものが多いでしょう。スレッドを分ける要請は無いものの、CPU余ってるなら使いたいと。で、OpenMPという訳か。

ミドルウェアで思い出しましたが、Globusってのもあったなあ。こちらは複数台のコンピュータを使って一つのタスクをする(つまり、グリッドコンピューティングをする)為のミドルウェアだそうですが、前から入れよう入れよう思っているものの全然調べてない......。まあうちのとこのサーバ群も遊んでるマシンは少なくなってきてはいますが。

概要: あーやだやだ、なんでどれもこれも横幅もフォントサイズもピクセル固定のスタイルばかりなんでしょうか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に変えておいてください。暗黙の型変換が行われるので、労力は小さくて済むでしょう。

概要: 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#でOgg Vorbisを扱う場合、方法は二つあります: libogg及びlibvorbisの全てのソースコード(C言語で書かれている)を、C#に移植する。 オリジナルのソースコードに...

さて、C#でOgg Vorbisを扱う場合、方法は二つあります:

  1. libogg及びlibvorbisの全てのソースコード(C言語で書かれている)を、C#に移植する。
  2. オリジナルのソースコードにはそれ程手を加えずDLLをビルドし、プラットフォーム呼び出しを使う。

liboggもlibvorbisも、少なくともXiph.orgではC#版が公開されていないので、C#に移植する場合はバージョンが更新されるごとに書き直さなくてはなりません。迷わず後者を選びます(Windows環境でない場合は、移植が必要になるかも知れません。少なくとも以下の方法は使えません)。

まず、DLLビルド用のプロジェクトを作ります。以下ではVisual Studio 2005 Professionalを使っていますが、それ以外の人は異なる部分を適宜読み替えて下さい。

Visual C++ - Win32 - Win32 プロジェクト

マイドキュメント以下のVisual Studio 2005\Ogg Vorbisディレクトリにliboggやlibvorbisを展開したので、Ogg Vorbisというソリューション名にしました。また、liboggとlibvorbisを一つのDLLにまとめるので、Integratedというプロジェクト名に。ここは好きに変えて、読み替えて下さい。

この後、アプリケーションの設定を行います。

種類はDLL、空のプロジェクト

アプリケーションの種類をDLLにするのは必須ですが、空のプロジェクトにするかどうかはお好みで。

次に、このプロジェクトに追加するファイルをコピーします。インクルードパスなどを設定すれば必要は無いですが、書き換える部分もあるので元のソースを取っておきたい場合はコピーして下さい。

  • libogg-x.x.x\include\ogg ディレクトリを $(ProjectDir)include に
  • libogg-x.x.x\src ディレクトリの中身を $(ProjectDir)source\ogg に
  • libvorbis-x.x.x\include\vorbis ディレクトリを $(ProjectDir)Include に
  • libvorbis-x.x.x\lib ディレクトリの中身を $(ProjectDir)source\vorbis に

$(ProjectDir)は、「プロジェクトファイルがあるディレクトリ + パス区切り文字」に展開して下さい。上記のようにコピーすれば、次のような構造になるはずです:

マイドキュメント\Visual Studio 2005\Projects\Ogg Vorbis\Integrated
  include
    ogg
    vorbis
  source
    ogg
    vorbis

コピーが済んだら、プロジェクトにソースファイルを追加します。ヘッダファイルは追加しないままで構いません。拡張子cのファイルだけ追加したいので、「ソース ファイル」カテゴリのコンテキストメニュー - 「追加」 - 「既存の項目」でワイルドカード「*.c」を使うと楽です。また、追加するソースはsource\ogg\*.cとsource\vorbis\*.cの全てですが、今回はエンコードまでしないため、source\vorbis\vorbisenc.cは除外します(これを追加するとDLLのサイズが異様に大きくなります。大量のグローバル変数のせい?)。また、source\vorbis\barkmel.c、source\vorbis\psytune.c、source\vorbis\tone.cは不必要(というより、main関数を含んでいるので入れては駄目)なのでこれらも除外します。

今度は、プロジェクトの設定を行います。プロジェクトのコンテキストメニューから「プロパティ」を選んで設定ダイアログを開きます。最初は共通の設定を行うので、ダイアログ左上の構成から「すべての構成」を選びます。共通で設定すべき構成プロパティの項目とその設定値は次の通りです。

  • 全般 - 文字セット : 設定しない
  • 全般 - 出力ディレクトリ : $(ConfigurationName)
  • C/C++ - 全般 - 追加のインクルードディレクトリ : .\include
  • C/C++ - 詳細 - 呼び出し規約 : __stdcall (/Gz)
  • C/C++ - 詳細 - コンパイル言語の選択 : C コードとしてコンパイル (/TC)
  • リンカ - 全般 - 出力ファイル : $(OutDir)\OggVorbis.dll

呼び出し規約の変更は忘れずに

追加のインクルードディレクトリと呼び出し規約の変更は確実に行って下さい。特に呼び出し規約。関数のスタッククリアをどこでやるかを決めるもので、既定値の__cdeclでは呼び出し側で、__stdcallでは呼び出された関数内部で行います。既定値の__cdeclでも出来ないことはないですが、DLLのエクスポート関数は普通__stdcallですし、.NET Frameworkでは__stdcallの関数しか(多分)作れないので、__stdcallを使います。それ以外の項目の変更は念の為です。

共通の設定が終わったら、最適化に関する設定を行います。外から見れば得られる結果は変わりませんが、内部的には速くなるので、最大限に最適化する方が良いでしょう。構成は「Release」にして、次の項目を変更します。

  • C/C++ - 最適化 - 最適化 : 最大限の最適化 (/Ox)

また、SSE2が使えるCPU(Pentium4)では、次の項目も設定しておくと速くなるかも知れません。

  • C/C++ - コード生成 - 拡張命令セットを有効にする : ストリーム SIMD 拡張機能 2 (/arch:SSE2)

但し、これはコンパイルする環境ではなくDLLを呼び出す環境で(このDLLを使う.NET Frameworkアプリケーションが実行される環境で)SSE2が使えないといけません。SSE2が使えるかどうかの判定(cpuid)は使わないみたいです(毎回やってれば遅くなるし、最初に一度だけするようにするとサイズが倍加するからでしょう)。/arch:SSEも同様。使う場合には、プロセッサ別にバイナリを用意するか、端からPentium4以外のCPUを見捨てるかを選択する必要があります。一度に全てをデコードする必要がある場合はともかく、ストリーミングバッファに書き込む場合、バッファ更新のインターバルよりも十分短い時間でデコードできることが多いので、使わない方が楽でいいと思います。

ここまで出来たら、とりあえずビルドしてみましょう。精度を失う暗黙の型変換や、sprintf、fopenなどの古い関数をまだ使っている、といった警告が嫌と言うほど出ますが、Xiph.orgを信用して無視します。どうしても五月蠅いという場合、コンパイラのコマンドラインオプションに/wdXXXXを追加することで全て非表示に出来ます(その警告を出さないだけで勿論潜在的には残ったままです)。無視しても問題ない警告を消すには次のコマンドラインオプションを追加します:

  • /wd4244 /wd4267 /wd4305 /wd4996

無視できる警告を無視する

これら以外の警告やエラーは改善すべきです。

  • error C2440: '関数' : 'int (__stdcall *)(const void *,const void *)' から 'int (__cdecl *)(const void *,const void *)' に変換できません。
  • warning C4024: 'qsort' : の型が 4 の仮引数および実引数と異なります。

こういうエラーが幾つか出ると思います。理由は明らかに呼び出し規約の既定値を__cdeclから__stdcallに変えたせいですね。勿論ここで呼び出し規約の既定値を__cdeclに戻すわけにも行きません。暗黙的なキャストが駄目だからと言って、次のようなキャストをするのは問題外です:

qsort(sortpointer,n,sizeof(*sortpointer),
  (int (__cdecl *)(const void *,const void *))icomp);

確かにコンパイルエラーは出ませんが、スタックポインタの値がおかしくなる為、潜在的で修復の難しいバグを抱え込むことになります。正しい解決方法は、qsortに渡される比較関数のみを__cdeclに変えることです。エラー一覧でqsortに__stdcallの比較関数を渡してしまっている箇所を探し、比較関数の名前で右クリック - 「定義へ移動」で定義場所を探し、__cdeclキーワードを追加します。具体的には、

static int icomp(const void *a,const void *b);   // floor1.c
static int comp(const void *a,const void *b);    // lsp.c
static int apsort(const void *a, const void *b); // psy.c
static int sort32a(const void *a,const void *b); // sharedbook.c

の4つを

static int __cdecl icomp(const void *a,const void *b);
static int __cdecl comp(const void *a,const void *b);
static int __cdecl apsort(const void *a, const void *b);
static int __cdecl sort32a(const void *a,const void *b);

と変えればOKです。実際のソースコードではプロトタイプ宣言ではなく、関数の実体が書かれているのでそのままコピーはしないで下さい(セミコロンの位置には実際は左中括弧)。

ここまで来れば、コンパイルに通るようになるはずです。これでよし……と言いたいところですが、マネージドコードから使うにはこれでは不十分です。というのは、エラー処理が出来ないからです。sourceディレクトリにSetError.cというファイルを作り、プロジェクトに追加して次の内容をコピーして下さい:

// SetError.c
#include <stdlib.h>
#include <errno.h>
 
void SetError( int errorOccurred ) {
  if ( errorOccurred )
    _set_errno( EINVAL );
  else
    _set_errno( 0 );
 
  return;
}

引数errorOccurredが真であればグローバル変数errnoをEINVAL(=22)に、偽であれば0に設定するものです。後で使います。

これでソースコードの修正は完了です。しかし、この状態ではどの関数もエクスポートされていません。「dumpbin /exports OggVorbis.dll」とやれば、Summary以外に何も出力されないことが確認できるでしょう。エクスポートとは、DLLの関数を外に見えるようにすることを指します。エクスポートされた関数は、関数名、エントリポイント(関数のアドレス)、序数が公開され、外部から呼び出すことが出来るようになります。関数をエクスポートするには幾つか方法があるのですが、今回はモジュール定義ファイルを使用します。sourceディレクトリにOggVorbis.defというファイルを作り、(一応)プロジェクトに追加して次の内容をコピーして下さい:

;OggVorbis.def
LIBRARY
EXPORTS
ov_open_callbacks
ov_clear
ov_info
ov_read
ov_pcm_total
ov_pcm_seek
ov_pcm_tell
SetError

詳しいことは説明しませんが、EXPORTSに続けてエクスポートしたい関数の名前を書いていけばエクスポートできます。モジュール定義ファイルの詳細はモジュール定義 (.def) ファイルを参照して下さい。通常のデコードをする場合は上記以外の関数をエクスポートする必要はないでしょうが、もし必要そうなものがあればVorbisfile API Referenceなどを見ながら追加してください。このドキュメントはlibvorbis-x.x.x\docにも入っています。

最後に、このモジュール定義ファイルをリンカの入力に追加します。プロジェクトのプロパティダイアログを開き、「すべての構成」で次の項目を変更します:

  • リンカ - 入力 - モジュール定義ファイル : .\source\OggVorbis.def

これでプラットフォーム呼び出しに必要なDLLの完成です。お疲れさまでした。……勿論、モジュール定義ファイルを追加したらリビルドしておいて下さいね。

……しかし、一行でも空白行作ったらそれ以降p要素やbr要素を滅茶苦茶に入れてくれるMovable Typeさんってどうにかならないんでしょうか。

このアーカイブについて

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

前のカテゴリはSounDecoderです。

次のカテゴリはWPFです。

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