EternalWindows
WAVE / WAVEフォーマットと再生位置

WAVEについての知識を深めるためには、WAVEフォーマットについて正しく理解しておく必要があります。 WAVEフォーマットはWAVEFORMATEX構造体で表され、次のように定義されています。

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

話を進めやすくするため、nSamplesPerSecから説明します。 これはサンプリングレートと呼ばれ、1秒間の音を表すためにどれだけの数値(サンプル)を要するかです。 たとえば、サンプリングレートが22050ならば22050個の数値が必要です。

BYTE sample[22050];

上のコードでsampleが1秒間の音を表すことになり、sample[0]は1サンプルとなります。 サンプル数はHzで表すため、上の例では22050Hz、または22.05kHzと表せます。 サンプリングレートは大きければ、大きいだけ音の精度は良くなり、 一般に用いられるサンプリングレートは、 上記のような22.05kHzや44.1kHzになります。

上記のsampleは、あくまで1秒間の音を再生するのに必要なサンプル数です。 再生時間が30秒のWAVEならば、次に示すだけのサンプルが必要です。

BYTE sample[22050 * 30];

サンプリングレートにおける1サンプルの意味、 つまり、sample[0]やsample[5]にどのような値が格納されているかは、特に気にする必要はありません。 重要なのは、sample[0]からsample[22050 - 1]までに0秒から1秒までの音が格納されているという考え方です。

続いて、メモリの使用量について考えていきます。 これは、1サンプル、つまり1つの数値を何バイトで表すかについて大きく変わります。 サンプリングレートが22.05kHzで、1サンプルのサイズが1バイトならば、 1秒間の音を再生するのに必要なサイズは22050となりますが、 1サンプルに2バイト必要とする場合はサイズが22050 * 2となります。 これを先の式のように表現すると、次のようになります。

WORD sample[22050];

1サンプルに必要なサイズは、wBitsPerSampleから確認できるようになっています。 このメンバは1サンプルを何ビットで表現するかを意味し、 8ビットを示す8か16ビットを示す16が格納されます。 16である場合は2バイトということなので、上記のようにWORD型で表現することができます。

メモリのサイズを増やすもう1つの要素として、nChannelsがあります。 nChannelsはチャンネルであり、1の場合はモノラル、2の場合はステレオを意味します。 ステレオの場合は2つのスピーカーからWAVEを出力することができます。 ステレオにおける1サンプルでは、片方のスピーカー用のサンプルに1バイト使用し、 もう片方のサンプルにも1バイト使用するため、合計2バイト必要になります。 この状態でさらにwBitsPerSampleが16である場合は、 1サンプルのサイズが2バイトになりますから、合計4バイト必要ということになり、 次のように表すことができます。

DWORD sample[22050];

これが最もサイズの大きくなる例といえます。 1サンプルに使用するバイト数がsizeof(DWORD)であることから、 1秒間に必要なサイズも22050 * sizeof(DWORD)というように増えることになります。 このような1秒間に必要なサイズは、必須平均データ転送レートと呼ばれ、 nAvgBytesPerSecに格納されています。

nBlockAlignは、1サンプルに使用するサイズが格納されています。 これまでの話から分かるように、このメンバには次の式で算出できる値が格納されています。

nBlockAlign = (wBitsPerSample / 8) * nChannels

たとえば、wBitsPerSampleが8でnChannelsが2であるならば、 nBlockAlignは2となり、1サンプルに2バイト必要ということが分かります。 また、この値とサンプリングレートを乗算すれば、 必須平均データ転送レートが求められることも分かります。

wFormatTagはフォーマットタグと呼ばれ、WAVEデータの形式を示す値が格納されています。 通常のWAVEファイルならばWAVE_FORMAT_PCMが格納されていますが、 RIFF/WAVE形式のMP3ファイルにはWAVE_FORMAT_MPEGLAYER3が格納されています。 この場合は、waveOutWriteを呼び出す前にMP3データをWAVEデータに変換する必要があるため、 wFormatTagはきちんと確認しておく必要があります。 また、WAVE_FORMAT_PCMの場合はWAVEファイルにPCMWAVEFORMAT構造体が格納されていますが、 RIFF/WAVE形式のMP3ファイルにはMPEGLAYER3WAVEFORMAT構造体が格納されています。 この構造体は、内部にWAVEFORMATEX構造体とその他のメンバを持っており、 その他のメンバの合計サイズがWAVEFORMATEX構造体のcbSizeに格納されています。

サンプリングレートや必須平均データ転送レートについて理解しておけば、 waveOutGetPositionで現在の再生位置を取得することができます。

MMRESULT waveOutGetPosition(
  HWAVEOUT hwo, 
  LPMMTIME pmmt,
  UINT cbmmt    
);

hwoは、WAVEデバイスのハンドルを指定します。 pmmtは、MMTIME構造体を指定します。 cbmmtは、pmmtのサイズを指定します。 MMTIME構造体は、次のようになります。

typedef struct mmtime_tag { 
    UINT wType; 
    union { 
        DWORD ms; 
        DWORD sample; 
        DWORD cb; 
        DWORD ticks; 
        struct { 
            BYTE hour; 
            BYTE min; 
            BYTE sec; 
            BYTE frame; 
            BYTE fps; 
            BYTE dummy; 
            BYTE pad[2] 
        } smpte; 
        struct { 
            DWORD songptrpos; 
        } midi; 
    } u; 
} MMTIME;

wTypeは、再生位置をどのような単位で取得するかを表す定数を指定します。 TIME_BYTESを指定した場合は、cbに現在までで再生したバイト数が格納され、 TIME_SAMPLESを指定した場合はsampleに現在までで再生したサンプル数が格納されます。 それ以外のメンバについては、midiStreamPositionを呼び出す場合に参照します。

今回のプログラムは、waveOutGetPositionを呼び出して現在の再生位置を取得し、 そこから現在の再生時間を算出します。

#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 WAVEFORMATEX wf = {0};
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD dwDataSize;
		
		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));
		
		SetTimer(hwnd, 1, 200, NULL);
		
		return 0;
	}
	
	case WM_TIMER: {
		TCHAR  szBuf[256];
		DWORD  dwSecond;
		MMTIME mmt;
		
		mmt.wType = TIME_SAMPLES;
		waveOutGetPosition(hwo, &mmt, sizeof(MMTIME));

		dwSecond = mmt.u.cb / wf.nSamplesPerSec;

		wsprintf(szBuf, TEXT("%02d:%02d"), dwSecond / 60, dwSecond % 60);
		SetWindowText(hwnd, szBuf);

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

			KillTimer(hwnd, 1);
		}

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

WM_CREATEでは、SetTimerを呼び出してタイマを作成しています。 これにより、周期的にWM_TIMERが送られることになり、 現在の再生時間を算出することができます。 WM_TIMERの処理は、次のようになっています。

mmt.wType = TIME_SAMPLES;
waveOutGetPosition(hwo, &mmt, sizeof(MMTIME));

dwSecond = mmt.u.sample / wf.nSamplesPerSec;

wTypeにTIME_SAMPLESを指定しているため、MMTIME.u.sampleが初期化されます。 ここに格納されるのは、現在までで再生したサンプル数であり、 それを1秒間に使用するサンプル数で割れば、 再生から何秒経過しているかが分ります。 ちなみに、TIME_BYTESを指定する場合は次のようになります。

mmt.wType = TIME_BYTES;
waveOutGetPosition(hwo, &mmt, sizeof(MMTIME));

dwSecond = mmt.u.cb / wf.nAvgBytesPerSec;

TIME_BYTESを指定した場合は、MMTIME.u.cbが初期化されます。 これは現在までで再生したバイト数を格納していますから、 1秒間におけるバイト数で割れば、 再生時間を秒単位で取得することができます。

再生位置の設定

waveOut関数には再生位置を設定する関数がありませんが、 必須平均データ転送レートの知識があれば、これは十分に実装可能です。 次のコードは、マウスの左ボタンが押された場合に、 5秒後の位置からWAVEを再生しようとします。

#include <windows.h>

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

MMRESULT waveOutSetPosition(HWAVEOUT hwo, LPWAVEHDR lpwh, LPBYTE lpWaveData, DWORD dwDataSize, LPMMTIME lpmmt);
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 DWORD        dwDataSize = 0;
	static HWAVEOUT     hwo = NULL;
	static WAVEHDR      wh = {0};
	static WAVEFORMATEX wf  = {0};
	
	switch (uMsg) {

	case WM_CREATE: {
		if (!ReadWaveFile(TEXT("sample.wav"), &wf, &lpWaveData, &dwDataSize))
			return -1;
		
		if (waveOutOpen(&hwo, WAVE_MAPPER, &wf, (DWORD)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_LBUTTONDOWN: {
		MMTIME mmt;

		mmt.wType = TIME_BYTES;
		mmt.u.cb  = 5 * wf.nAvgBytesPerSec;

		waveOutSetPosition(hwo, &wh, lpWaveData, dwDataSize, &mmt);

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

MMRESULT waveOutSetPosition(HWAVEOUT hwo, LPWAVEHDR lpwh, LPBYTE lpWaveData, DWORD dwDataSize, LPMMTIME lpmmt)
{
	if (lpmmt->wType != TIME_BYTES)
		return MMSYSERR_INVALFLAG;

	waveOutReset(hwo);
	waveOutUnprepareHeader(hwo, lpwh, sizeof(WAVEHDR));

	lpwh->lpData         = (LPSTR)lpWaveData + lpmmt->u.cb;
	lpwh->dwBufferLength = dwDataSize - lpmmt->u.cb;
	lpwh->dwFlags        = 0;

	waveOutPrepareHeader(hwo, lpwh, sizeof(WAVEHDR));
	waveOutWrite(hwo, lpwh, sizeof(WAVEHDR));

	return MMSYSERR_NOERROR;
}

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

WM_LBUTTONDOWNで呼び出しているwaveOutSetPositionが、再生位置を設定する自作関数となります。 この関数は、WAVEHDR構造体やWAVEデータなどを受け取り、 新しく設定したい再生位置をMMTIME構造体で受け取ります。 5に必須平均データ転送レートを掛ければ、5秒再生するのに必要なバイト数を算出できるため、 これを関数内部で利用することになります。

MMRESULT waveOutSetPosition(HWAVEOUT hwo, LPWAVEHDR lpwh, LPBYTE lpWaveData, DWORD dwDataSize, LPMMTIME lpmmt)
{
	if (lpmmt->wType != TIME_BYTES)
		return MMSYSERR_INVALFLAG;

	waveOutReset(hwo);
	waveOutUnprepareHeader(hwo, lpwh, sizeof(WAVEHDR));

	lpwh->lpData         = (LPSTR)lpWaveData + lpmmt->u.cb;
	lpwh->dwBufferLength = dwDataSize - lpmmt->u.cb;
	lpwh->dwFlags        = 0;

	waveOutPrepareHeader(hwo, lpwh, sizeof(WAVEHDR));
	waveOutWrite(hwo, lpwh, sizeof(WAVEHDR));

	return MMSYSERR_NOERROR;
}

5秒後の位置から再生を行うということは、WAVEHDR.lpDataにWAVEデータの先頭アドレスを指定するわけにはいきません。 指定するのは、5秒後の位置の先頭でければならないため、WAVEデータに5秒後の位置を足すようにしています。 また、dwBufferLengthにはWAVEデータのサイズを指定しますが、 こについてもWAVEデータ全体のサイズを指定してはいけません。 全体のサイズから既に再生済みとするサイズを引いた値を指定する必要があります。 waveOutPrepareHeaderやwaveOutWriteの前にwaveOutResetを呼び出すのは、 現在再生されているWAVEを停止するためです。 これが停止され、直ちにWAVEを再生し始めることによって、 再生位置が変更されたように聴き取れることになります。



戻る