WAVEファイルを読み込む

概要:PCM形式のWAVEファイルを読み込む関数を作成します

WAVE の量子化ビット数は8ビットか16ビットです。
24ビットBMPのように扱いやすさを理由に32ビットに変換するような作業は必要ありません。
従って、ファイルの構造さえ理解してしまえば、純粋に読み込むだけです。

■RIFF形式

WAVEファイルはRIFF形式で保存されています。
RIFF形式は様々なチャンクから構成される汎用形式です。
チャンクとはデータの集合体であり、
チャンクの名前を表す4バイトの文字列から始まります。

WAVE ファイルの場合は、RIFF形式である事を表す "RIFF" という文字列から始まり、
ファイルの先頭から4〜7バイトにRIFF形式のバイト数、
ファイルの先頭から8〜11バイトにRIFF形式の種類を表す文字列 "WAVE" があります。

RIFF形式のバイト数は通常、ファイルのバイト数-8 です。
そうじゃない場合もあるみたいですが、たぶん大丈夫です。

そしてその後ろに「fmtチャンク、factチャンク、dataチャンク」が続きます。
factチャンクは通常存在せず、重要な情報も格納していません。

■WAVEファイルの構造(PCM形式)

WAVEファイルの構造はfactチャンクの有る無しで2種類に分類できます。

factチャンクが無い(通常)

アドレス データ
0 "RIFF"
+4 (ファイルのバイト数)-8
+8 "WAVE"
+12 "fmt "
+16 フォーマット情報(PCMWAVEFORMAT構造体)のバイト数
+20 フォーマット情報(PCMWAVEFORMAT構造体)
+36 "data"
+40 波形データのバイト数
+44 〜 波形データ

factチャンクが有る(特殊)

アドレス データ
0 "RIFF"
+4 (ファイルのバイト数)-8
+8 "WAVE"
+12 "fmt "
+16 フォーマット情報(PCMWAVEFORMAT構造体)のバイト数
+20 フォーマット情報(PCMWAVEFORMAT構造体)
+36 "fact"
+40 factチャンクのバイト数
+44 全サンプル数
+48 "data"
+52 波形データのバイト数
+56 〜 波形データ

fmtチャンクを表す文字列 "fmt " の4バイト目は半角空白である事に注意して下さい。

WAVEデータ作成に必要なのはPCMWAVEFORMAT構造体と波形データですが、
他のデータも様々なチェックに使用します。
factチャンクには重要な情報は格納されていないので無視します。

!修正事項!

波形データの先頭アドレスなどは、表のように固定値ではなく、
フォーマット情報のバイト数、factチャンクのバイト数、によって変動します。

修正したプログラムはこちらです。
これは「簡単ノベルゲームクリエイター1.3」で使用しているファイルです。

■PCMWAVEFORMAT構造体

PCMWAVEFORMAT構造体は実質的に
WAVEFORMATEX構造体の最後のメンバ WORD cbSize が無いだけで、他は全く同じです。

○PCMWAVEFORMAT構造体

typedef struct {
  WAVEFORMAT wf;
  WORD wBitsPerSample;
} PCMWAVEFORMAT;


typedef struct {
  WORD wFormatTag;
  WORD nChannels;
  DWORD nSamplesPerSec;
  DWORD nAvgBytesPerSec;
  WORD nBlockAlign;
} WAVEFORMAT;


○WAVEFORMATEX構造体

typedef struct {
  WORD wFormatTag;
  WORD nChannels;
  DWORD nSamplesPerSec;
  DWORD nAvgBytesPerSec;
  WORD nBlockAlign;
  WORD wBitsPerSample;
  WORD cbSize;
} WAVEFORMATEX;


■WAVEファイルを読み込む関数

PCM形式のWAVEファイルを読み込んで、WAVEデータを作成する関数を作ります。

関数の名前: LoadWaveFile
char *lpFileName:ファイルの名前を指すポインタ
WAVEFORMATEX *lpwfe:データを格納するWAVEFORMATEX構造体を指すポインタ
WAVEHDR *lpwhdr:データを格納するWAVEHDR構造体を指すポインタ
戻り値:0(成功) or 負値(失敗)

int LoadWaveFile(char *lpFileName,WAVEFORMATEX *lpwfe,WAVEHDR *lpwhdr)
{
    ……
}

■ファイルオープン&オールコピー

ファイルを開いて、その全てをメモリ領域にコピーします。
メモリ領域にコピーするのは「ファイルに直接アクセスするのは不便だから」です。

    HANDLE fh=CreateFile(lpFileName,GENERIC_READ,0,NULL,
        OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if(fh==INVALID_HANDLE_VALUE){
        MessageBox(NULL,"ファイルを開けません",lpFileName,MB_OK);
        return -1;
    }
    DWORD dwFileSize=GetFileSize(fh,NULL);
    BYTE *lpBuf=(BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,dwFileSize);
    DWORD dwReadSize;
    ReadFile(fh,lpBuf,dwFileSize,&dwReadSize,NULL);
    CloseHandle(fh);

■ファイル形式のチェック

ファイルの先頭から8〜11バイトに "WAVE" という文字列があればWAVEファイルであると判断できます。

    char str[4];
    strncpy(str,(char*)(lpBuf+8),4);
    if(strncmp(str,"WAVE",4)){
        HeapFree(GetProcessHeap(),0,lpBuf);
        MessageBox(NULL,"WAVEファイルを指定して下さい",lpFileName,MB_OK);
        return -2;
    }

■フォーマット情報のコピー

フォーマット情報(WAVEの属性)をWAVEFORMATEX構造体にコピーします。
ファイルにはPCMWAVEFORMAT構造体の形で保存されていますが、
最後のメンバ WORD cbSize が無いだけで、他は同じです。

PCMWAVEFORMAT構造体のサイズは16バイトです。
ファイルの先頭から16〜19バイトにフォーマット情報のバイト数が格納されていますが、
PCM形式の場合は16バイトに決まっています。
もし16バイトじゃなければPCM形式ではない事になるので、後々のチェックに引っ掛かるでしょう。

ちなみに、コピー先であるWAVEFORMATEX構造体のサイズは20バイトなので、
sizeof(WAVEFORMATEX) としてはいけません。

    CopyMemory(lpwfe,lpBuf+20,16);    // フォーマット情報をコピー

■WAVE形式のチェック

WAVEファイルである事はわかりました。
形式はどうでしょうか?
形式はWAVEFORMATEX構造体の wFormatTag メンバに格納されています。

    if(lpwfe->wFormatTag != WAVE_FORMAT_PCM){
        HeapFree(GetProcessHeap(),0,lpBuf);
        MessageBox(NULL,"PCM形式のWAVEファイルを指定して下さい",lpFileName,MB_OK);
        return -3;
    }

■ファイルの先頭から波形データの先頭までのバイト数

ファイルの先頭から36〜39バイトには "fact" か "data" があります。
factチャンクには必要な情報はありませんが、factチャンクの有無で後々のアドレスが違ってくるので、
ファイルの先頭から波形データの先頭までのバイト数を設定します。

    strncpy(str,(char*)(lpBuf+36),4);
    int offset;
    if(!strncmp(str,"fact",4)) offset=56;         // factチャンクが有る
    else if(!strncmp(str,"data",4)) offset=44;    // dataチャンク(factチャンクが無い)
    else{
        HeapFree(GetProcessHeap(),0,lpBuf);
        MessageBox(NULL,"正体不明のファイルです",lpFileName,MB_OK);
        return -4;
    }

■波形データをコピー

ファイルの先頭から波形データの先頭までのバイト数である offset の4バイト前には
波形データのバイト数が格納されています。
波形データのバイト数を取得&保存したら、
このサイズ分の新たなメモリ領域を確保して波形データをコピーしましょう。
波形データのコピーが終了したらファイル全体をコピーしたメモリ領域は解放してしまいます。

新しく確保しているのはメモリ領域の解放の時を考えての事です。
関数を呼び出した側が取得するポインタは
波形データの先頭を指すポインタです(WAVEHDR構造体のlpDataメンバ)。
当然、メモリ領域の解放にもこのポインタを使うでしょうが、
このポインタはメモリ領域の先頭を指していません。
メモリ領域の先頭を指していないポインタでメモリ領域を解放する事はできないのです。
もちろん関数の詳細を知っていれば先頭のアドレスを計算できますが、関数の使いやすさは大幅減です。

新しいメモリ領域には含まれない波形データのバイト数は
以前のメモリ領域を解放する前に保存しておく必要がある事に注意して下さい。
WAVEHDR構造体の dwBufferLength メンバの設定がまだ残っているからです。

    // 波形データをコピー
    DWORD size=*(DWORD*)(lpBuf+offset-4);
    BYTE *lpWave=(BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,size);
    CopyMemory(lpWave,lpBuf+offset,size);
    HeapFree(GetProcessHeap(),0,lpBuf);

■WAVEHDR構造体の設定

WAVEHDR構造体の設定はいつも通りです。

    lpwhdr->lpData=(LPSTR)lpWave;
    lpwhdr->dwBufferLength=size;
    lpwhdr->dwFlags=WHDR_BEGINLOOP | WHDR_ENDLOOP;
    lpwhdr->dwLoops=1;

    return 0;

■WAVEファイルの終了処理

WAVEファイルの終了処理を行う関数も作っておきましょう。
メモリ領域を解放する関数は取得した関数によって決まっています。
しかしそれは関数の呼び出し側に意識させるべきではありません。
メモリ領域の解放処理が伴う場合は専用の関数を用意するべきでしょう。

関数の名前:CloseWaveFile
HWAVEOUT hWaveOut:ウェーブフォームオーディオ出力デバイスのハンドル
WAVEHDR *lpwhdr:クリーンアップするデータブロックを識別するWAVEHDR構造体を指すポインタ

void CloseWaveFile(HWAVEOUT hWaveOut,WAVEHDR *lpwhdr)
{
    waveOutReset(hWaveOut);
    waveOutUnprepareHeader(hWaveOut,lpwhdr,sizeof(WAVEHDR));
    waveOutClose(hWaveOut);
    if(lpwhdr->lpData){
        HeapFree(GetProcessHeap(),0,lpwhdr->lpData);
        lpwhdr->lpData=NULL;
    };
}

■WAVEファイルを読み込んで再生する

LoadWaveFile関数を使ってWAVEファイルを読み込み、再生するプログラムは以下のようになります。

#include<windows.h>
#include"WaveFileIO.h"

// winmm.lib をリンクする
#pragma comment(lib,"winmm")

LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
    WAVEFORMATEX wfe;
    static WAVEHDR whdr;
    static HWAVEOUT hWaveOut;

    switch(uMsg){
        case WM_CREATE:
            LoadWaveFile("Windows XP Startup.wav",&wfe,&whdr);
            waveOutOpen(&hWaveOut,WAVE_MAPPER,&wfe,0,0,CALLBACK_NULL);
            waveOutPrepareHeader(hWaveOut,&whdr,sizeof(WAVEHDR));
            waveOutWrite(hWaveOut,&whdr,sizeof(WAVEHDR));
            return 0;
        case WM_DESTROY:
            CloseWaveFile(hWaveOut,&whdr);
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

非常にスッキリしていて、美しい!

★☆ ダウンロード ☆★


戻る / ホーム