EternalWindows
MCI / MCIコマンド文字列

マルチメディアプレイヤーの1つの課題として挙げられるのは、 より多くのマルチメディアファイルの再生に対応することであると思われます。 本来ならばこれを実現するためには、各種マルチメディアファイルの形式を理解すると共に、 再生に必要になる関数も理解しなければなりませんが、 高水準APIであるMCI(Media Control Interface)を使用する場合は話は別となります。 MCIでは、個々のファイル形式の違いを意識せず、 どのようなファイルでも統一的な方法で再生することができるため、 再生対象のファイル形式を気にする必要はありません。 さらに、MCIでは、再生や一時停止といった操作を行うために、専用の関数を呼び出すことはありません。 用意されているのは、MCIデバイスにコマンドを送信する関数であり、 この関数を通じて再生や一時停止といった操作を行うことになります。 つまり、各種ファイルを同じように扱えるだけでなく、 各種操作も同じように行えることになります。

MCIデバイスにコマンドを送信するには、文字列による方法とメッセージによる方法があります。 今回は、文字列でMCIコマンドを送信するmciSendStringについて説明します。

MCIERROR mciSendString(
  LPCTSTR lpszCommand,  
  LPTSTR lpszReturnString,  
  UINT cchReturn,       
  HANDLE hwndCallback   
);

lpszCommandは、MCIコマンド文字列を指定します。 lpszReturnStringは、実行したコマンドを受け取るバッファを指定します。 cchReturnは、lpszReturnStringのサイズを指定します。 hwndCallbackは、通知を受けるウインドウハンドルを指定します。

mciSendStringを使用するためには、まずMCIデバイスをオープンすることになります。 これにより、メディアファイルもオープンされることになり、 再生の準備が整うことになります。 MCIデバイスをオープンするには、openコマンドを使用します。

mciSendString(TEXT("open sample.wav"), NULL, 0, NULL);

これにより、MCIデバイスがオープンされ、sample.wavというファイルもオープンされることになります。 第2引数はコマンドの結果を受け取るバッファですが、 openコマンドの場合はNULLを指定することができます。 第4引数はウインドウハンドルですが、これもNULLで構いません。 続いて、再生を行うplayコマンドを示します。

mciSendString(TEXT("play sample.wav notify"), NULL, 0, hwnd);

これにより、sample.wavが再生されることになります。 notifyというキーワードを必須ではありませんが、 これを指定すると再生の終了時にメッセージを受け取ることができます。 この場合は、第4引数はウインドウハンドルを指定することになります。

コマンドを送信する度に、ファイル名を指定するのはあまり好ましくありません。 再生するファイル名を変更することになった場合に、多くの箇所を変更することになるからです。 このため、ファイル名をエイリアス名(別名)で表現することがよくあります。

mciSendString(TEXT("open sample.wav alias bgm"), NULL, 0, NULL);

aliasというキーワードの後に、エイリアス名を自由に指定します。 上記の例ではbgmとしていますから、 これ以降に送信するコマンドにはこのエイリアス名を指定することができます。

mciSendStringに指定できるコマンドの一部を次に示します。 %sの個所には、ファイル名またはエイリアス名を指定します。 また、括弧のキーワードはオプションです。

操作内容 コマンド文字列 補足説明
デバイスのオープン open [filename] (alias %s) ---
再生 play %s (notify) ---
停止 stop %s ---
デバイスのクローズ close %s ---
一時停止 pause %s ---
一時停止の再開 resume %s ---
タイムフォーマットをミリ秒にする set %s time format milliseconds millisecondsはmsと表記してもよい。
現在位置の移動 seek %s to [移動位置] 移動位置には0以上の値の他、startやendも指定できる。
再生時間の取得 status %s length 第2引数に再生時間を表す文字列が返る。
現在位置の取得 status %s position 第2引数に現在位置を表す文字列が返る。
現在の状態を取得 status %s mode 第2引数に現在の状態を表す文字列が返る。
playing : 再生中
stopped : 停止中
paused : 一時停止中

stop、close、pause、resumeには、%sの箇所にallを指定することもできます。 この場合、MCIデバイスによって再生されている全てのファイルに対してコマンドが有効になります。 たとえば、アプリケーション内で2つのファイルをmciSendStringで再生している場合、 両方のファイルを停止するためにstopを2回実行する必要はありません。 stop allとすることで、どちらのファイルも停止することになります。 allを指定すれば、エイリアス名に依存したコードが減ることになるため、 1つのファイルのみを再生する場合でも有用です。

MCIでは、どのようなメディアファイルでも統一的に扱えることになっていますが、 一部のコマンドについては、特定のファイルを扱えないことがあります。 たとえば、MIDIファイルを扱う場合は、一時停止状態のデバイスをresumeコマンドで再開できません。 よって、MIDIファイルではpauseコマンドを実行するべきではないといえます。 resumeコマンドの代わりにplayコマンドを実行すればよいように思えますが、 音の音色が本来とは異なる可能性があります。 サポートされていないコマンドを実行した場合は、 MCIERR_UNSUPPORTED_FUNCTIONが返ります。

コマンドの送信が失敗し、MCIERROR型の変数に0以外の値が返った場合、 mciGetErrorStringを呼び出すことでエラーの詳細を取得することができます。

BOOL mciGetErrorString(
  DWORD fdwError,        
  LPTSTR lpszErrorText,  
  UINT cchErrorText      
);

fdwErrorは、mciSendStringまたはmciSendCommandの戻り値を指定します。 lpszErrorTextは、エラーの内容を受け取るバッファを指定します。 cchErrorTextは、lpszErrorTextのサイズを指定します。

今回のプログラムは、mciSendStringを使用してメディアファイルを再生します。 また、マウスの左ボタンが押された場合は、再生を一時停止します。

#include <windows.h>
#include <shlwapi.h>

#pragma comment (lib, "winmm.lib")
#pragma comment (lib, "shlwapi.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)
{
	switch (uMsg) {

	case WM_CREATE: {
		MCIERROR mciError;

		mciError = mciSendString(TEXT("open sample.wav alias bgm"), NULL, 0, NULL);
		if (mciError != 0) {
			TCHAR szBuf[256];
			mciGetErrorString(mciError, szBuf, sizeof(szBuf) / sizeof(TCHAR));
			MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
			return -1;
		}
		
		mciSendString(L"set bgm time format ms", NULL, 0, 0);
		mciSendString(TEXT("play bgm notify"), NULL, 0, hwnd);	
		
		SetTimer(hwnd, 1, 200, NULL);
		
		return 0;
	}

	case WM_LBUTTONDOWN: {
		TCHAR szBuf[256];

		mciSendString(TEXT("status bgm mode"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
		if (lstrcmp(szBuf, TEXT("playing")) == 0)
			mciSendString(TEXT("pause bgm"), 0, 0, NULL);
		else if (lstrcmp(szBuf, TEXT("paused")) == 0)
			mciSendString(TEXT("resume bgm"), NULL, 0, NULL);
		else
			;	
		return 0;
	}
	
	case WM_TIMER: {
		TCHAR szBuf[256];
		DWORD dwSecond;

		mciSendString(TEXT("status bgm mode"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
		if (lstrcmp(szBuf, TEXT("playing")) != 0)
			return 0;

		mciSendString(TEXT("status bgm position"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
		dwSecond = StrToInt(szBuf) / 1000;

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

		return 0;
	}

	case MM_MCINOTIFY:
		if (wParam != MCI_NOTIFY_SUCCESSFUL)
			return 0;
		mciSendString(TEXT("seek bgm to start"), NULL, 0, hwnd);
		mciSendString(TEXT("play bgm notify"), NULL, 0, hwnd);
		return 0;

	case WM_DESTROY:
		KillTimer(hwnd, 1);
		mciSendString(TEXT("stop bgm"), NULL, 0, NULL);
		mciSendString(TEXT("close bgm"), NULL, 0, NULL);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

openコマンドを指定したmciSendStringにsample.wavを指定しているため、 カレントディレクトリに存在するsample.wavというファイルが再生されることになります。 既に述べたようにMCIは様々なファイル形式に対応しているため、 MIDIファイルやMP3ファイルを指定しても問題なく再生されるはずです。 また、適切なコーデックがインストールされている場合は、 AVIファイルを再生することもできるでしょう。 ただし、動画ファイルを再生する場合は、専用のウインドウが作成されることになります。 openコマンドでは、ファイルが存在しないなどの理由で失敗することが考えられるため、 戻り値が成功を示す0でない場合は、mciGetErrorStringを呼び出してエラー情報を取得しています。 SetTimerを呼び出しているのは、現在の再生位置を周期的に取得するためです。 今回のプログラムでは、SetTimerの第3引数に200を指定しているため、 WM_TIMERが200ミリ秒間隔で送られることになります。

case WM_TIMER: {
	TCHAR szBuf[256];
	DWORD dwSecond;

	mciSendString(TEXT("status bgm mode"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
	if (lstrcmp(szBuf, TEXT("playing")) != 0)
		return 0;

	mciSendString(TEXT("status bgm position"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
	dwSecond = StrToInt(szBuf) / 1000;

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

	return 0;
}

まず、statusコマンドにmodeを指定して、現在のデバイスの状態を取得します。 この結果がplayingでない場合は、現在再生されていないということなので、 再生位置を取得する必要はありません。 playingである場合は、statusコマンドにpositionを指定して、現在の再生位置を取得します。 WM_CREATEで実行したタイムフォーマットのコマンドにより、 この位置はミリ秒単位で格納されているため、 1000で割ることで秒に変換することができます。 StrToIntは文字列を数値に変換する関数であり、 shlwapi.hのインクルードとshlwapi.libのリンクを必要とします。 続いて、一時停止処理を確認します。

case WM_LBUTTONDOWN: {
	TCHAR szBuf[256];

	mciSendString(TEXT("status bgm mode"), szBuf, sizeof(szBuf) / sizeof(TCHAR), NULL);
	if (lstrcmp(szBuf, TEXT("playing")) == 0)
		mciSendString(TEXT("pause bgm"), 0, 0, NULL);
	else if (lstrcmp(szBuf, TEXT("paused")) == 0)
		mciSendString(TEXT("resume bgm"), NULL, 0, NULL);
	else
		;	
	return 0;
}

現在の状態がplayingである場合はファイルが再生されていますから、 pauseコマンドで停止するようにします。 一方、現在の状態がpausedである場合は再生が一時停止していますから、 resumeコマンドで再開することになります。 ただし、既に述べたように、MIDIファイルではresumeコマンドに失敗します。 最後に、ループ再生の処理を確認します。

case MM_MCINOTIFY:
	if (wParam != MCI_NOTIFY_SUCCESSFUL)
		return 0;
	mciSendString(TEXT("seek bgm to start"), NULL, 0, hwnd);
	mciSendString(TEXT("play bgm notify"), NULL, 0, hwnd);
	return 0;

playコマンドにnotifyとウインドウハンドルを指定した場合は、 再生の終了後にMM_MCINOTIFYが送られることになっています。 よって、この時にもう一度playコマンドを実行すれば、 再生がループしているように聞こえます。 seekコマンドにstart(0でも可)を指定しているのは、現在位置をファイルの先頭に戻すためです。 wParamは、再生の終了によってMM_MCINOTIFYが送られたのであれば、 MCI_NOTIFY_SUCCESSFULが格納されていますが、 それ以外の理由で送られた場合は、別の値が格納されている可能性があります。 たとえば、MIDIファイルに対してpauseコマンドを実行した場合は、 ウインドウハンドルの指定に関わらず、 wParamがMCI_NOTIFY_ABORTEDであるMM_MCINOTIFYが送られることになります。 このような予測しないMM_MCINOTIFYが送られた場合は、 ループ再生の処理を実行しないようにします。

MCIとスレッド

mciSendStringやmciSendCommandでMIDIファイルをオープンする場合、 他のファイルと比べて関数から制御が返るまで時間が掛かることになります。 今回のプログラムでは、WM_CREATEでmciSendStringを実行しているため、 関数から制御が返らないことにはウインドウも表示されず、 明らかな待機時間が生じることになります。 メインスレッドでこうした待機時間が発生してはウインドウを操作できませんから、 別スレッドによってmciSendStringを実行するのも候補といえます。

#include <windows.h>

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

DWORD WINAPI ThreadProc(LPVOID lpParameter);
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 HANDLE hThread = NULL;
	static DWORD  dwThreadId = 0;

	switch (uMsg) {

	case WM_CREATE:
		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, (LPVOID)hwnd, 0, &dwThreadId);
		return 0;

	case MM_MCINOTIFY:
		if (wParam == MCI_NOTIFY_SUCCESSFUL)
			PostThreadMessage(dwThreadId, MM_MCINOTIFY, 0, 0);
		return 0;

	case WM_DESTROY:
		if (hThread != NULL) {
			PostThreadMessage(dwThreadId, WM_NULL, 0, 0);
			WaitForSingleObject(hThread, 2000);
			CloseHandle(hThread);
		}
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	MSG msg;

	mciSendString(TEXT("open sample.mid alias bgm"), NULL, 0, NULL);

	do {
		mciSendString(TEXT("seek bgm to start"), NULL, 0, (HWND)lpParameter);
		mciSendString(TEXT("play bgm notify"), NULL, 0, (HWND)lpParameter);

		GetMessage(&msg, NULL, 0, 0);

	} while (msg.message == MM_MCINOTIFY);
	
	mciSendString(TEXT("stop bgm"), NULL, 0, NULL);
	mciSendString(TEXT("close bgm"), NULL, 0, NULL);

	return 0;
}

このプログラムでは、WM_CREATEでスレッドを作成し、そのスレッドでファイルを再生しようとしています。 れにより、メインスレッドでmciSendStringを呼び出す必要がなくなりますから、 関数から制御が返るまで待たされるようなことはなくなり、 直ちにウインドウが表示されることになります。 ThreadProcではplayコマンドを実行した後にGetMessageで処理を待機します。 処理が再開されるのは、メインスレッドによってPostThreadMessageが呼び出された時であり、 MM_MCINOTIFYではない場合は、終了処理を実行することになります。



戻る