ホーム < ゲームつくろー! < DirectX技術編


その2 wavファイルから音を抽出


 前章で見てきたように、DirectSoundで音を再生する事自体は極めて簡単です。しかし、その肝心の音データ(波形データ)を取り出すのが一苦労だったりします。波形データ自体はサンプリングレートとビットレートに従った単なる数字の羅列ですが、その数字の羅列はwavファイル内に埋没しています。それを確実に取り出す必要があるわけです。この章では、Win32API関数群を使ってwavファイルから波形データを抽出する方法を見ていく事にします。



@ wavファイル

 wavファイルはリフ(RIFF: Resource InterChange File Format)というフォーマット形式に従っています。これは「チャンク(Chunk: 塊)」と呼ばれるデータの一まとまりを並べて音の情報を整理しています。表にすると以下の通りです:

チャンク名 項目 サイズ(byte) データ
RIFFチャンク ChunkID 4 "RIFF"
ChunkSize 4 4
FormType 4 "WAVE"
RIFFチャンク内 fmtチャンク ChunkID 4 "fmt"
ChunkSize 4 **
WAVEFORMATEX構造体
dataチャンク ChunkID
ChunkSize
wavデータ

 Waveフォーマットの場合3つのチャンク(RIFFチャンク、fmtチャンク、dataチャンク)で構成されています。各チャンクはそれぞれChunkIDとChunkSizeそしてデータ部分(オレンジの枠)というサブ構成になっています。ChunkIDには何のチャンクであるかを表す4文字の文字が格納されています。ChunkSizeにはデータ部のサイズが示されています。
 RIFFチャンクはファイルの種類を表します。実は上の方式はWAVE以外の情報も表せます(マルチメディアフォーマット)。Waveファイルの場合はFormatTypeに「WAVE」と書き込まれています。fmtチャンクはそのファイルのフォーマット形式が格納されています。Waveの場合はWAVEFORMATEX構造体のデータがそのまま格納されています。dataチャンクに実際の波形データが存在します。
 DirectSoundで必要なのは太文字で示したWAVEFORMATEX構造体とwavデータです。この構造を頭に入れておきますと、次からの作業の意味が良くわかります。



A mmio関数群でデータを取り出す

 Win32API群でチャンクフォーマットを扱うにはmmio関数群(multimedia IO)を用います。これらを使うにはwinmm.libライブラリをプロジェクトに追加しmmsystem.hヘッダーファイルをインクルードする必要があります:

ライブラリとヘッダーインクルード
#pragma comment ( lib, "winmm.lib" )
#include <mmsystem.h>

 mmio関数群を用いてチャンクにアクセスしていくのが基本操作です。
 まず最初にwavファイルをオープンします。これはmmioOpen関数を用います:

ファイルオープン
HMMIO hMmio = NULL;
MMIOINFO mmioInfo;

// Waveファイルオープン
memset( &mmioInfo, 0, sizeof(MMIOINFO) );
hMmio = mmioOpen( filepath, &mmioInfo, MMIO_READ );
if( !hMmio )
    return false; // ファイルオープン失敗

 HMMIOはmmioを表すハンドルです。MMIOINFO構造体はメディア情報を表す構造体です。Waveファイルをオープンする時にはゼロで初期化したMMIOINFO構造体を渡す必要があります。これをしないとオープンは失敗します。mmioOpen関数の第1引数はwavファイル名です。第3引数にはオープン時の様々なフラグを設定します。この関数はwavファイルを作成するときにも使いますので、かなりなフラグ数があります。今回はデータを取り出すだけなのでMMIO_READを設定します。オープンに成功すると第2引数にそのWaveファイルのメディア情報が格納され、戻り値に有効なmmioハンドルが返ります。

 次にチャンクの情報を取得していきます。一番最初にあるのはルートにあたる「RIFFチャンク」です。ここから子のチャンクに入るにはmmioDecend関数を用います(Decend: 下る、降りる)。最初の図にありますが、チャンクは入れ子になっています。この関数は親チャンクから子チャンクへ降りようというわけです。

RIFFチャンクサーチ
// RIFFチャンク検索
MMRESULT mmRes;
MMCKINFO riffChunk;
riffChunk.fccType = mmioFOURCC('W', 'A', 'V', 'E');
mmRes = mmioDescend( hMmio, &riffChunk, NULL, MMIO_FINDRIFF );
if( mmRes != MMSYSERR_NOERROR ) {
    mmioClose( hMmio, 0 );
    return false;
}

 3行目にmmioFOURCC関数が使われています。wavファイルは「FOURCC」という4文字の文字列でチャンクの種類を区別しています。mmioDescend関数は基本的にこの文字列を見て探すチャンクを判断するため、これを設定しておく必要があるわけです。

 mmioDescend関数の第1引数はオープンしたHMMIOハンドルです。第2引数にはチャンクの情報を格納するMMCKINFO構造体へのポインタを渡します。第3引数は第2引数の親チャンクのチャンク情報を与えます。今はルートであるRIFFチャンクを探すのでここはNULLにします。第4引数は何をするか決めるフラグで、MMIO_FINDRIFFを指定するとRIFF情報を探してきてくれます。関数が成功すると見つけたチャンク情報がMMCKINFO構造体に格納され、MMSYSERR_NOERRORが返ります。

 riffChunkは今後親チャンク情報として使用するのでWaveファイルから音を抽出するまでとっておきましょう。

 次にriffChunk内にあるフォーマットチャンクを探します:

フォーマットチャンクサーチ
// フォーマットチャンク検索
MMCKINFO formatChunk;
formatChunk.ckid = mmioFOURCC('f', 'm', 't', ' ');
mmRes = mmioDescend( hMmio, &formatChunk, &riffChunk, MMIO_FINDCHUNK );
if( mmRes != MMSYSERR_NOERROR ) {
    mmioClose( hMmio, 0 );
    return false;
}

 探し方はRIFFチャンクの時とほとんど同じですが、MMCKINFO構造体のckidにフォーマットチャンクのFORUCCを与えないとmmioDescend関数に蹴られます。

 無事フォーマットチャンクが探せたらFormatChunkの中にチャンク情報が格納されます。フォーマットチャンクにはWAVEFORMATEX構造体が格納されているはずです。これを読み込むにはmmioRead関数を用います:

WAVEFORMATEX構造体格納
DWORD fmsize = formatChunk.cksize;
DWORD size = mmioRead( hMmio, (HPSTR)&waveFormatEx, fmsize );
if( size != fmsize ) {
    mmioClose( hMmio, 0 );
    return false;
}

 mmioRead関数の第3引数に読み込むデータサイズを指定する必要があります。今回はWAVEFORMATEX構造体になるわけですが、このサイズはすでにformatChunk.cksizeに格納されています。第2引数にWAVEFORMATEX構造体へのポインタを渡すのですがHPSTR型にキャストする必要があります。関数が成功すると読み込んだサイズが返ります。何らかの間違いで最初に指定した読み込みサイズ(fmsize)と差異があった場合は不正なのでエラーチェックをしておきます。

 これで音の情報も格納できました。続いて3つ目のデータチャンクから実際にデータを吸い出します。データチャンクはRIFFチャンクの下にありますが、現在はフォーマットチャンクにいるので一度RIFFチャンクに戻ってから改めてmmioDescend関数で降りることになります。上に戻るのはmmioAscend関数です:

WAVEFORMATEX構造体格納
mmioAscend( hMmio, formatChunk, 0 );

 データチャンクの検索はこれまでとほぼ同じです:

データチャンク検索
// データチャンク検索
MMCKINFO dataChunk;
dataChunk.ckid = mmioFOURCC('d', 'a', 't', 'a');
mmRes = mmioDescend( hMmio, &dataChunk, &riffChunk, MMIO_FINDCHUNK );
if( mmRes != MMSYSERR_NOERROR ) {
    mmioClose( hMmio, 0 );
    return false;
}

 これでデータチャンク情報が得られますので、そこから波形部分を抽出して一時メモリに格納しておきます:

音データ格納
char *pData = new char[ dataChunk.cksize ];
size = mmioRead( hMmio, (HPSTR)pData, dataChunk.cksize );
if(size != dataChunk.cksize ) {
    delete[] pData;
    return false;
}


ここまでエラー無く進めば、pDataに音のデータが格納されています。もうmmioハンドルは必要ないので閉じてしまいましょう:

ファイルクローズ
mmioClose( hMmio, 0 );

これで一般的なmmioによるwavファイルからの波形データ抽出は終了です。抽出した波形データをセカンダリバッファに格納すれば、音が鳴ります!



A でっかいwavにご用心

 以下はちょっと余談です。
 @の方法の最後で、音データのサイズを調べmmioRead関数で全部格納しました。しかし、wavファイルがとてつもなく大きい場合、メモリが確保できない場合があります。もちろん、確保できたかどうかをチェックする事も大切なのですが、そもそもすべてのデータを取り出す必要があるかは慎重に考えるべきでしょう。効果音のような小さなものであればほぼ問題はありません。しかしBGMの場合、無圧縮PCM音源だと数10MB〜100MBがあたりまえなので、これはメモリに読み込むべきではないでしょう。

 そういう「でっかいwevは分割して逐次的に読み込む」のが常套手段です。

 データチャンクを検索した段階で、ファイルポインタがデータの先頭に移動しています。mmioRead関数はそのファイルポインタを動かしデータを取得します。そこで、読み込むデータサイズをMaxから指定の大きさに変更します。

音データの部分格納
const DWORD readsize = 1024;
char pData[readsize];
pData = new char[readsize];
size = mmioRead( hMmio, (HPSTR)pData, readsize );
if(size <= 0){
    return;
}

curOffset += size;   // ファイルポインタのオフセット値

readsizeという変数に読み込み単位を格納しておいて、mmioReadで読み込みを行います。mmioReadは読み込んだサイズを返しますので0以下であれば終わりと判断できます。注目はcurOffsetで、これは音データからのオフセット値を格納しています。こうする事で、もう一度先頭から読み返さなくてはならなくなった時に、ファイルポインタを移動させる事が簡単にできるようになります。

 ファイルポインタを移動させるのはmmioSeek関数です。たとえば、

音データの先頭に移動
mmioSeek( hMmio, -curOffset, SEEK_CUR );
curOffset = 0;   // ファイルポインタを先頭に戻す

とすれば、データの先頭に戻ります。SEEK_CURというのが現在位置を基にするためのフラグです。もちろん、最初からチャンクのオフセットを計算していって、絶対ポジションを与えてもいっこうに問題ありません。これで事実上符号付き整数の大きさに相当するwavファイルが読み込めるようになります。また、お気付きかとは思いますが、これはストリーム再生の時にも必要となります。


 ここまでの基礎知識を用いて音を鳴らすサンプルプログラムはこちらです。