EternalWindows
WAVE / WAVEの再生

マルチメディアアプリケーションの開発において、 何よりも大きな壁と成り得るのは、音声についての知識が必要になる点であると思われます。 ここで述べている音声とはデジタル化された波形データのことであり、 このデータはサンプリングレートやビット数など複雑な概念が絡んで作成されています。 特に波形の形によって、音量や音のスピードがどのように変化するかは、 windowsとは全く別の話題であり、高度な専門知識が要求されます。 本章で取り上げるWAVEは、PCM(無圧縮)の波形データを格納したファイル形式のことであり、 WAVEを再生するとは、こうした波形データを再生することを意味しています。 つまり、ときとして音声に関する深い知識が要求されることがあります。

波形データを指定してWAVEを再生するには、 低水準なWindows Multimedia APIであるwaveOut関数を呼び出すことになります。 実際のところ、WAVEを再生したいだけであればMCIを使用するのが簡単ですが、 波形データを加工してWAVEを再生したい場合は、waveOut関数を呼び出すことになります。 また、MP3など何らかの規格でエンコードされた波形データをデコードした場合は、 そのデコードされた波形データをwaveOut関数で再生することになります。

waveOut関数を呼び出すには、まずwaveOutOpenでWAVEデバイスをオープンすることになります。

MMRESULT waveOutOpen(
  LPHWAVEOUT phwo,      
  UINT_PTR uDeviceID, 
  LPWAVEFORMATEX pwfx,      
  DWORD_PTR dwCallback,
  DWORD_PTR dwCallbackInstance,
  DWORD fdwOpen    
);

phwoは、WAVEデバイスのハンドルを受け取る変数のアドレスを指定します。 uDeviceIDは、オープンしたいWAVEデバイスのIDを指定します。 WAVE_MAPPERを指定した場合は、デフォルトのデバイスがオープンされます。 pwfxは、WAVEデータのフォーマットを格納したWAVEFORMATEX構造体のアドレスを指定します。 dwCallbackは、コールバック機能を実装するためのハンドルを指定します。 ウインドウハンドルやスレッドハンドルを指定した場合は、 WAVEデータの再生が終了した場合にメッセージを受信することができます。 コールバック機能を利用しない場合は、0を指定します。 dwCallbackInstanceは、dwCallbackにwaveOutProc型コールバック関数を指定した場合に意味を持ちます。 それ以外の場合は、0を指定します。 fdwOpenは、WAVEデバイスにオープンに関する定数を指定します。 CALLBACK_WINDOWを指定した場合はdwCallbackにウインドウハンドルを指定することができ、 CALLBACK_THREADを指定した場合はdwCallbackにスレッドハンドルを指定することができます。

WAVEデバイスをオープンしたら、次にWAVEデータを準備することになります。 ここで言う準備とは、WAVEデータを物理メモリに常駐させるための作業です。 物理メモリ上のデータは時にページングファイルへと退避することがありますが、 こうした現象をWAVEの再生中に発生させないようにすることが目的です。 WAVEデータを準備するには、waveOutPrepareHeaderを呼び出します。

MMRESULT waveOutPrepareHeader(
  HWAVEOUT hwo,  
  LPWAVEHDR pwh, 
  UINT cbwh      
);

hwoは、WAVEデバイスのハンドルを指定します。 pwhは、WAVEデータなどを含むWAVEHDR構造体のアドレスを指定します。 cbwhは、pwhのサイズを指定します。

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

typedef struct wavehdr_tag { 
    LPSTR      lpData; 
    DWORD      dwBufferLength; 
    DWORD      dwBytesRecorded; 
    DWORD_PTR  dwUser; 
    DWORD      dwFlags; 
    DWORD      dwLoops; 
    struct wavehdr_tag * lpNext; 
    DWORD_PTR reserved; 
} WAVEHDR, *LPWAVEHDR; 

lpDataは、WAVEデータを指定します。 これは、バッファと呼ばれることもあります。 dwBufferLengthは、lpDataのサイズを指定します。 dwBytesRecordedは、録音したデータのバイト数が格納されます。 WAVEを再生する場合は、このメンバは使用しません。 dwUserは、アプリケーション固有の値を指定します。 たとえば、マルチバッファリング時におけるバッファのインデックスを指定することができます。 dwFlagsは、WAVEデータの現在の状態を表す定数がwaveOut関数によって指定されます。 ただし、dwLoopsを初期化する場合は、アプリケーションがWHDR_BEGINLOOPとWHDR_ENDLOOPを指定します。 dwLoopsは、ループ再生をする回数を指定します。 0を指定した場合は、自動でループ再生が行われることはありません。 lpNextは、予約されているためNULLで問題ありません。 reservedは、予約されているため0で問題ありません。

WAVEデータを準備したら、waveOutWriteでWAVEデータをデバイスに送ることができます。 これによって、WAVEデータが再生されることになります。

MMRESULT waveOutWrite(
  HWAVEOUT hwo,  
  LPWAVEHDR pwh, 
  UINT cbwh      
);

hwoは、WAVEデバイスのハンドルを指定します。 pwhは、WAVEHDR構造体のアドレスを指定します。 cbwhは、pwhのサイズを指定します。

再生しているWAVEを停止したい場合は、waveOutResetを呼び出します。

MMRESULT waveOutReset(
  HWAVEOUT hwo  
);

hwoは、WAVEデバイスのハンドルを指定します。

不要になったWAVEデータを開放する場合は、 事前にwaveOutUnprepareHeaderを呼び出しておく必要があります。

MMRESULT waveOutUnprepareHeader(
  HWAVEOUT hwo,  
  LPWAVEHDR pwh, 
  UINT cbwh      
);

hwoは、WAVEデバイスのハンドルを指定します。 pwhは、WAVEHDR構造体のアドレスを指定します。 cbwhは、pwhのサイズを指定します。

WAVEの再生が終了したり停止した場合は、 waveOutCloseでWAVEデバイスを閉じることができます。

MMRESULT waveOutClose(
  HWAVEOUT hwo  
);

hwoは、WAVEデバイスのハンドルを指定します。

今回のプログラムは、waveOut関数を使用してWAVEを再生します。

#include <windows.h>

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

BOOL ReadWaveFile(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE *lplpData, LPDWORD lpdwDataSize);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static LPBYTE   lpWaveData = NULL;
	static HWAVEOUT hwo = NULL;
	static WAVEHDR  wh = {0};
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD        dwDataSize;
		WAVEFORMATEX wf;
		
		if (!ReadWaveFile(TEXT("sample.wav"), &wf, &lpWaveData, &dwDataSize))
			return -1;
		
		if (waveOutOpen(&hwo, WAVE_MAPPER, &wf, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR) {
			MessageBox(NULL, TEXT("WAVEデバイスのオープンに失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}

		wh.lpData         = (LPSTR)lpWaveData;
		wh.dwBufferLength = dwDataSize;
		wh.dwFlags        = 0;

		waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR));
		waveOutWrite(hwo, &wh, sizeof(WAVEHDR));
		
		return 0;
	}
	
	case WM_DESTROY:
		if (hwo != NULL) {
			waveOutReset(hwo);
			waveOutUnprepareHeader(hwo, &wh, sizeof(WAVEHDR));	
			waveOutClose(hwo);
		}

		if (lpWaveData != NULL)
			HeapFree(GetProcessHeap(), 0, lpWaveData);

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

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;
}

WAVEを再生するためには、WAVEフォーマットとWAVEデータが必要になります。 当然ながら、これらはWAVEファイルに格納されているため、 ReadWaveFileという自作関数で取得するようにしています。 第2引数がWAVEフォーマットであり、 第3引数でWAVEデータ、第4引数でWAVEデータのサイズとなります。 この関数の内部実装については、マルチメディア入力の章にて説明しています。 取得したWAVEフォーマットはwaveOutOpenの第3引数に指定され、 これによりデバイスのオープンに成功することになります。 今回はコールバック機能を利用しないため、 第6引数にCALLBACK_NULLを指定し、第4引数と第5引数に0を指定します。 waveOutWriteを呼び出すまでの処理は次のようになっています。

wh.lpData         = (LPSTR)lpWaveData;
wh.dwBufferLength = dwDataSize;
wh.dwFlags        = 0;

waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR));
waveOutWrite(hwo, &wh, sizeof(WAVEHDR));

WAVEHDR構造体であるwhにWAVEデータとそのサイズを指定します。 また、dwFlagsについては0に初期化しておきます。 構造体の初期化が終了すればwaveOutPrepareHeaderでWAVEデータを準備し、 その後にwaveOutWriteでWAVEを再生します。 waveOutPrepareHeaderを呼び出さずに、waveOutWriteを呼び出した場合は失敗することになります。

WAVEデバイスを閉じる処理などは、次のようになっています。

if (hwo != NULL) {
	waveOutReset(hwo);
	waveOutUnprepareHeader(hwo, &wh, sizeof(WAVEHDR));
	waveOutClose(hwo);
}

if (lpWaveData != NULL)
	HeapFree(GetProcessHeap(), 0, lpWaveData);

waveOutUnprepareHeaderやwaveOutCloseを呼び出すには、 再生されているWAVEを事前にwaveOutResetで停止させておく必要があります。 そうでなければ、waveOutUnprepareHeaderやwaveOutCloseは失敗することになります。 既にWAVEデータの再生が終了している場合は、 waveOutResetは何も行わずに成功を示す値を返します。 WAVEデータは自作関数のReadWaveFileによって明示的に確保されているため、 不要になった場合は明示的に開放することになります。


戻る