2006年9月アーカイブ

概要: ここで必要になる構造体の最後は、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さんってどうにかならないんでしょうか。

概要: BGMなどの長いサウンドを先頭から末尾まで全てメモリ上に展開すると、色々リソースを浪費するため、DirectSoundを使う場合はストリーミングバッファというものが使えます。これは、メモリ上に確保する...

BGMなどの長いサウンドを先頭から末尾まで全てメモリ上に展開すると、色々リソースを浪費するため、DirectSoundを使う場合はストリーミングバッファというものが使えます。これは、メモリ上に確保するバッファの長さは2秒分程度にして、バッファを再生していくにつれ内容を書き換えるという使用法を可能にするものです。よくウェブサイトから動画をダウンロードするとき「ストリーミング再生」という言葉を目にしますが、概念は同じです。特にデータサイズが大きい動画の場合、メモリ上に全てを展開することが不可能なこともあるでしょう。

そんな訳で、ストリーミング再生をすることは重要なのですが、用意する音声データの方もそれなりに問題になります。非圧縮PCMデータ(Waveファイル)の場合、サンプル当たり (チャンネル数) x (サンプリングサイズビット数 / 8) バイト消費します。CD並みの音質では2チャンネル16ビットのサンプルを44100Hzでサンプリングするので、例えば3分の曲では 2 x ( 16 / 8 ) x 44100 x ( 3 x 60 ) B = 31752000 B = 30.3 MB となる訳ですね。

こんなに容量が必要となると、迂闊にBGMとしてPCMデータを使えません。圧縮して配布する場合も、一般的な可逆圧縮では思うような圧縮率が得られませんし、使える状態(解凍済み)のデータはやっぱり大きいままです。

音声データを小さくするには、やはり音声データを圧縮することに特化した方法を使うのが一番です。それと、余程感度のいい耳を持っていない限り、多少の損失は判らない場合が多いので、可逆圧縮よりも非可逆圧縮をする方がいいでしょう。有名なものでMP3やWMAなどがありますが、今回は法律的制限が非常に緩やかなOgg Vorbisを使うことにします。

Ogg Vorbisとは、Xiph.orgで開発された、音声圧縮用のコーデックです。修正BSDライセンスで配布され、使用に際しては著作元の表示が義務づけられるのみで、ライセンス料を払ったり、ソースコードを開示したりする必要がありません。また、拡張子は大抵oggを使いますが、Oggというのはコンテナ名なので、Ogg Vorbis以外でも拡張子oggを持つことがあります。まぁ、難しい事は僕もよく知らないので、とにかくOgg Vorbisで書かれたデータをデコードしてPCMデータを得られるようにすることを目的にします。

とりあえずはOgg VorbisのSDKを用意しましょう。Xiph.org: Downloadsで、liboggとlibvorbisの安定化バージョンをダウンロードします(現時点ではlibogg 1.1.3、libvorbis 1.1.2が最新版)。これにはVC++ 6.0でビルドできるワークスペースファイルとプロジェクトファイルが同梱されています(win32ディレクトリ以下)。ただし、今回は使用しません。これを使うと確かに手軽に使えるようになるのですが、DLLのファイルサイズは大きくなります。それでもいいという人はVariousible - Developpers' Room - Ogg Vorbis SDKのビルドを参考にして下さい(これを書くに当たって参考にさせていただきました。感謝!)。VC++でスタティックリンクする場合には多分問題ないでしょう。その場合、libogg-x.x.xディレクトリをoggに、libvorbis-x.x.xディレクトリをvorbisにリネームしておくと楽です。

ちなみに、VC++ 6.0用プロジェクトをビルドしてそのDLL版を使う場合、必要なDLLはliboggのogg.dll、libvorbisのvorbisfile.dllとvorbis.dll(約1MB)の3つです。気付かないとはまりますが、vorbisfile_dynamicはvorbis_dynamicに依存しています

概要: 偶然ですが、Movable Type(略してMT)を導入したいという衝動に駆られる前は、Mersenne Twister(略してMT)のC#による実装をしていました。ちなみにMersenne Twis...

偶然ですが、Movable Type(略してMT)を導入したいという衝動に駆られる前は、Mersenne Twister(略してMT)のC#による実装をしていました。ちなみにMersenne Twisterというのは、松本眞さんと西村拓士さんによって開発された非常に高性能な擬似乱数ジェネレータのことです。

一通り実装し終えたので、機能的にオリジナルと等価であることを確かめた後、32ビット符号無し整数で乱数を与えるgenrand_int32()関数を使い、Pentium4 1.70GHzマシンでパフォーマンステストをしてみました。結果がこちら:

  • オリジナルC言語版 genrand_int32() : 1.718746e-008 seconds per call
  • 今回のC#版 genrand_int32() : 2.440930e-008 seconds per call

大体3/4程度のパフォーマンスですが、まぁマネージドコードにしては頑張ってる方でしょう。

次に、unsigned intを返すgenrand_int32()を4294967296.0で割って、[ 0.0, 1.0 )に均等に分布する実数乱数を得る関数、genrand_real2()でも試してみました。

  • オリジナルC言語版 genrand_real2() : 3.309488e-008 seconds per call
  • 今回のC#版 genrand_real2() : 7.074817e-008 seconds per call

......何、これ。doubleにキャストして4294967296.0で割っただけなのに、差が付きすぎじゃあありませんか? ちなみに、C言語版では最適化によってgenrand_real2()のインライン展開が行われていましたが、それをしなくても3.590222e-008 seconds per callの速さです。

こんな時は逆アセンブルに限ります。ildasmで覗いてみると、この関数の中では

  IL_0000:  ldarg.0
  IL_0001:  call       instance uint32 MersenneTwister::genrand_int32()
  IL_0006:  conv.r.un
  IL_0007:  conv.r8
  IL_0008:  ldc.r8     2.3283064365386963e-010
  IL_0011:  mul
  IL_0012:  ret

という処理が行われていることが判りました。どうも.NET Framework 2.0のSDKにはMSIL(CIL)のリファレンスがついていないようで、それのためだけにバージョン1.1のSDKを引っ張ってくるのも面倒なのでECMA Specs - Part 3 (CIL)を見てこれにコメントを振ると、

  IL_0000:  ldarg.0  // this参照をスタックにプッシュ
  IL_0001:  call       instance uint32 MersenneTwister::genrand_int32()
  // this.genrand_int32()の呼び出し
  IL_0006:  conv.r.un  // スタックの先頭の
  // genrand_int32()の戻り値を、uint32からfloatに変換
  IL_0007:  conv.r8  // スタックの先頭のfloat値をfloat64値に変換
  IL_0008:  ldc.r8     2.3283064365386963e-010  // 定数をスタックにプッシュ
  IL_0011:  mul  // スタックの先頭の二つを掛け合わせて
  IL_0012:  ret  // 制御を返す

ということのようです。ぱっと見、conv.r.unとconv.r8はだぶっているようにも見えるのですが。ちなみに、CLR 2.0からの新機能のような気がしますが、Visual Studio 2005ではこのコードの機械語版も見られます(素晴らしい!)。尤も、C++のプログラムを逆アセンブルするほど読みやすくはないのですが。で、そのアセンブリ語訳が:

00000015  mov         ecx,edi 
00000017  call        dword ptr ds:[00A78CBCh] 
0000001d  mov         esi,eax 
0000001f  xor         eax,eax 
00000021  mov         dword ptr [esp],esi 
00000024  mov         dword ptr [esp+4],eax 
00000028  fild        qword ptr [esp] 
0000002b  fstp        qword ptr [esp] 
0000002e  fld         qword ptr [esp] 
00000031  fmul        dword ptr ds:[00DB1934h] 
00000037  add         esp,8 
0000003a  pop         esi  
0000003b  pop         edi  
0000003c  ret

アセンブリ言語レベルの最適化に詳しいわけではないですが、0x2b~0x2eが無駄な手順を踏んでいることくらいすぐわかります。これはfst qword ptr [esp]で一発です。その前に、わざわざespの指すアドレスに値をストアしたところで、使われていません。それに、0x1d~0x28はmov dword ptr [esp], eax、fild dword ptr [esp]とやれば済む話です。

これは困ったものです。C#のコンパイラには任せておけないとMSILを自分で書き換えるのでは、折角生産性を高めるためにC#を使っている意味がありません。

...が、これには抜け道が存在しました。uint32を直接float64に変換するのは無理なようですが、一旦int32を介せばスムーズに変換できるようなのです。即ち、今までは

public double genrand_real2() {
  return this.genrand_int32() * ( 1.0 / 4294967296.0 );
}

としていたところを

public double genrand_real2() {
  return (int)( this.genrand_int32() >> 1 ) * ( 1.0 / 2147483648.0 );
}

で書き換えるのです。精度は1ビット落ちるものの、これで速度が手に入れば安いものです。これを再びコンパイルし、逆アセンブルしたところ、MSILの段階でconv.r.unが消え、機械語レベルでも

00000013  mov         ecx,edi 
00000015  call        dword ptr ds:[00A78CBCh] 
0000001b  mov         esi,eax 
0000001d  shr         esi,1 
0000001f  mov         dword ptr [esp],esi 
00000022  fild        dword ptr [esp] 
00000025  fmul        dword ptr ds:[00DB1924h] 
0000002b  pop         ecx  
0000002c  pop         esi  
0000002d  pop         edi  
0000002e  ret

と、望ましいコードが得られました。勿論、そのパフォーマンスは

  • 修正C#版 genrand_real2() : 3.681116e-008 seconds per call

と、オリジナルに迫る速度を弾き出してくれました。まぁ、Mersenne TwisterにはMMXやSSEを使って更に最適化されたバージョンもあるのですが、それらのことは考えないようにしましょう。これだけ出れば個人的には満足です。

今回の教訓 : 符号無し整数から浮動小数点数への直接の変換は行わないこと

概要: Movable Typeを導入してみました。突然「覚え書きを残しておくウェブログが欲しい!」という衝動に駆り立てられたもので。結果的に流行に乗せられているようでどうもすっきりしないのですが、折角設置し...

Movable Typeを導入してみました。突然「覚え書きを残しておくウェブログが欲しい!」という衝動に駆り立てられたもので。結果的に流行に乗せられているようでどうもすっきりしないのですが、折角設置してみたんで暫く使い勝手を見てみることにします。

Wikipediaによれば、ウェブログというのは、元々はウェブページにコメントを加えて紹介する形式のウェブサイトを指していたとのこと。だから、毎日つけなくても、内容が日記じゃなくてもウェブログと呼ぶことは出来そうです。尤も、内容がウェブに関連するものでなければそれはただのログという気もしますが、ウェブ上で公開しているログもきっとウェブログ。

そんな訳で、このウェブログはウェブの話題に限定しない覚え書きとして使おうと思っています。具体的には、日々のプログラミングにまつわるちょっとした話。半ば愚痴、半ば役立ち情報を目指してまったり進行します。

このアーカイブについて

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

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

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