EternalWindows
MCI / MCIコマンドメッセージ

MCIを使用したマルチメディアファイルの再生は、 各種ファイルの違いを意識する必要がないという大きな利点があります。 これにより、アプリケーションは、前節で述べたmciSendStringや後述するmciSendCommandを呼び出して、 どのようなファイルでも統一的な方法で再生することができます。 ただし、MCIによる再生は、決して万能というわけではありません。 たとえば、MCIでは再生スピードを変化させることができませんから、 このような動作を行いたい場合はMCIではなく、より低水準な関数群を呼び出す必要があります。 また、MIDIファイルを再生する場合は、任意の位置へのシークや一時停止からの再開が正しく機能しないため、 他のファイルと比べて優劣が生じることになります。

MCIによる再生には、前節で述べたコマンド文字列を送信する方法と、 コマンドメッセージを送信する方法があります。 コマンドメッセージを送信するには、mciSendCommandを呼び出します。

MCIERROR mciSendCommand(
  MCIDEVICEID IDDevice,
  UINT uMsg,
  DWORD fdwCommand,
  DWORD dwParam
);

IDDeviceは、コマンドを送信するMCIデバイスの識別子を指定します。 デバイスのオープン時には0を指定します。 uMsgは、送信したいコマンドを指定します。 fdwCommandは、コマンドについてのフラグを指定します。 コマンドによっては0を指定する場合もあります。 dwParamは、コマンドについてのパラメータを格納する構造体のアドレスを指定します。 コマンドによっては0を指定する場合もあります。

コマンドメッセージによってMCIデバイスを制御するには、 MCIデバイスの識別子が必要となります。 これは、MCI_OPENコマンドでMCIデバイスをオープンすることによって取得できます。

MCI_OPEN_PARMS mciOpen;

mciOpen.lpstrDeviceType  = TEXT("WaveAudio");
mciOpen.lpstrElementName = TEXT("sample.wav");
mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_ELEMENT, (DWORD_PTR)&mciOpen);

MCIデバイスをオープンするには、第2引数にMCI_OPENを指定します。 また、MCI_OPENを指定する場合は、第4引数にMCI_OPEN_PARMS構造体を指定します。 lpstrDeviceTypeはデバイスタイプであり、これはファイルの種類を示す文字列を指定します。 WAVEファイルを再生したい場合は、WaveAudioを指定することになります。 lpstrElementNameは再生したいファイル名であり、 lpstrDeviceTypeにWaveAudioを指定しているため、WAVEファイルを指定しています。 第3引数のMCI_OPEN_TYPEはlpstrDeviceTypeを初期化することを意味し、 MCI_OPEN_ELEMENTはlpstrElementNamを初期化することを意味しています。 関数が成功した場合は、MCI_OPEN_PARMS.wDeviceIDにMCIデバイスの識別子が格納されます。 デバイスタイプとファイルの関係を次に示します。

ファイル デバイスタイプ タイプID
WAVE WaveAudio MCI_DEVTYPE_WAVEFORM_AUDIO
MIDI Sequencer MCI_DEVTYPE_SEQUENCER
CDAudio CDAudio MCI_DEVTYPE_CD_AUDIO
AVI avivideo 定義されていない
MP3 MPEGVideo 定義されていない

一部のファイルでは、デバイスタイプの代わりにタイプIDを指定することもできます。 タイプIDを指定する例を次に示します。

MCI_OPEN_PARMS mciOpen;

mciOpen.lpstrDeviceType  = MCI_DEVTYPE_WAVEFORM_AUDIO;
mciOpen.lpstrElementName = TEXT("sample.wav");
mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT, (DWORD_PTR)&mciOpen);

この例では、lpstrDeviceTypeにタイプIDを指定します。 タイプIDを指定する場合は、mciSendCommandの第3引数にMCI_OPEN_TYPE_IDを指定します。 mciSendCommandの第2引数に指定できるコマンドの一部を次に示します。

操作内容 コマンドメッセージ コマンドフラグ
デバイスのオープン MCI_OPEN MCI_OPEN_TYPEとMCI_OPEN_ELEMENTを指定。
再生 MCI_PLAY 再生終了後に通知が必要な場合はMCI_NOTIFYを指定。
停止 MCI_STOP ---
デバイスのクローズ MCI_CLOSE ---
一時停止 MCI_PAUSE ---
一時停止の再開 MCI_RESUME ---
タイムフォーマットをミリ秒にする MCI_SET MCI_FORMAT_MILLISECONDSを指定。
現在位置の移動 MCI_SEEK 0以上の値やMCI_SEEK_TO_STARTなどを指定。
再生時間の取得 MCI_STATUS MCI_STATUS_PARMS.dwItemにMCI_STATUS_LENGTHを指定。
現在位置の取得 MCI_STATUS MCI_STATUS_PARMS.dwItemにMCI_STATUS_POSITIONを指定。
現在の状態を取得 MCI_STATUS MCI_STATUS_PARMS.dwItemにMCI_STATUS_MODEを指定。
MCI_MODE_PLAY : 再生中
MCI_MODE_STOP : 停止中
MCI_MODE_PAUSE : 一時停止中

MCI_STATUSコマンドは、再生に関係する情報や、MCIデバイスの現在の状態などを取得することができます。 次に、再生時間を取得する例を示します。

MCI_STATUS_PARMS mciStatus;

mciStatus.dwItem = MCI_STATUS_LENGTH;
mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, &mciStatus);

第2引数にMCI_STATUSを指定する場合は、 第4引数にMCI_STATUS_PARMS構造体を指定します。 この構造体のdwItemにはどのような情報を取得するかを表す定数が指定可能であり、 MCI_STATUS_LENGTHは再生時間を取得する定数です。 また、dwItemメンバを使用する場合は、 mciSendCommandの第3引数にMCI_STATUS_ITEMを指定します。 取得した値はdwReturnというDWORD型に格納されており、 mciSendStringの時のように文字列を整数に変換する処理は必要ありません。

今回のプログラムは、MCIコマンドメッセージによってファイルを再生します。 また、マウスの左ボタンが押された場合は、再生を一時停止することになります。 基本的には、前節のプログラムをmciSendCommandで実現した内容です。

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

	switch (uMsg) {

	case WM_CREATE: {
		MCIERROR       mciError;
		MCI_OPEN_PARMS mciOpen;
		MCI_PLAY_PARMS mciPlay;
		MCI_SET_PARMS  mciSet;
	
		mciOpen.lpstrDeviceType  = (LPCTSTR)MCI_DEVTYPE_WAVEFORM_AUDIO;
		mciOpen.lpstrElementName = TEXT("sample.wav");
		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;

		mciSet.dwTimeFormat = MCI_FORMAT_MILLISECONDS;
		mciSendCommand(mciDeviceId, MCI_SET, MCI_SET_TIME_FORMAT, (DWORD_PTR)&mciSet);

		mciPlay.dwCallback = (DWORD_PTR)hwnd;
		mciSendCommand(mciDeviceId, MCI_PLAY, MCI_NOTIFY, (DWORD_PTR)&mciPlay);

		SetTimer(hwnd, 1, 200, NULL);
		
		return 0;
	}

	case WM_LBUTTONDOWN: {
		MCI_STATUS_PARMS mciStatus;
		
		mciStatus.dwItem = MCI_STATUS_MODE;
		mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);
		if (mciStatus.dwReturn == MCI_MODE_PLAY)
			mciSendCommand(mciDeviceId, MCI_PAUSE, 0, 0);
		else if (mciStatus.dwReturn == MCI_MODE_PAUSE)
			mciSendCommand(mciDeviceId, MCI_RESUME, 0, 0);
		else
			;
		return 0;
	}
	
	case WM_TIMER: {
		TCHAR            szBuf[256];
		DWORD            dwSecond;
		MCI_STATUS_PARMS mciStatus;
		
		mciStatus.dwItem = MCI_STATUS_MODE;
		mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);
		if (mciStatus.dwReturn != MCI_MODE_PLAY)
			return 0;
		
		mciStatus.dwItem = MCI_STATUS_POSITION;
		mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);

		dwSecond = (DWORD)mciStatus.dwReturn / 1000;

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

		return 0;
	}

	case MM_MCINOTIFY: {
		MCI_PLAY_PARMS mciPlay;
		
		if (wParam != MCI_NOTIFY_SUCCESSFUL)
			return 0;
		
		mciSendCommand(mciDeviceId, MCI_SEEK, MCI_SEEK_TO_START, 0);
		
		mciPlay.dwCallback = (DWORD_PTR)hwnd;
		mciSendCommand(mciDeviceId, MCI_PLAY, MCI_NOTIFY, (DWORD_PTR)&mciPlay);
		
		return 0;
	}

	case WM_DESTROY:
		KillTimer(hwnd, 1);
		mciSendCommand(mciDeviceId, MCI_STOP, 0, 0);
		mciSendCommand(mciDeviceId, MCI_CLOSE, 0, 0);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

まず、mciSendCommandにMCI_OPENを指定して、MCIデバイスをオープンします。 続いて、オープンしたデバイス識別子を静的変数に保存し、 他のメッセージからでもmciSendCommandを呼び出せるようにします。 MCI_PLAYコマンドでは、第3引数にMCI_NOTIFYを指定しているため、 再生の終了後にMM_MCINOTIFYが送られることになります。 これを受け取るには、第4引数にMCI_PLAY_PARMS構造体を指定し、 dwCallbackメンバにウインドウハンドルを指定しておきます。 SetTimerを呼び出しているのは、現在の再生位置を確認するために、 mciSendCommandを周期的に呼び出す必要があるからです。 SetTimerの第3引数に200を指定しているため、200ミリ秒間隔でWM_TIMERが送られることになります。

case WM_TIMER: {
	TCHAR            szBuf[256];
	DWORD            dwSecond;
	MCI_STATUS_PARMS mciStatus;
	
	mciStatus.dwItem = MCI_STATUS_MODE;
	mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);
	if (mciStatus.dwReturn != MCI_MODE_PLAY)
		return 0;
	
	mciStatus.dwItem = MCI_STATUS_POSITION;
	mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);

	dwSecond = (DWORD)mciStatus.dwReturn / 1000;

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

	return 0;
}

MCI_STATUS_PARMS.dwItemにMCI_STATUS_MODEを指定して、MCI_STATUSコマンドを送信した場合、 dwReturnに現在のMCIデバイスの状態が格納されます。 これがMCI_MODE_PLAYではない場合は再生が行われていないことを意味するため、 現在位置を取得する必要はありません。 MCI_MODE_PLAYの場合は、MCI_STATUS_PARMS.dwItemにMCI_STATUS_POSITIONを指定して、 現在位置を取得することになります。 WM_CREATEでMCI_FORMAT_MILLISECONDSを指定したmciSendCommandを実行しているため、 dwReturnに返される時間はミリ秒単位です。 よって、1000で割ることで秒に変換することができます。 続いて、一時処理を確認します。

case WM_LBUTTONDOWN: {
	MCI_STATUS_PARMS mciStatus;
	
	mciStatus.dwItem = MCI_STATUS_MODE;
	mciSendCommand(mciDeviceId, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);
	if (mciStatus.dwReturn == MCI_MODE_PLAY)
		mciSendCommand(mciDeviceId, MCI_PAUSE, 0, 0);
	else if (mciStatus.dwReturn == MCI_MODE_PAUSE)
		mciSendCommand(mciDeviceId, MCI_RESUME, 0, 0);
	else
		;
	return 0;
}

現在の状態がMCI_MODE_PLAYである場合は、再生中ということでMCI_PAUSEコマンドを送信します。 また、MCI_MODE_PAUSEの場合は、一時停止中ということでMCI_RESUMEによって再開します。 ただし、このMCI_RESUMEに関しては、MIDIファイルに対応していません。 また、MIDIファイルに対してMCI_PAUSEを実行した場合は、 wParamがMCI_NOTIFY_ABORTEDであるMM_MCINOTIFYが送られることになります。 このような予測しないMM_MCINOTIFYは、処理しないようにする必要があります。

case MM_MCINOTIFY: {
	MCI_PLAY_PARMS mciPlay;
	
	if (wParam != MCI_NOTIFY_SUCCESSFUL)
		return 0;
	
	mciSendCommand(mciDeviceId, MCI_SEEK, MCI_SEEK_TO_START, 0);
	
	mciPlay.dwCallback = (DWORD_PTR)hwnd;
	mciSendCommand(mciDeviceId, MCI_PLAY, MCI_NOTIFY, (DWORD_PTR)&mciPlay);
	
	return 0;
}

再生が終了した場合に送られるMM_MCINOTIFYは、wParamがMCI_NOTIFY_SUCCESSFULになっているため、 この場合のみループ再生の処理を実行することになります。 まず、MCI_SEEKコマンドにMCI_SEEK_TO_STARTを指定して、現在位置をファイルの先頭にシークし、 MCI_PLAYコマンドでファイルを再生します。

MCI Extensionsについて

mciSendCommandでWAVEファイルを再生する場合、lpstrDeviceTypeにWaveAudioという文字列を指定できるわけですが、 こうしたファイルに関連するデバイスタイプは次のレジストリキーに列挙されています。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\MCI Extensions

上記レジストリキーを表示するプログラムを次に示します。

#include <windows.h>

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 HWND hwndListBoxLeft = NULL;
	static HWND hwndListBoxRight = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		DWORD i = 0;
		HKEY  hKey;
		TCHAR szData[256];
		TCHAR szValue[256];
		DWORD dwData;
		DWORD dwValue;
		DWORD dwResult;

		hwndListBoxLeft = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndListBoxRight = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);

		dwResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\MCI Extensions"), 0, KEY_QUERY_VALUE, &hKey);
		if (dwResult != ERROR_SUCCESS) {
			MessageBox(NULL, TEXT("レジストリキーのオープンに失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}

		for (;;) {
			dwData = sizeof(szData);
			dwValue = sizeof(szValue);

			dwResult = RegEnumValue(hKey, i, szValue, &dwValue, 0, 0, (LPBYTE)szData, &dwData);
			if (dwResult == ERROR_NO_MORE_ITEMS)
				break;

			SendMessage(hwndListBoxLeft, LB_ADDSTRING, 0, (LPARAM)szValue);
			SendMessage(hwndListBoxRight, LB_ADDSTRING, 0, (LPARAM)szData);

			i++;
		}
		
		RegCloseKey(hKey);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndListBoxLeft, 0, 0, LOWORD(lParam) / 2, HIWORD(lParam), TRUE);
		MoveWindow(hwndListBoxRight, LOWORD(lParam) / 2, 0, LOWORD(lParam) / 2, HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

プログラムを実行してみると、ファイルの拡張子に対応するデバイスタイプを確認することができます。 基本的に、ここで表示されていない拡張子を持ったファイルは、MCIで再生できないと考えてよいと思われます。 ちなみに、レジストリキーに書き込まれたデバイスタイプの値を変更しても、 mciSendCommandの呼び出しには影響がないようです。



戻る