ウェーブ ファイルの読み書き

ファイルからサンプルをメモリに読み込む方法、および、メモリ上のサンプルをファイルに書き込む方法について説明します。

注:下記のコードを実行するには、windows.h および mmsystem.h をインクルードし、winmm.lib をリンクする必要があります。

まず、一般的なウェーブ ファイルの構造について説明します。ウェーブ ファイルは Resource Interchange File Format (RIFF) というフォーマットに従っています。これはチャンクによって情報を構造化するもので、各チャンクは 4 文字の ASCII からなる ID 部と、ダブルワードのサイズ部と、そのサイズが示す大きさのデータ部の 3 部で構成されています。また、ひとつのチャンクの中に 1 つ以上のチャンクを含むこともできます。含まれているチャンクは「サブチャンク」と呼ばれます。

RIFF では必ず最初に RIFF チャンクがあります。ウェーブ ファイルの場合、そのデータ部の先頭に WAVE という 4 文字が存在します。他に、サブチャンクとして fmt と data がデータ部に存在します。これ以外のサブチャンクはオプションなので、特に気にする必要はありません。次に fmt と data のデータ部の構造について説明します。

fmt はフォーマットを記録するもので、次の情報が最低限要求されます。

WORD  wFormatTag;
WORD  nChannels;
DWORD dwSamplesPerSec;
DWORD dwAvgBytesPerSec;
WORD  wBlockAlign;

これに追加される情報は wFormatTag によって異なります。例えば最も一般的な WAVE_FORMAT_PCM の場合、WORD wBitsPerSample が追加されています。

data はサンプルを記録するものですが、フォーマットによって手順が変化します。ここでは WAVE_FORMAT_PCM の場合について説明します。

モノラルの場合、サンプルは単純に時系列順に並んでいます。ステレオの場合、左チャネルと右チャネルとが交互に並んでいます。

また、量子化ビット数が 1 から 8 までの場合(通常 8 ビット)、各サンプルは 1 バイトの符号なし整数として記録されます。すなわち 0 以上 255 以下の数値を取ります。忘れやすいことですが、中心は 128 であり 0 ではないことに注意が必要です。

量子化ビット数が 9 以上の場合は、それを確保できる最小限の符号付整数として記録されます。例えば 9 から 16 の場合(通常 16 ビット)、各サンプルは 2 バイトの符号付整数として記録されます。すなわち -32768 以上 32767 以下の数値を取ります。

以上がファイル構造に関する説明です。以下では API について説明します。


読み込みで使用する API は以下です。

mmioOpen

open という名前のとおり、ファイルを開きます。

mmioClose

close という名前のとおり、ファイルを閉じます。

mmioRead

read という名前のとおり、ファイルの現在位置から指定されたバイト数をメモリに読み込みます。

mmioDescend

descend は「降りる」という意味です。指定された親チャンク内の現在のファイル位置から終端に向かって、指定された ID を持つサブチャンクを検索し、そのデータ サイズを取得します。したがってファイル位置はサブチャンクのデータ部の先頭に移動するので、続けて mmioRead を呼び出せばデータを取得できます。

mmioAscend

ascend は descend の対義語で「登る」という意味です。指定されたサブチャンクの終端にファイル位置を移動させ、親チャンクに戻ります。読み込みの場合はこれだけですが、書き込みの場合はサブチャンクのサイズ情報を修正するという作用もあります。これについては後述します。

mmioFOURCC

これはマクロですが、チャンクの ID を作成する時に使うと便利です。

上記の API を使用したコードを以下に示します。

void ReadFromWaveFile()
{
    HMMIO hmmio = NULL;   //ファイル ハンドル
    MMCKINFO mmckParent;  //RIFF チャンク用
    MMCKINFO mmckInfo;    //サブチャンク用

    WAVEFORMATEX wf;      //フォーマット情報
    DWORD dwDataSize = 0; //サンプルを保持するメモリのサイズ
    void* pvData = NULL;  //サンプルを保持するメモリへのポインタ


    //読み込み元のファイルを開く。
    hmmio = mmioOpen(TEXT("read_test.wav"), NULL, 
        MMIO_ALLOCBUF | MMIO_READ);

    //RIFF チャンクを探す。
    mmckParent.fccType = mmioFOURCC('W', 'A', 'V', 'E');
    mmioDescend(hmmio, &mmckParent, NULL, MMIO_FINDRIFF);

    //RIFF チャンク内で fmt サブチャンクを探す。
    mmckInfo.ckid = mmioFOURCC('f', 'm', 't', ' ');
    mmioDescend(hmmio, &mmckInfo, &mmckParent, MMIO_FINDCHUNK);

    //フォーマット情報を読み込む。
    //データ部のサイズは WAVEFORMATEX と一致しないかも知れないが問題ない。
    mmioRead(hmmio, (HPSTR) &wf, sizeof(WAVEFORMATEX));

    //fmt サブチャンクから RIFF チャンクに戻る。
    mmioAscend(hmmio, &mmckInfo, 0);

    //RIFF チャンク内で data サブチャンクを探す。
    mmckInfo.ckid = mmioFOURCC('d', 'a', 't', 'a');
    mmioDescend(hmmio, &mmckInfo, &mmckParent, MMIO_FINDCHUNK);
    dwDataSize = mmckInfo.cksize;

    //サンプル全体を保持するメモリを確保する。
    pvData = VirtualAlloc(NULL, dwDataSize, MEM_COMMIT, PAGE_READWRITE);

    //サンプルを読み込む。
    mmioRead(hmmio, (HPSTR) pvData, dwDataSize);

    //ファイルを閉じる。
    mmioClose(hmmio, 0);

    //この後、pvData を使って何かする。
    //8bit なら unsigned char* に、16bit なら short* にキャストする。

    //pvData を解放する。
    VirtualFree(pvData, 0, MEM_RELEASE);
}

実際のアプリケーションでは、サンプルに対してシーケンシャルでなくランダムな読み込みが必要になる場合もあると思います。その場合は mmioSeek という、名前のとおりシークする(ファイル位置を変える)関数を使用してください。


一方、書き込みで使用する API は以下です。

mmioOpen, mmioClose, mmioAscend

これらは読み込みと同じですが、Open のパラメータは書き込み用のものになります。また mmioAscend は追加の動作を行ないます。mmioCreateChunk の項目を参照してください。

mmioWrite

write という名前のとおり、メモリから指定されたバイト数をファイルの現在位置に書き込みます。

mmioCreateChunk

指定された ID を持つチャンクを、現在のファイル位置に作成します。まず 4 バイトの ID が書き込まれます。それから 4 バイトのサイズ情報が書き込まれますが、これは実際のサイズでなくて構いません。

この時点でファイル位置はデータ部の先頭にあるので、続けて mmioWrite を呼び出せばデータを書き込むことができます。データを全て書き込んだら mmioAscend を呼び出します。mmioAscend は呼び出された時点でのファイル位置がデータ部の終端であるとみなし、データ部のサイズを計算して先ほどのサイズ情報を修正します。

その後の動作ですが、データ部のサイズが偶数の場合は特にありません。しかし奇数の場合は、偶数にするために無意味な 1 バイトが追加されます。そしてファイル位置がそのバイトの次に移動します。この無意味なバイトはサイズ情報には含まれません。

上記の API を使用したコードを以下に示します。

void WriteToWaveFile()
{
    HMMIO hmmio = NULL;
    MMCKINFO mmckParent;
    MMCKINFO mmckInfo;

    //以下の 3 つは実際のデータを設定しておく必要がある。
    //ここでは動作確認のために、44100Hz, 16bit, 2ch のフォーマットを持つ、
    //簡単な 1 秒間の矩形波 (約 400Hz) を作成する。
    WAVEFORMATEX wf;
    DWORD dwDataSize = 0;
    void* pvData = NULL;

    wf.wFormatTag = WAVE_FORMAT_PCM;
    wf.cbSize = 0;
    wf.nSamplesPerSec = 44100;
    wf.wBitsPerSample = 16;
    wf.nChannels = 2;
    wf.nBlockAlign = wf.wBitsPerSample * wf.nChannels / 8;
    wf.nAvgBytesPerSec = wf.nBlockAlign * wf.nSamplesPerSec;

    dwDataSize = wf.nAvgBytesPerSec;
    pvData = VirtualAlloc(NULL, dwDataSize, MEM_COMMIT, PAGE_READWRITE);
    for (unsigned int i = 0; i < 44100; i++)
    {
        short v = ((short) (i / 55 % 2 * 2) - 1) * 8192;  //sample
        ((short*) pvData)[i * 2] = v;      //left channel
        ((short*) pvData)[i * 2 + 1] = v; //right channel
    }


    //出力先のファイルを作成する。
    hmmio = mmioOpen(TEXT("write_test.wav"), NULL, 
        MMIO_ALLOCBUF | MMIO_CREATE | MMIO_WRITE);

    //RIFF チャンクを作成する。
    mmckParent.fccType = mmioFOURCC('W', 'A', 'V', 'E');
    mmioCreateChunk(hmmio, &mmckParent, MMIO_CREATERIFF);

    //fmt サブチャンクを作成する。
    mmckInfo.ckid = mmioFOURCC('f', 'm', 't', ' ');
    mmioCreateChunk(hmmio, &mmckInfo, 0);

    //フォーマット情報を書き込む。
    mmioWrite(hmmio, (char*) &wf, sizeof(WAVEFORMATEX));

    //fmt サブチャンクのサイズ情報を修正し、RIFF チャンクに戻る。
    mmioAscend(hmmio, &mmckInfo, 0);

    //data サブチャンクを作成する。
    mmckInfo.ckid = mmioFOURCC('d', 'a', 't', 'a');
    mmioCreateChunk(hmmio, &mmckInfo, 0);

    //サンプルを書き込む。
    mmioWrite(hmmio, (char*) pvData, dwDataSize);

    //data サブチャンクのサイズ情報を修正し、RIFF チャンクに戻る。
    mmioAscend(hmmio, &mmckInfo, 0);

    //RIFF チャンクのサイズ情報を修正する。
    mmioAscend(hmmio, &mmckParent, 0);

    //ファイルを閉じる。
    mmioClose(hmmio, 0);

    //メモリを解放する。
    VirtualFree(pvData, 0, MEM_RELEASE);
}

以上です。