カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

ウェーブファイルを読み込む(.NET C#バージョン)

このセクションでは、C#.NET環境下でウェーブファイルを読み込む手順について解説します。ここでは、リフ形式を支援するmmioマルチメディアIO関数を用います。

リフ形式

ウェーブファイルのフォーマットは、マルチメディア情報を格納するために設計された、リソース交換ファイル(RIFF: Resource Interchange File Format)形式に沿っています。

リフ形式は、「チャンク」と呼ばれる単位から成っていて、ファイルは複数のチャンクが数珠つなぎになった構造をしています。一つのチャンクは、以下に示す4つの部分から成っています。

説明 データタイプ
チャンクのタイプを識別するコード FOURCC
チャンクのデータ部分のサイズ DWORD
チャンクデータ BYTE配列
1ByteのNULL文字 (データサイズが奇数の場合) BYTE

チャンクの先頭には、チャンクタイプと呼ばれるチャンクを識別するためのフィールドがあります。データタイプは、FOURCCと呼ばれ、4文字のアスキー文字がアドレス順に並んだものです。

次にDWORDでデータ部分のサイズが入ります。その後、実際のデータが続きます。もしデータ部分が奇数の場合、1バイトのNULL文字を追加し、チャンク全体が偶数バイトになるようにします。

データサイズはNULL文字が追加された場合でも、本来のデータサイズ(奇数)を示します。ディスク等にセーブされるサイズは、データサイズ+1になります。

リフチャンク

リフ形式は、1つあるいは複数のチャンクから成りますが、特に最初のチャンクを「リフチャンク」と呼び、データの先頭部分にフォームタイプと呼ばれるフィールドがある構造をしています。チャンクのデータ部分のサイズは、フォームタイプと実際のデータのサイズを加えたものになります。

説明 データタイプ
チャンクタイプ「‘R’‘I’‘F’‘F’」 FOURCC
データサイズ(フォームタイプと実際のデータの合計) DWORD
チャンクデータ フォームタイプ FOURCC
実際のデータ BYTE配列
1ByteのNULL文字 (データサイズが奇数の場合) BYTE

フォームタイプによってどのような種類のファイルかを識別します。以下に代表的なものを示します。

フォームタイプ ファイルの種別
「‘W’‘A’‘V’‘E’」 ウェーブファイル
「‘A’‘V’‘I’‘ ’」 AVIファイル

サブチャンク

リフチャンクのデータ部分に別のチャンクを挿入し、入れ子状態にすることができます。挿入されたチャンクをサブチャンクと呼びます。

リフチャンクを親チャンク、サブチャンクを子チャンクと呼ぶこともあります。

ウェーブファイル

ウェーブファイルはリフ形式で保存されたサウンドファイルの一種です。ウェーブファイルは、ベースになる「リフチャンク」に、「フォーマットチャンク」と「データチャンク」、2つのサブチャンクが埋め込まれた構造をしています。

以下にウェーブファイルのイメージを示します。

フォーマットチャンク

フォーマットチャンクは、波形データがどのような属性を持っているのかを示す構造体が格納されます。

説明 データタイプ
チャンクタイプ「‘f’‘m’‘t’‘ ’」 FOURCC
ウェーブフォーマット構造体のサイズ DWORD
ウェーブフォーマット構造体 構造体
1ByteのNULL文字 (データサイズが奇数の場合) BYTE

ウェーブフォーマット構造体のサイズは一般に偶数なので、NULL文字が挿入されることはありません。

ウェーブフォーマット構造体の詳細は、セクション「ウェーブフォーマット」を参照してください。

データチャンク

データチャンクは、実際の波形データが格納されます。

説明 データタイプ
チャンクタイプ「‘d’‘a’‘t’‘a’」 FOURCC
波形データのサイズ DWORD
波形データ 配列
1ByteのNULL文字 (データサイズが奇数の場合) BYTE

波形データは、モノラルで分解能が8ビットのときに奇数になる可能性があります。

ウェーブファイルの構造

以下にウェーブファイル全体の構造を示します。

リフチャンク データタイプ
チャンクタイプ「‘R’‘I’‘F’‘F’」 FOURCC
データサイズ DWORD
リフチャンクのデータ フォームタイプ「‘W’‘A’‘V’‘E’」 FOURCC
 フォーマットチャンク チャンク
チャンクタイプ「‘f’‘m’‘t’‘ ’」 FOURCC
ウェーブフォーマット構造体のサイズ DWORD
ウェーブフォーマット構造体 構造体
 データチャンク チャンク
チャンクタイプ「‘d’‘a’‘t’‘a’」 FOURCC
波形データのサイズ DWORD
波形データ 配列
1ByteのNULL文字(波形データが奇数の場合) BYTE

データサイズは、フォーマットチャンクとデータチャンク、そしてフォームタイプ(FOURCC)のサイズを足し合わせた数値になります。これは、ファイル全体のサイズから、チャンクタイプ(FOURCC)とデータサイズ(DWORD)の領域、8バイト分を引いた数値に等しくなります。

ネット上に、データサイズを「ファイルサイズ-8」と表記している例が見られますが、これはウェーブファイルがリフチャンク1つだけから構成されているため、たまたまそうなっているだけです。本来の意味は、リフチャンクのデータ部分のサイズを示します。

マルチメディアファイル入出力API

ウェーブファイルの読み書きに、直接リフ形式を解釈することも可能です。しかし、Win32APIには、リフ形式のファイルを扱う専用関数群(mmio:multimedia IO)が用意されています。ここではそれらを利用した方が便利でしょう。mmio関数には、ファイルの基本操作に加え、チャンクを操作する関数が支援されています。

以下に、ウェーブファイルを読み込むときに用いる関数を示します。

関数名 説明
mmioOpen ファイルをオープンします
mmioClose ファイルをクローズします
mmioDescend チャンクに降ります
mmioAscend チャンクから抜けます
mmioRead データを読み込みます
mmioSeek 読み出し位置を変更します
mmioGetInfo ファイルの情報を取得します

ウェーブファイルオープンの流れ

以下にmmioマルチメディアIO関数を用いてウェーブファイルをオープンする手順を示します。

オープン・クローズ

mmioOpen()関数でウェーブファイルをオープンします。オープンで得られたハンドルを以降の処理で用います。すべての処理が終わったら、mmioClose()関数でハンドルをクローズします。

チャンク間の移動

ファイルをオープンしたらまずリフチャンクへ降ります。そこから必要に応じてフォーマットチャンクやデータチャンクへ降ります。処理が終わり、フォーマットチャンクやデータチャンクから抜けたら、最後にリフチャンクから抜けます。

チャンクデータを読み込むときは、目的のチャンクへ移動する必要があります。例えばウェーブフォーマットを読み込むときは、フォーマットチャンクへ移動します。チャンクへ移動の後、mmioRead()関数でデータを読み込みます。チャンクへの移動は、mmioDescend()関数を用います。

チャンク間の移動、例えば、フォーマットチャンクからデータチャンクへ移動するときは、いったん現在のチャンクから抜けた後、次のチャンクへ降ります。現在のチャンクから抜けるには、mmioAscend()関数を用います。

データの読み出し

データの読み出しは、mmioRead()関数を用います。データの途中から読み出したいときは、mmioSeek()関数で目的の位置へ移動した後、mmioRead()関数で読み出します。

コード例

ここでは全体の流れをつかむために、オープンからリードの準備までのコードを示します。

    public int OpenFile(string filePath)
    {
        MmioInfo    ioInfo     = new MmioInfo();
        MmckInfo    ciParChunk = new MmckInfo();
        MmckInfo    ciSubChunk = new MmckInfo();
        int         result;

        if (HMio != IntPtr.Zero)            return MmResult.NoError;

        FilePath = filePath;

        HMio = mmioOpen(filePath, ref ioInfo, Mmio.Read);
        if (HMio == IntPtr.Zero)            return (int)ioInfo.wErrorRet;

        ciParChunk.fccType = mmioStringToFOURCC("WAVE", 0);
        result = mmioDescend(HMio, ref ciParChunk, IntPtr.Zero, Mmio.FindRiff);
        if (result != MmResult.NoError)     return result;

        ciSubChunk.ckid = mmioStringToFOURCC("fmt", 0);
        result = mmioDescend(HMio, ref ciSubChunk, ref ciParChunk, Mmio.FindChunk);
        if (result != MmResult.NoError)     return result;

        int formatSize = (int)Math.Min(
            ciSubChunk.cksize, Marshal.SizeOf(typeof(WaveFormatEx)));
        int readSize = mmioRead(HMio, ref WaveFormat, formatSize);
        if (formatSize != readSize)         return MmIOErr.CanNotRead;

        result = mmioAscend(HMio, ref ciSubChunk, 0);
        if (result != MmResult.NoError)     return result;

        ciSubChunk.ckid = mmioStringToFOURCC("data", 0);
        result = mmioDescend(HMio, ref ciSubChunk, ref ciParChunk, Mmio.FindChunk);
        if (result != MmResult.NoError)     return result;

        DataOffset = mmioSeek(HMio, 0, Seek.Cur);
        DataSize = (int)ciSubChunk.cksize;
        CurPosition = 0;

        return MmResult.NoError;
    }

サンプルのOpenFile関数では以下のプロパティを用います。

    public IntPtr           HMio { get; set; }
    public string           FilePath { get; set; }
    public int              DataOffset { get; set; }
    public int              DataSize { get; set; }
    public int              CurPosition { get; set; }

    public WaveFormatEx     WaveFormat = new WaveFormatEx();

HMio

アンマネージ環境では「HMMIO」で示されるウェーブファイルのハンドルです。マネージ環境では、汎用ポインター「IntPtr」を用います。

このハンドルは、標準のファイルハンドル「FILE*」とは互換性がないので、標準のファイル関数に引数として渡してはいけません。

FilePath

オープンしたファイルのパスを保存します。

DataOffset

ウェーブファイルでウェーブデータが格納されているオフセット位置を記録します。任意の位置からウェーブデータを読み込むとき、基準点として用います。

DataSize

ウェーブデータのサイズです。最後まで読み込んだかどうかを判断するとき用います。

CurPosition

次に読み込むデータのオフセット位置を示します。

WaveFormat

現在開いているウェーブのウェーブフォーマット構造体です。

構造体

チャンク情報を保持するため以下の構造体を用います。アンマネージ環境で定義されている構造体は、マネージ環境では用いることができないため、マネージ環境用のものを改めて定義します。

以下に構造体のコード例を示します。

    [StructLayout(LayoutKind.Sequential, Pack=2, CharSet=CharSet.Auto)]
    public struct MmioInfo
    {
        // general fields
        public uint         dwFlags;        // general status flags
        public FourCC       fccIOProc;      // pointer to I/O procedure
        public IntPtr       pIOProc;        // pointer to I/O procedure
        public uint         wErrorRet;      // place for error to be returned
        public IntPtr       htask;          // alternate local task

        // fields maintained by MMIO functions during buffered I/O
        public int          cchBuffer;      // size of I/O buffer (or 0L)
        public IntPtr       pchBuffer;      // start of I/O buffer (or NULL)
        public IntPtr       pchNext;        // pointer to next byte to read/write
        public IntPtr       pchEndRead;     // pointer to last valid byte to read
        public IntPtr       pchEndWrite;    // pointer to last byte to write
        public int          lBufOffset;     // disk offset of start of buffer

        // fields maintained by I/O procedure
        public int          lDiskOffset;    // disk offset of next read or write
        public uint         adwInfo0;       // data specific to type of MMIOPROC
        public uint         adwInfo1;
        public uint         adwInfo2;
        public uint         adwInfo3;

        // other fields maintained by MMIO
        public uint         dwReserved1;    // reserved for MMIO use
        public uint         dwReserved2;    // reserved for MMIO use
        public IntPtr       hmmio;          // handle to open file
    }

    [StructLayout(LayoutKind.Sequential, Pack=2, CharSet=CharSet.Auto)]
    public struct MmckInfo
    {
        public FourCC       ckid;           // chunk ID
        public uint         cksize;         // chunk size
        public FourCC       fccType;        // form type or list type
        public uint         dwDataOffset;   // offset of data portion of chunk
        public uint         dwFlags;        // flags used by MMIO functions
    }
マネージ環境で用いる構造体の書き方は、セクション「マーシャリング」を参照してください。

ファイルをオープンする

mmioOpen

ファイルパスとMmioInfo構造体の参照を渡します。構造体への参照を示すために「ref」キーワードが必要です。mmioOpen関数に渡されるファイル名は、nullターミネートの文字列ですが、末尾のnullを含め最長128バイトに制限されています。オープンに失敗した時、MmioInfo構造体にエラーコードが戻ります。エラーコードが必要ない時は、nullを設定することができます。プログラム例では、読み取り専用でオープンすることを示す「Mmio.Read」を指定しています。

        HMio = mmioOpen(filePath, ref ioInfo, Mmio.Read);
        if (HMio == IntPtr.Zero)            return (int)ioInfo.wErrorRet;

C#では"mmsystem.h"などC/C++用のヘッダーファイルをインクルードできないため、必要なものをヘッダーファイルから抜き出し、C#のソースで改めて定義する必要があります。Mmioクラスでは、ここで用いる定数を定義しています。

    public class Mmio
    {
        public const int    Read        = 0x00000000;   // open file for reading only
        public const int    FindChunk   = 0x0010;       // mmioDescend: find a chunk by ID
        public const int    FindRiff    = 0x0020;       // mmioDescend: find a LIST chunk
        public const int    FindList    = 0x0040;       // mmioDescend: find a RIFF chunk
    }

.NET環境からWin32APIをコールすることを「プラットフォーム呼び出し」と言います。英語では「PInvoke」と言います。プラットフォーム呼び出しを行うためには、「DllImport」属性を用いた宣言文を書かなければなりません。以下にmmioOpenの宣言部分を示します。

プラットフォーム呼び出しについての詳しい解説は、セクション「プラットフォーム呼び出し」あるいは「マーシャリング」を参照してください。

この関数は、AnsiとUnicodeでコールされる関数が異なるため、EntryPointディレクティブで関数名を指定する必要があります。ここでは、AnsiタイプのmmioOpenAを指定しています。

    [DllImport("winmm.dll", EntryPoint="mmioOpenA")]
    public static extern IntPtr mmioOpen(
        string szFileName, ref MmioInfo lpmmioinfo, int dwOpenFlags);

リフチャンクを見つける

リフチャンクを見つける場合、親チャンクのパラメータ、MmckInfo構造体のメンバfccTypeにフォームタイプとして「WAVE」を設定します。通常mmioDescend()関数は、親チャンクと子チャンク、2つのパラメータを必要としますが、リフチャンクを見つける場合は、親チャンクのパラメータのみを設定します。

mmioDescend()関数にMmio.FindRiffフラグを設定します。ここではFourCCパラメータを作成するのに、mmioStringToFOURCC関数を使っています。

リフチャンクが見つからない場合、ウェーブファイルでない可能性があります。

フォーマットを読み込む

フォーマットチャンクを見つけるために、mmioDescend()関数に2つのMmckInfo構造体を渡します。1つは、リフチャンクを見つける時に使った、親チャンクを示すMmckInfo構造体、もう1つは、子チャンクを示すMmckInfo構造体です。子チャンク用のMmckInfo構造体のメンバckidにFourCCパラメータ「fmt 」をセットします。mmioDescend()関数に、Mmio.FindChunkフラグを設定します。

フォーマットチャンクに到達したら、mmioRead()関数を用いてデータを読み込みます。チャンクサイズは子チャンクのMmckInfo構造体のcksizeメンバに書かれています。mmioRead()関数は、読み込んだデータサイズが戻るので、それをエラーチェックに用いています。プログラム例では、WaveFormatにウェーブフォーマットを読み込んでいます。

ウェーブフォーマットを読み終わったら、フォーマットチャンクから抜けます。

データチャンクを見つける

次に、フォーマットチャンクを見つけた時と同じ要領で、FourCCパラメータに「data」をセットし、データチャンクを見つけます。これ以降、いつでもウェーブデータを読み出すことができます。

mmioDescend

チャンクへ降りる関数です。この関数は、C/C++では以下のように定義されています。ここで3番目のパラメータは、親チャンクを指定しないとき、NULLを指定することになっています。

MMRESULT mmioDescend(
    HMMIO       hmmio,
    LPMMCKINFO  lpck,
    LPMMCKINFO  lpckParent,
    UINT        uFlags);

構造体への参照はnullにすることができないため、mmioDescendを元の関数通り宣言すると、3番目のパラメータをnullにすることができません。

    [DllImport("winmm.dll")]
    public static extern int mmioDescend(
        IntPtr          hmmio,
        ref MmckInfo    lpck,
        ref MmckInfo    lpckParent,
        int             uFlags);

そこで対策として、3番目の変数をIntPtrとしたバージョンを定義します。3番目のパラメータをIntPtr.Zeroつまりnullにしたとき、下側の宣言が採用されます。C#はC/C++に比べ変数の型の管理が厳密なので、この例のように複数の宣言を必要とする場合があります。

    [DllImport("winmm.dll")]
    public static extern int mmioDescend(
        IntPtr          hmmio,
        ref MmckInfo    lpck,
        IntPtr          lpckParent,
        int             uFlags);

mmioAscend

チャンクから抜ける関数です。以下に宣言を示します。

    [DllImport("winmm.dll")]
    public static extern int mmioAscend(IntPtr hmmio, ref MmckInfo lpck, int uFlags);

mmioSeek

ファイルの読み出し位置を設定する関数です。データチャンクに降りたとき、以下の文でデータの先頭位置を取得しています。

        DataOffset = mmioSeek(HMio, 0, Seek.Cur);

ウェーブデータを読み込む

ウェーブフォーマットの読み取りと要領は全く同じです。ただウェーブデータはアプリケーションによって格納方法が異なりますので、仕様に合わせてコーディングする必要があります。

ウェーブデータを読み終わったら、データチャンクから抜けます。

ファイルのクローズ

ファイルをクローズします。mmioOpen()関数を用いてオープンしたハンドルは、必ずmmioClose()関数を用いてクローズしてください。ファイルをクローズしたことを示すために、ハンドルをnullに、ファイルのパスをクリアしています。

アプリケーションが終了しても、HMioハンドルは自動的にクローズされることはありません。オープンしたハンドルは必ずクローズするようにして下さい。

    public int CloseFile()
    {
        int result = MmResult.NoError;

        if (HMio != IntPtr.Zero)
        {
            result = mmioClose(HMio, 0);
        }
        HMio = IntPtr.Zero;
        FilePath = "";

        return result;
    }
ファイルを書き込みモードでオープンしたときは、クローズする前に、mmioAscend()関数で正しくサブチャンクおよびリフチャンクを抜ける必要があります。これによってチャンクサイズが正しくセットされます。しかし読み出しモードの場合は、読み出し位置がサブチャンク内にあっても、ファイルをクローズすることが可能です。

読み出し位置のシーク

スクロールバーを動かした時や、ループ再生などで、次に演奏する位置が変更になる場合があります。この場合、mmioSeek関数でファイルの読み出し位置を変更します。

読み出し位置を設定する時、1サンプリング点のデータが分割されないよう注意する必要があります。次のコードサンプルは、バイト単位で設定できますが、実際に運用するときは、1サンプリング点のデータサイズの整数倍の位置を指定しなければなりません。

    public bool SeekPosition(int position)
    {
        if (HMio == IntPtr.Zero)        return false;

        if (position < 0)               CurPosition = 0;
        if (DataSize < position)        CurPosition = DataSize;
        else                            CurPosition = position;

        mmioSeek(HMio, DataOffset + CurPosition, Seek.Set);
        return true;
    }

ウェーブデータの読み出し

最初にバッファ全体を無音データでクリアします。次にバッファが一杯になるまでデータを読み込みます。もし残っているデータがバッファサイズに満たないときは、残りのデータをすべて読み込みます。現在位置を更新し、読み込んだデータサイズを戻します。

mmioRead関数はエラーが発生すると「-1」が戻るため、Max関数を用いてゼロ以上の値が戻るようにしています。サンプルで示したReadData関数は、すべてのデータを読み込んだ状態と、エラーが発生した状態を区別していません。

    public int ReadData(ref byte[] dataBuff)
    {
        FillSilence(ref dataBuff);

        if (HMio == IntPtr.Zero)            return 0;
        if (DataSize <= CurPosition)        return 0;

        int readSize = Math.Min(dataBuff.Length, DataSize - CurPosition);
        CurPosition += readSize;
        readSize = mmioRead(HMio, dataBuff, readSize);

        return Math.Max(0, readSize);
    }

FillSilence関数は、バッファを無音データでクリアします。ウェーブの分解能が8ビットのとき、無音の値はゼロではないので注意が必要です。

    public void FillSilence(ref byte[] dataBuff)
    {
        byte silence = (WaveFormat.wBitsPerSample == 8) ? (byte)128 : (byte)0;

        for (int i=0; i<dataBuff.Length; i++)
        {
            dataBuff[i] = silence;
        }
    }

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ