EternalWindows
マルチメディア入力 / WAVEの読み取り

優れたマルチメディアプレイヤーを開発するための1つの要素として、 数多くのファイル形式に対応することは異論のないところであると思われます。 しかし、これを満たすためには各ファイル形式を理解していなければならず、 対応するファイルの数が多いほど大変な作業となります。 そこで、こうした形式の差異をできるだけを隠蔽するための 形式としてRIFF(Resource Interchange File Format)があります。 RIFF形式は、データがファイル上のどこに格納されているかを抽象化しているため、 アプリケーションが目的のデータを取得しやすくなるという利点があります。 実際のところ、この取得したデータを理解するためには、 やはりファイル固有の知識が必要になるのですが、 データの取得を統一的に行えるという点で、RIFF形式の存在は大きいといえます。 次に、ファイル形式としてRIFFを採用しているファイルを示します。

ファイルの種類 フォームタイプ
WAVEファイル WAVE
RMIファイル RMID
AVIファイル AVI

RIFF形式のファイルの先頭4バイトは、RIFFという4文字が格納されています。 その後の4バイトはファイルサイズ - 8の値が格納され、 さらにその後の4バイトにはフォームタイプが格納されています。 フォームタイプとは、ファイルの種類を表す4文字のことです。 先頭のRIFFという4文字だけでは、ファイル形式がRIFFであることを特定できるだけであるため、 フォームタイプを通じてファイルの種類を確認できるようになっています。 フォームタイプやチャンクID(後述)に指定される文字列はFOURCCと呼ばれ、 それは必ず4文字で構成されていなければなりません。 たとえば、AVIファイルのフォームタイプは"AVI "であり、最後の1文字には空白が入ることになります。

RIFF形式の最大の特徴は、チャンクと呼ばれる概念を用いることで、 目的のデータの取得を容易にすることです。 本来ファイルから目的のデータを取得するためには、そのデータがファイル上のどこに存在するかを 把握しておかなければなりませんが、RIFF形式の場合はこれは不要です。 各データは特定のチャンクに格納されているため、 そのチャンクのチャンクIDを指定するだけで目的のデータを取得することができるのです。 例としてWAVEファイルにおけるチャンクの構造を示します。

RIFF形式に存在する全てのチャンクは、RIFFチャンクの中(下と表現することもできる)に存在します。 つまり、チャンクには階層構造が存在しています。 各チャンクの先頭4バイトにはチャンクIDが格納されており、 その後の4バイトはデータサイズ、そしてその後に実際のデータが格納されます。 WAVEファイルには、fmt という名前(4文字目は空白)を持ったチャンクが存在し、 そこにはWAVEファイルのフォーマットが格納されています。 一方、dataチャンクには、再生に使用されるWAVEデータが格納されています。 なお、RIFFファイルの先頭4バイトのRIFFというのは、RIFFチャンクのチャンクIDです。

特定のチャンクからデータを取得するには、そのチャンクにディセンドする必要があります。 ディセンドとは、チャンクの中に入る(進入する)という意味ですが、特に難しく考える必要はありません。 チャンクというのは概念的なものであり、実際にはファイルに階層構造が存在しているわけではないからです。 ファイル内のデータはあくまでバイト列であり、ディセンドによって行われるのは、 目的のデータが存在する場所までファイル位置が移動するだけです。 逆に、元の位置へと戻す操作はアセンドと呼ばれています。 チャンクにディセンドするには、mmioDescendを呼び出します。

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

hmmioは、マルチメディアファイル入出力のハンドルを指定します。 lpckは、ディセンドするチャンクを表したMMCKINFO構造体を指定します。 lpckParentは、NULLを指定して問題ありません。 wFlagsは、RIFFチャンクにディセンドする場合にMMIO_FINDRIFFを指定し、 LISTチャンクにディセンドする場合にMMIO_FINDLISTを指定します。 それ以外のチャンクにディセンドする場合は0を指定します。

チャンクからアセンドするには、mmioAscendを呼び出します。

MMRESULT mmioAscend(
  HMMIO hmmio,      
  LPMMCKINFO lpck,  
  UINT wFlags       
);

hmmioは、マルチメディアファイル入出力のハンドルを指定します。 lpckは、アセンドするチャンクを表したMMCKINFO構造体を指定します。 wFlagsは、予約されているため0を指定します。

MMCKINFO構造体は、次のように定義されています。

typedef struct { 
  FOURCC ckid; 
  DWORD  cksize; 
  FOURCC fccType; 
  DWORD  dwDataOffset; 
  DWORD  dwFlags; 
} MMCKINFO; 

ckidは、チャンクを識別するFOURCCを指定します。 mmioDescendの第3引数に0を指定した場合は、このメンバを初期化します。 cksizeは、チャンクのサイズが格納されます。 fccTypeは、フォームタイプまたはリストタイプを表すFOURCCを指定します。 mmioDescendの第3引数にMMIO_FINDRIFFやMMIO_FINDLISTを指定した場合は、 このメンバを初期化します。 dwDataOffsetは、ディセンドやアセンドを行った後のファイル上での位置が格納されます。 dwFlagsは、原則として0が格納されます。

MMCKINFO構造体では、FOURCCをFOURCC型で表現しています。 この型を取得するには、mmioStringToFOURCCを呼び出します。

FOURCC mmioStringToFOURCC(
  LPCTSTR sz,  
  UINT wFlags 
);

szは、FOURCCに変換したい文字列を指定します。 wFlagsは、0またはMMIO_TOUPPERを指定します。

今回のプログラムは、WAVEファイルからフォーマットとWAVEデータを取得します。 また、フォーマットの一部を表示します。

#include <windows.h>

#pragma comment (lib, "winmm.lib")

BOOL ReadWaveFile(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE *lplpData, LPDWORD lpdwDataSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR        szBuf[256];
	LPBYTE       lpData;
	DWORD        dwDataSize;
	WAVEFORMATEX wf;

	if (!ReadWaveFile(TEXT("sample.wav"), &wf, &lpData, &dwDataSize))
		return 0;
	
	wsprintf(szBuf, TEXT("サンプリングレート %d, チャンネル %d, ビット %d"), wf.nSamplesPerSec, wf.nChannels, wf.wBitsPerSample);
	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	
	HeapFree(GetProcessHeap(), 0, lpData);

	return 0;
}

BOOL ReadWaveFile(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE *lplpData, LPDWORD lpdwDataSize)
{
	HMMIO    hmmio;
	MMCKINFO mmckRiff;
	MMCKINFO mmckFmt;
	MMCKINFO mmckData;
	LPBYTE   lpData;

	hmmio = mmioOpen(lpszFileName, NULL, MMIO_READ);
	if (hmmio == NULL) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}
	
	mmckRiff.fccType = mmioStringToFOURCC(TEXT("WAVE"), 0);
	if (mmioDescend(hmmio, &mmckRiff, NULL, MMIO_FINDRIFF) != MMSYSERR_NOERROR) {
		MessageBox(NULL, TEXT("WAVEファイルではありません。"), NULL, MB_ICONWARNING);
		mmioClose(hmmio, 0);
		return FALSE;
	}

	mmckFmt.ckid = mmioStringToFOURCC(TEXT("fmt "), 0);
	if (mmioDescend(hmmio, &mmckFmt, NULL, MMIO_FINDCHUNK) != MMSYSERR_NOERROR) {
		mmioClose(hmmio, 0);
		return FALSE;
	}
	mmioRead(hmmio, (HPSTR)lpwf, mmckFmt.cksize);
	mmioAscend(hmmio, &mmckFmt, 0);
	if (lpwf->wFormatTag != WAVE_FORMAT_PCM) {
		MessageBox(NULL, TEXT("PCMデータではありません。"), NULL, MB_ICONWARNING);
		mmioClose(hmmio, 0);
		return FALSE;
	}

	mmckData.ckid = mmioStringToFOURCC(TEXT("data"), 0);
	if (mmioDescend(hmmio, &mmckData, NULL, MMIO_FINDCHUNK) != MMSYSERR_NOERROR) {
		mmioClose(hmmio, 0);
		return FALSE;
	}
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, mmckData.cksize);
	mmioRead(hmmio, (HPSTR)lpData, mmckData.cksize);
	mmioAscend(hmmio, &mmckData, 0);

	mmioAscend(hmmio, &mmckRiff, 0);
	mmioClose(hmmio, 0);

	*lplpData = lpData;
	*lpdwDataSize = mmckData.cksize;

	return TRUE;
}

ReadWaveFileという自作関数が、WAVEファイルからフォーマットとデータを取得する関数となります。 第1引数は読み取りたいWAVEファイルの名前であり、第2引数はWAVEFORMATEX構造体のアドレスです。 この構造体が、WAVEにおけるフォーマットになります。 第3引数はWAVEデータを受け取るポインタのアドレスと、データのサイズを受け取る変数のアドレスです。 これらのデータは今回のプログラムでは利用しませんが、 実際にWAVEを再生する場合は必要になります。 関数の内部を順に見ていきます。

mmckRiff.fccType = mmioStringToFOURCC(TEXT("WAVE"), 0);
if (mmioDescend(hmmio, &mmckRiff, NULL, MMIO_FINDRIFF) != MMSYSERR_NOERROR) {
	MessageBox(NULL, TEXT("WAVEファイルではありません。"), NULL, MB_ICONWARNING);
	mmioClose(hmmio, 0);
	return FALSE;
}

このコードは、RIFFチャンクにディセンドする処理です。 fmt チャンクやdataチャンクはRIFFチャンクの下に存在するため、 これらのチャンクのデータを取得するためには、まずRIFFチャンクの中に入る必要があります。 RIFFチャンクにディセンドする場合は、mmioDescendにMMIO_FINDRIFFを指定し、 MMCKINFO.fccTypeを適切に初期化しておく必要があります。 今回、読み取り対象としているのはWAVEファイルであるため、フォームタイプはWAVEです。 よって、mmioStringToFOURCCにWAVEを指定しています。 フォーマットを取得する処理は次のようになっています。

mmckFmt.ckid = mmioStringToFOURCC(TEXT("fmt "), 0);
if (mmioDescend(hmmio, &mmckFmt, NULL, MMIO_FINDCHUNK) != MMSYSERR_NOERROR) {
	mmioClose(hmmio, 0);
	return FALSE;
}
mmioRead(hmmio, (HPSTR)lpwf, mmckFmt.cksize);
mmioAscend(hmmio, &mmckFmt, 0);
if (lpwf->wFormatTag != WAVE_FORMAT_PCM) {
	MessageBox(NULL, TEXT("PCMデータではありません。"), NULL, MB_ICONWARNING);
	mmioClose(hmmio, 0);
	return FALSE;
}

フォーマットはfmt チャンクに存在するため、まずはこのチャンクにディセンドしなければなりません。 このためには、MMCKINFO.ckidに目的のチャンクを示すFOURCCを指定し、 mmioDescendにMMIO_FINDCHUNKを指定します。 これで関数が成功した場合は、MMCKINFO.cksizeにチャンクのサイズが格納されているため、 この値の分だけファイルを読み取れば、フォーマットを取得できたことになります。 既に述べたようにWAVEファイルのフォーマットはPCMWAVEFORMAT構造体ですから、 mmioReadに指定するサイズはsizeof(PCMWAVEFORMAT)として問題ありません。 wFormatTagを調べているのは、dataチャンクに格納されているデータが 圧縮などされていないPCM形式のデータであるかどうかを確認するためです。 PCM形式のデータではない場合、再生する前に変換処理が必要となります。

fmt チャンクからフォーマットを取得したならば、同チャンクからアセンドする必要があります。 つまり、fmt チャンクから外に出て、RIFFチャンクに戻ることになります。 理由は、dataチャンクがfmt チャンクの下ではなく、RIFFチャンクの下に存在するためです。 fmt チャンクの中ではdataチャンクにディセンドすることができないため、 事前にmmioAscendでアセンドしておく必要があるのです。 dataチャンクのデータを取得する処理は、次のようになっています。

mmckData.ckid = mmioStringToFOURCC(TEXT("data"), 0);
if (mmioDescend(hmmio, &mmckData, NULL, MMIO_FINDCHUNK) != MMSYSERR_NOERROR) {
	mmioClose(hmmio, 0);
	return FALSE;
}
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, mmckData.cksize);
mmioRead(hmmio, (HPSTR)lpData, mmckData.cksize);
mmioAscend(hmmio, &mmckData, 0);

基本的な処理は、fmt チャンクのときと同様になります。 まず、dataチャンクを表すFORCCをMMCKINFO.ckidに指定し、 その後にMMIO_FINDCHUNKを指定してmmioDescendを呼び出します。 dataチャンクのサイズはWAVEデータのサイズですから、 この分のメモリを確保してmmioReadでデータを取得します。

fmt チャンクに格納されているのはPCMWAVEFORMAT構造体ですが、 コード上ではWAVEFORMATEX構造体で取得しようとしています。 これは、WAVEの再生時に必要なのがPCMWAVEFORMATではなくWAVEFORMATEXであるため、 WAVEFORMATEXで取得したほうが都合がよいからです。 PCMWAVEFORMATのメンバは、WAVEFORMATEXのメンバの一部に相当しますから、 WAVEFORMATEXで問題なく取得することができます。 ただし、あくまで格納されているのはPCMWAVEFORMAT構造体ですから、 mmckFmt.cksizeの箇所にsizeof(WAVEFORMATEX)を指定してはいけません。

WAVEファイルの作成

WAVEファイルの形式を理解しておけば、マルチメディアファイル入出力関数を使用してWAVEファイルを作成することができます。 次に、コード例を示します。

BOOL WriteWaveFile(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE lpWaveData, DWORD dwDataSize)
{
	HMMIO    hmmio;
	MMCKINFO mmckRiff;
	MMCKINFO mmckFmt;
	MMCKINFO mmckData;
	
	hmmio = mmioOpen(lpszFileName, NULL, MMIO_CREATE | MMIO_WRITE);
	if (hmmio == NULL)
		return FALSE;

	mmckRiff.fccType = mmioStringToFOURCC(TEXT("WAVE"), 0);
	mmioCreateChunk(hmmio, &mmckRiff, MMIO_CREATERIFF);

	mmckFmt.ckid = mmioStringToFOURCC(TEXT("fmt "), 0);
	mmioCreateChunk(hmmio, &mmckFmt, 0);
	mmioWrite(hmmio, (char *)lpwf, sizeof(PCMWAVEFORMAT));
	mmioAscend(hmmio, &mmckFmt, 0);

	mmckData.ckid = mmioStringToFOURCC(TEXT("data"), 0);
	mmioCreateChunk(hmmio, &mmckData, 0);
	mmioWrite(hmmio, (char *)lpWaveData, dwDataSize);
	mmioAscend(hmmio, &mmckData, 0);

	mmioAscend(hmmio, &mmckRiff, 0);
	mmioClose(hmmio, 0);

	return TRUE;
}

まず、mmioOpenにMMIO_CREATEとMMIO_WRITEを指定し、 ファイルを作成すると同時に書き込みオープンします。 次に、そのファイルにRIFFチャンクを作成するため、 MMIO_CREATERIFFを指定してmmioCreateChunkを呼び出します。 このとき、フォームタイプを表すfccTypeメンバにWAVEを指定しておきます。 続いて、mmioCreateChunkでfmt チャンクを作成し、 mmioWriteでWAVEFORMATEX構造体を書き込みます。 第3引数にsizeof(PCMWAVEFORMAT)としているため、 WAVEFORMATEX構造体のPCMWAVEFORMATに相当する部分のみが書き込まれ、 不要なメンバが書き込まれることはありません。 最後に、mmioCreateChunkでdataチャンクを作成し、 mmioWriteでWAVEデータを書き込みます。



戻る