EternalWindows
WAVE / ループ再生と一時停止

WAVEをループ再生するには、再生が終了したタイミングを適切に捕らえ、 そこでwaveOutWriteを呼び出すことになります。 waveOutOpenにはコールバック機能に関する引数があるため、 これを利用することで再生の終了を検出することができます。

waveOutOpen(&hwo, WAVE_MAPPER, &wf, (DWORD_PTR)hwnd, 0, CALLBACK_WINDOW);

第6引数にCALLBACK_WINDOWを指定します。 これにより、第4引数にウインドウハンドルを指定することができ、 下記に示すメッセージがウインドウプロシージャに送られることになります。

メッセージ 説明
MM_WOM_OPEN waveOutOpenの呼び出しが成功した場合に、内部でポストされる。 wParamはWAVEデバイスのハンドル。lParamは0。
MM_WOM_DONE WAVEの再生が終了した場合や、waveOutResetが呼ばれた場合にポストされる。 wParamはWAVEデバイスのハンドル。lParamはwaveOutWriteに指定したWAVEHDR構造体のアドレス。
MM_WOM_CLOSE waveOutCloseの呼び出しが成功した場合に、内部でポストされる。 wParamはWAVEデバイスのハンドル。lParamは0。

上記の表から分かるように、再生が終了した場合はMM_WOM_DONEが送られます。 よって、このときにwaveOutWriteを呼び出すことになります。

続いて、再生されているWAVEを一時停止させる方法について見ていきます。 これは、waveOutPauseを呼び出すことで実現できます。

MMRESULT waveOutPause(
  HWAVEOUT hwo 
);

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

一時停止されたWAVEの再生は、waveOutRestartで再開することができます。

MMRESULT waveOutRestart(
  HWAVEOUT hwo 
);

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

今回のプログラムは、コールバック機能を利用して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};
	static BOOL     bPause = FALSE;
	
	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, (DWORD_PTR)hwnd, 0, CALLBACK_WINDOW) != 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_LBUTTONDOWN:
		bPause = !bPause;
		if (bPause)
			waveOutPause(hwo);
		else
			waveOutRestart(hwo);
		return 0;

	case MM_WOM_DONE:
		waveOutWrite((HWAVEOUT)wParam, (LPWAVEHDR)lParam, 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;
}

MM_WOM_DONEが送られた時にwaveOutWriteを呼び出すことで、ループ再生を実現しています。 wParamにはWAVEデバイスのハンドルが格納され、 lParamにはWAVEHDR構造体のアドレスが格納されているので、 それぞれキャストして使用することができます。 WM_LBUTTONDOWNではポーズ状態を表す変数の値を逆にし、 ポーズ状態になった場合はwaveOutPauseを、 ポーズ状態でなくなる場合はwaveOutRestartを呼び出すようにしています。

waveOutResetの呼び出しで、MM_WOM_DONEが送られるという点に注意してください。 たとえば、次のようにwaveOutResetの呼び出しを加える場合は、 MM_WOM_DONEの処理も変更することになります。

case WM_RBUTTONDOWN:
	bReset = TRUE;
	waveOutReset(hwo);
	return 0;

case MM_WOM_DONE:
	if (bReset)
		bReset = FALSE;
	else
		waveOutWrite((HWAVEOUT)wParam, (LPWAVEHDR)lParam, sizeof(WAVEHDR));
	return 0;

このコードでは、マウスの右ボタンを押した場合にwaveOutResetを呼び出すようにしています。 これにより、MM_WOM_DONEがポストされることになりますが、 この場合はwaveOutWriteを実行してはいけません。 waveOutResetを呼び出した理由は再生を停止するためですから、 waveOutWriteで実行する必要はありません。 よって、MM_WOM_DONEがwaveOutResetによって送られたものかを調べるべく、 bResetという静的変数を利用するようにしています。 ちなみに、WM_DESTROYにおけるwaveOutResetの呼び出しでは、 MM_WOM_DONEが実行されることはありません。 内部的にはMM_WOM_DONEがポストされているのですが、 PostQuitMessageによってメッセージループから抜けることになるため、 MM_WOM_DONEが取得されることはありません。 これは、waveOutCloseがポストするMM_WOM_CLOSEについても同じことです。

WAVEデバイスのキューについて

今回のプログラムでは、ループ再生を実装するためにMM_WOM_DONEでwaveOutWriteを呼び出していましたが、 場合によっては一瞬の音切れを感じることがあるかもしれません。 これは、再生が停止してからwaveOutWriteを呼び出すまでにわずかな空き時間が生じることが原因ですが、 決して解決策がないわけではありません。 WAVEデバイスは内部にキューというものを持っており、 そのキューにバッファ(WAVEデータ)が書き込まれている場合は、それを再生します。 バッファを書き込むのは、既に紹介しているwaveOutWriteです。 そして、再生が終了するとMM_WOM_DONEをポストし、 キューにバッファが書き込まれているかを確認します。 この確認時にバッファが書き込まれていれば直ぐに再生されることになりますから、 再生が終了する前に予めバッファを書き込んでおけば、 音切れが生じることはなくなります。 次に、コード例を示します。

#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[2] = {0};
	static BOOL     bReset = FALSE;
	static DWORD    dwBufferCount = 2;
	
	switch (uMsg) {

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

		for (i = 0; i < dwBufferCount; i++) {
			wh[i].lpData         = (LPSTR)lpWaveData;
			wh[i].dwBufferLength = dwDataSize;
			wh[i].dwFlags        = 0;

			waveOutPrepareHeader(hwo, &wh[i], sizeof(WAVEHDR));
			waveOutWrite(hwo, &wh[i], sizeof(WAVEHDR));
		}

		return 0;
	}

	case WM_RBUTTONDOWN:
		bReset = TRUE;
		waveOutReset(hwo);
		return 0;

	case MM_WOM_DONE:
		if (bReset) {
			if (--dwBufferCount == 0)
				bReset = FALSE;
		}
		else
			waveOutWrite((HWAVEOUT)wParam, (LPWAVEHDR)lParam, sizeof(WAVEHDR));
		return 0;
	
	case WM_DESTROY:
		if (hwo != NULL) {
			waveOutReset(hwo);
			waveOutUnprepareHeader(hwo, &wh[0], sizeof(WAVEHDR));
			waveOutUnprepareHeader(hwo, &wh[1], 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;
}

WAVEHDR構造体を2つ用意し、waveOutWriteを2回実行します。 1回目の呼び出しでは、キューにバッファが書き込まれて、それは直ちに再生することになります。 2回目の呼び出しでは、キューにバッファが書き込まれて、1回目に書き込んだバッファの再生が 終了して時点で直ちに再生されます。 こうした手法は、マルチバッファリングと呼ばれたりもします。 waveOutResetが実行された場合はMM_WOM_DONEがポストされるため、 これがバッファの数だけ送られてきて時点でリセットフラグをFALSEにするようにしています。

実をいうと、MM_WOM_DONEはループ再生のタイミングを知らせるためのメッセージではありません。 これは、アプリケーションにバッファが返されたことを示すためのメッセージであり、 その返されたバッファをlParamより参照できることになっているのです。 waveOutResetは、現在再生されているバッファとキューに格納されているバッファをアプリケーションに返しますから、 この関数によってMM_WOM_DONEがポストされるのは当然の事であるといえます。 キューにバッファが格納されているといった内部事情を詳しく知りたい場合は、 次のようにWAVEHDR.dwFlagsを確認してみるとよいでしょう。

関数 説明
waveOutPrepareHeader dwFlagsにWHDR_PREPAREDを加える。
waveOutWrite dwFlagsにWHDR_INQUEUEを加える。
waveOutReset dwFlagsからWHDR_INQUEUEを取り除き、WHDR_DONEを加える。
waveOutUnprepareHeader dwFlagsからWHDR_PREPAREDを取り除く。

waveOutWriteを呼び出した場合、WAVEHDR.dwFlagsにWHDR_INQUEUEが指定されます。 これは、現在バッファがキューに格納されていることを意味しています。 そして、再生が終了した場合やwaveOutResetが実行された場合は、 バッファがアプリケーションに返ってきますから、 このときのWAVEHDR.dwFlagsにはWHDR_INQUEUEが指定されていません。 また、バッファがアプリケーションに返ってきても、 WHDR_PREPAREDは取り除かれていませんから、 waveOutWriteの前にwaveOutPrepareHeaderを実行しておく必要はありません。



戻る