EternalWindows
WAVE / 入力デバイスによる録音

コンピュータ上で再生されている音声は、WAVE入力デバイスを使用して録音することができます。 このデバイスに対してバッファを追加した場合、そのバッファに録音中のデータが随時書き込まれていくため、 これをファイルに保存すればよいことになります。 WAVE入力デバイスは、waveInOpenでオープンすることになります。

MMRESULT waveInOpen(
  LPHWAVEIN phwi,           
  UINT uDeviceID,           
  LPWAVEFORMATEX pwfx,      
  DWORD dwCallback,         
  DWORD dwCallbackInstance, 
  DWORD fdwOpen             
);

phwiは、WAVE入力デバイスのハンドルを受け取る変数のアドレスを指定します。 uDeviceIDは、使用するデバイスのIDを指定します。 デフォルトのデバイスを使用する場合は、WAVE_MAPPERを指定します。 pwfxは、WAVEFORMATEX構造体のアドレスを指定します。 dwCallbackは、コールバック機能を実装するためのハンドルを指定します。 dwCallbackInstanceは、dwCallbackにwaveInProc型コールバック関数を指定した場合に 意味を持ちます。それ以外の場合は、0を指定します。 fdwOpenは、WAVE入力デバイスにオープンに関する定数を指定します。 CALLBACK_WINDOWを指定した場合はdwCallbackにウインドウハンドルを指定することができ、 メッセージを受信できるようになります。

WAVE入力デバイスをオープンしたら、デバイスにバッファを追加するための準備を行うことになります。 これには、waveInPrepareHeaderを呼び出します。

MMRESULT waveInPrepareHeader(
  HWAVEIN hwi,   
  LPWAVEHDR pwh, 
  UINT cbwh      
);

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

バッファの準備が終了すれば、waveInAddBufferでデバイスにバッファを追加することができます。

MMRESULT waveInAddBuffer(
  HWAVEIN hwi,   
  LPWAVEHDR pwh, 
  UINT cbwh      
);

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

録音は、waveInStartで開始することになります。 これにより、waveInAddBufferで追加したバッファに録音データが書き込まれていきます。

MMRESULT waveInStart(
  HWAVEIN hwi 
);

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

録音を停止する場合は、waveInResetを呼び出します。

MMRESULT waveInReset(
  HWAVEIN hwi 
);

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

WAVEデバイスに追加したバッファは、 バッファの中身が録音データで一杯になった場合や、 waveInResetの呼び出し時にアプリケーションに返されることになります。 これによりバッファが不要になった場合は、 開放の前にwaveInUnprepareHeaderを呼び出すようにします。

MMRESULT waveInUnprepareHeader(
  HWAVEIN hwi,   
  LPWAVEHDR pwh, 
  UINT cbwh      
);

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

WAVE入力デバイスは、waveInCloseで閉じることになります。

MMRESULT waveInClose(
  HWAVEIN hwi 
);

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

WAVE入力デバイスに追加したバッファに、どれだけのサンプルが格納されているのかを確認する場合は、 waveInGetPositionを呼び出します。

MMRESULT waveInGetPosition(
  HWAVEIN hwi,   
  LPMMTIME pmmt, 
  UINT cbmmt     
);

hwiは、WAVE入力デバイスのハンドルを指定します。 pmmtは、MMTIME構造体のアドレスを指定します。 cbmmtは、pmmtのサイズを指定します。

録音を開始するには、予めステレオミキサーを有効にしておく必要があります。 Windows XPでは、次のような処理を実行します。



ボリュームコントロールは、タスクバーの通知領域や プログラム/アクセサリ/エンターテイメントから起動することができます。 音量を高くしすぎると録音時にノイズが入るため、注意してください。

今回のプログラムは、コンピュータ上で再生されている音声をWAVEファイルとして保存します。

#include <windows.h>

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

BOOL WriteWaveFile(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE lpWaveData, DWORD dwDataSize);
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 HWAVEIN      hwi = NULL;
	static WAVEHDR      wh = {0};
	static WAVEFORMATEX wf = {0};
	static BOOL         bPlayMediaFile = FALSE; 
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD dwRecordSecond = 30;
		DWORD dwDataSize;

		if (bPlayMediaFile) {
			mciSendString(TEXT("open sample.mid alias bgm"), NULL, 0, NULL);
			mciSendString(TEXT("play bgm"), NULL, 0, NULL);
		}

		wf.wFormatTag      = WAVE_FORMAT_PCM;
		wf.nChannels       = 1;
		wf.nSamplesPerSec  = 22050;
		wf.wBitsPerSample  = 8;
		wf.nBlockAlign     = wf.wBitsPerSample / 8 * wf.nChannels;
		wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign;
		
		if (waveInOpen(&hwi, WAVE_MAPPER, &wf, (DWORD)hwnd, 0, CALLBACK_WINDOW) != MMSYSERR_NOERROR) {
			MessageBox(NULL, TEXT("WAVEデバイスのオープンに失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}

		dwDataSize = wf.nAvgBytesPerSec * dwRecordSecond;
		lpWaveData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);

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

		waveInPrepareHeader(hwi, &wh, sizeof(WAVEHDR));
		waveInAddBuffer(hwi, &wh, sizeof(WAVEHDR));
		waveInStart(hwi);

		SetTimer(hwnd, 1, 200, NULL);
		
		return 0;
	}
	
	case MM_WIM_DATA:
		MessageBox(NULL, TEXT("バッファが録音データで一杯になりました。"), TEXT("OK"), MB_OK);
		return 0;
	
	case WM_TIMER: {
		TCHAR  szBuf[256];
		DWORD  dwSecond;
		MMTIME mmt;
		
		mmt.wType = TIME_SAMPLES;
		waveInGetPosition(hwi, &mmt, sizeof(MMTIME));

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

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

		return 0;
	}
	
	case WM_DESTROY:
		if (bPlayMediaFile) {
			mciSendString(TEXT("stop bgm"), NULL, 0, NULL);
			mciSendString(TEXT("close bgm"), NULL, 0, NULL);
		}

		if (hwi != NULL) {
			MMTIME mmt;
			
			mmt.wType = TIME_BYTES;
			waveInGetPosition(hwi, &mmt, sizeof(MMTIME));

			waveInReset(hwi);
			waveInUnprepareHeader(hwi, &wh, sizeof(WAVEHDR));		
			waveInClose(hwi);

			KillTimer(hwnd, 1);

			WriteWaveFile(TEXT("record.wav"), &wf, lpWaveData, mmt.u.cb);
		}

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

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

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

waveInOpenでWAVE入力デバイスをオープンするためには、WAVEフォーマットを指定しなければなりません。 nSamplesPerSecに22050を指定しているため、録音されるWAVEのサンプリングレートは22.5kHzであり、 nBlockAlignが1であることから、1秒間の音声を録音するのに必要なメモリは22050バイトになります。 dwRecordSecondの60というのは、最高60秒間音声を録音するという意味で、 これとnAvgBytesPerSecを掛けることで、 60秒間録音するために必要なメモリを算出できることになります。 後は確保したメモリをWAVEHDR構造体に指定し、 waveInPrepareHeaderとwaveInAddBufferを呼び出します。 waveInStartを呼び出すことによって録音が開始され、 バッファに録音データが書き込まれていきます。

WM_TIMERでは、waveInGetPositionを呼び出してバッファに書き込まれたサンプル数を取得しています。 TIME_SAMPLESで取得したサンプル数をnSamplesPerSecで割れば、 録音開始からどれくらい時間が経過しているのかが分かります。 今回は、60秒間録音できるだけのバッファを確保しましたから、 60秒経過した場合にバッファの中身は録音データで一杯になります。 waveInOpenでコールバック機能を指定している場合は、 このときにMM_WIM_DATAがポストされることになります。 MM_WIM_DATAで再びwaveInAddBufferを呼び出せば、録音を継続することができますが、 そのためには既に録音されたデータをどう管理するかを考える必要があります。

録音データは、WM_DESTROYで保存されます。

if (hwi != NULL) {
	MMTIME mmt;
	
	mmt.wType = TIME_BYTES;
	waveInGetPosition(hwi, &mmt, sizeof(MMTIME));
	
	WriteWaveFile(TEXT("record.wav"), &wf, lpWaveData, mmt.u.cb);

	waveInReset(hwi);
	waveInUnprepareHeader(hwi, &wh, sizeof(WAVEHDR));		
	waveInClose(hwi);

	KillTimer(hwnd, 1);
}

WAVEファイルの保存は、WriteWaveFileで行われています。 この自作関数の実装については、マルチメディアファイル入出力関数の章で取り上げています。 1つ注意しなければならないのは、データのサイズを表す第4引数にWAVEHDR.dwBufferLengthを指定してはならないという点です。 今回のプログラムでこのメンバに格納されている値は、60秒間音声を格納できるだけのサイズであり、 録音されたバイト数を表しているわけではありません。 WAVEファイルには録音されたデータだけを書き込むべきですから、 録音されたバイト数が必要になります。 これを表すメンバとしてWAVEHDR.dwBytesRecordedがありますが、 このメンバは録音が時間一杯で終了した場合のみ初期化されることになっています。 つまり、今回のプログラムで60秒経過する前にウインドウを閉じた場合、 WAVEHDR.dwBytesRecordedには0が格納され、これを参照するわけにはいきません。 よって、waveInGetPositionで録音したバイト数を明示的に取得し、 これをWriteWaveFileに指定することになります。 録音したバイト数を取得するには、MMTIME.wTypeにTIME_BYTESを指定します。

今回のプログラムは音声を録音するわけですから、 プログラムの起動後に何らかのマルチメディアアプリケーションを起動することになると思われます。 しかし、こうした作業を毎回行うのは大変ですから、 今回のプログラムには起動時と同時に音声を再生するための処理が用意されています。 それが、mciSendStringの呼び出しであり、bPlayMediaFileがTRUEの場合に実行されます。 "open sample.mid ..."のsample.midの部分を独自のファイルに変更することにより、 任意のファイルの音声を録音できるようになります。 録音を通じて、MIDIからWAVEへの変換を行うという方法はよく用いられます。

MCIによる録音

ボリュームコントロール等でステレオミキサーの設定を行っていれば、 waveIn関数以外にMCIでも録音が可能です。 MCIによる録音はwaveIn関数と比べて比較的容易であり、 録音データを格納するバッファを確保する必要もありませんから、 録音時間について気にする必要もなくなります。 次に、コード例を示します。

#include <windows.h>

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

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 MCIDEVICEID mciDeviceId = 0;
	static BOOL        bPlayMediaFile = FALSE; 

	switch (uMsg) {

	case WM_CREATE: {
		MCIERROR       mciError;
		MCI_OPEN_PARMS mciOpen;
		
		mciOpen.lpstrElementName = TEXT("");
		mciOpen.lpstrDeviceType = (LPCTSTR)MCI_DEVTYPE_WAVEFORM_AUDIO;
		mciError = mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT, (DWORD_PTR)&mciOpen);
		if (mciError != 0) {
			TCHAR szBuf[256];
			mciGetErrorString(mciError, szBuf, sizeof(szBuf) / sizeof(TCHAR));
			MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
			return -1;
		}

		mciDeviceId = mciOpen.wDeviceID;

		if (bPlayMediaFile) {
			mciSendString(TEXT("open sample.mid alias bgm"), NULL, 0, NULL);
			mciSendString(TEXT("play bgm"), NULL, 0, NULL);
		}
		
		mciSendCommand(mciDeviceId, MCI_RECORD, 0, 0);

		return 0;
	}

	case WM_DESTROY: {
		MCI_SAVE_PARMS mciSave;
		
		if (bPlayMediaFile) {
			mciSendString(TEXT("stop bgm"), NULL, 0, NULL);
			mciSendString(TEXT("close bgm"), NULL, 0, NULL);
		}

		mciSendCommand(mciDeviceId, MCI_STOP, MCI_WAIT, 0);

		mciSave.lpfilename = TEXT("record.wav");
		mciSendCommand(mciDeviceId, MCI_SAVE, MCI_WAIT | MCI_SAVE_FILE, (DWORD_PTR)&mciSave);

		mciSendCommand(mciDeviceId, MCI_CLOSE, 0, 0);

		PostQuitMessage(0);

		return 0;
	}

	default:
		break;

	}

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

まず、mciSendCommandの第2引数にMCI_OPENを指定して、MCIデバイスをオープンします。 このとき、MCI_OPEN_PARMS構造体のlpstrElementNameには空の文字列を指定し、 lpstrDeviceTypeにはWAVEデバイスを指定します。 続いて、mciSendCommandにMCI_RECORDを指定し、録音を開始します。 録音を終了したい場合はウインドウを閉じ、これによりファイルへの保存が行われることになります。 まず、MCI_STOPで録音を停止し、次にMCI_SAVEでファイルを保存します。 保存するファイル名はMCI_SAVE_PARMS.lpfilenameに指定し、 mciSendCommandの第3引数にはMCI_SAVE_FILEを指定します。 MCI_WAITは、第2引数のコマンド操作が終了するまで、関数が制御を返さないことを意味しています。

MCIによる録音で、録音時間を制限したい場合は、 MCI_RECORD_PARMS構造体を用いることになります。 次に、例を示します。

MCI_RECORD_PARMS mciRecord;

mciRecord.dwTo = 60 * 1000;
mciRecord.dwCallback = (DWORD_PTR)hwnd;

mciSendCommand(mciDeviceId, MCI_RECORD, MCI_TO | MCI_NOTIFY, (DWORD_PTR)&mciRecord);

dwToに、録音時間を指定します。 これはミリ秒単位でなければならないため、 1000ミリ秒に60を掛けることで、60秒を指定したことになります。 dwCallbackには、ウインドウハンドルを指定することができます。 これを指定すると、録音時間を経過した場合にMM_MCINOTIFYが送られることになります。 dwToを初期化する場合は第3引数にMCI_TOを指定し、 dwCallbackを初期化する場合はMCI_NOTIFYを指定します。 MCI_NOTIFYの処理は、次のようになります。

case MM_MCINOTIFY:
	if (wParam == MCI_NOTIFY_SUCCESSFUL)
		MessageBox(NULL, TEXT("録音終了時間になりました。"), TEXT("OK"), MB_OK);
	return 0;

録音時間の経過によってMM_MCINOTIFYが送られた場合は、 wParamがMCI_NOTIFY_SUCCESSFULになります。 一方、MCI_STOPで録音が停止した場合は、wParamがMCI_NOTIFY_ABORTEDになります。

MCIによる録音のフォーマットは、既定でビット数が8ビット、チャンネルがモノラル、 サンプリングレートが11KHzになっています。 これを変更したい場合は、MCI_RECORDコマンドを送信する前にMCI_SETコマンドを送信します。

MCI_WAVE_SET_PARMS mciWave;
		
mciWave.wBitsPerSample = 16;
mciSendCommand(mciDeviceId, MCI_SET, MCI_WAVE_SET_BITSPERSAMPLE, (DWORD_PTR)&mciWave);

WAVEデバイスをオープンしている場合は、第4引数にMCI_WAVE_SET_PARMS構造体を指定することができます。 第3引数は、同構造体の中で初期化するメンバを表す定数を指定し、 MCI_WAVE_SET_BITSPERSAMPLEならば、wBitsPerSampleメンバを初期化することができます。 上記のコードを実行した場合は、フォーマットのビット数が16ビットになります。



戻る