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

MIDIストリームでループ再生を実現するには、 midiStreamOpenのコールバック機能を利用することになります。 次に、例を示します。

midiStreamOpen(&hms, &uId, 1, (DWORD_PTR)hwnd, 0, CALLBACK_WINDOW);

midiStreamOpenの第6引数にCALLBACK_WINDOWを指定した場合、 第4引数にウインドウハンドルを指定することができます。 このウインドウのウインドウプロシージャには、 MIDIストリームからアプリケーションにバッファが返された時に、 MM_MOM_DONEがポストされることになるため、 これを検出することでループ再生を実現できるようになります。

アプリケーションにバッファが返されるという点をもう少し補足します。 midiStreamOutを呼び出した場合、MIDIストリームの一時停止状態が解除されていれば、 関数に指定したバッファが再生されることになります。 このバッファに設定されている全てのイベントの再生が終了した場合、 MIDIストリームはこのバッファをMM_MOM_DONEのLPARAMに指定すると共に、 次のバッファが既に書き込まれているかどうかを確認します。 このとき、書き込まれている場合は、そのバッファのイベントを再生することになります。 逆に、書き込まれていない場合は、何も再生されることはありません。 バッファが返されたときに、再びバッファを書き込むようにすれば、 MIDIストリーム内にバッファを維持できるようになるため、 再生が途切れることはなくなります。

続いて、一時停止の処理について説明します。 これは、MIDIストリームを使用している場合は極めて簡単であり、 midiStreamPauseを呼び出すだけで構いません。

MMRESULT midiStreamPause(
  HMIDISTRM hms 
);

hmsは、MIDIストリームのハンドルを指定します。

今回のプログラムは、MIDIをループ再生します。 また、マウスの左ボタンが押された場合に一時停止または再開を行います。

#include <windows.h>

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

struct EVENT {
	BYTE   state;   // ステータスバイト
	BYTE   data1;   // 第1データバイト
	BYTE   data2;   // 第2データバイト
	BYTE   type;    // タイプ
	int    nData;   // データ長
	LPBYTE lpData;  // 可変長データ
	DWORD  dwDelta; // デルタタイム

	struct EVENT *lpNext; // 次のイベントへのポインタ
};
typedef struct EVENT EVENT;
typedef struct EVENT *LPEVENT;

struct SHORTEVENT {
	DWORD dwDeltaTime;
	DWORD dwStreamID;
	DWORD dwEvent;
};
typedef struct SHORTEVENT SHORTEVENT;
typedef struct SHORTEVENT *LPSHORTEVENT;

WORD    g_wTime = 0;
BOOL    g_bPlayMusic = FALSE;
HANDLE  g_hheap = NULL;
LPEVENT g_lpHeader = NULL;

DWORD GetBufferCount(DWORD dwMaxBufferSize);
void SetBuffer(LPMIDIHDR *lplpBuffer, DWORD dwBufferCount, DWORD dwMaxBufferSize);
void PlayMusic(HMIDISTRM hms, LPMIDIHDR lpBuffer, DWORD dwBufferCount);
void StopMusic(HMIDISTRM hms, LPMIDIHDR lpBuffer, LPDWORD lpdwBufferCount);
BOOL ReadMidiFile(LPTSTR lpszFileName);
BOOL ReadTrack(HMMIO hmmio, LPEVENT *lplpEvent);
LPEVENT MargeTrack(LPEVENT *lplpEvent, WORD wTruck);
void ReadAndReverse(HMMIO hmmio, LPVOID lpData, DWORD dwSize);
void ReadDelta(HMMIO hmmio, LPDWORD lpdwDelta);
LPVOID Alloc(DWORD dwSize);
BOOL SelectMidiFile(HWND hwnd, LPTSTR lpszFileName);
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 HMIDISTRM hms = NULL;
	static LPMIDIHDR lpBuffer = NULL;
	static DWORD     dwBufferCount = 0;
	static DWORD     dwBufferResetCount = 0;
	static BOOL      bPause = FALSE;

	switch (uMsg) {

	case WM_CREATE: {
		UINT     uId = MIDI_MAPPER;
		MMRESULT mr;

		mr = midiStreamOpen(&hms, &uId, 1, (DWORD_PTR)hwnd, 0, CALLBACK_WINDOW);
		
		return mr == MMSYSERR_NOERROR ? 0 : -1;
	}
	
	case WM_LBUTTONDOWN: {
		TCHAR szFileName[MAX_PATH];
		DWORD dwMaxBufferSize = 65536 - sizeof(MIDIHDR);

		if (!SelectMidiFile(hwnd, szFileName))
			return 0;

		dwBufferResetCount = dwBufferCount;
		StopMusic(hms, lpBuffer, &dwBufferCount);

		g_hheap = HeapCreate(0, 4096, 0);
		if (g_hheap == NULL)
			return 0;

		if (!ReadMidiFile(szFileName)) {
			MessageBox(NULL, TEXT("MIDIファイルの読み込みに失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}

		dwBufferCount = GetBufferCount(dwMaxBufferSize);
		SetBuffer(&lpBuffer, dwBufferCount, dwMaxBufferSize);
		PlayMusic(hms, lpBuffer, dwBufferCount);

		g_bPlayMusic = TRUE;

		return 0;
	}
	
	case MM_MOM_DONE:
		if (dwBufferResetCount == 0) {
			LPMIDIHDR lpMhdr = (LPMIDIHDR)lParam;
			
			midiOutUnprepareHeader((HMIDIOUT)wParam, lpMhdr, sizeof(MIDIHDR));
			PlayMusic(hms, lpMhdr, 1);
		}
		else // midiStreamStopによってバッファが返ってきたため、バッファ数を減らす
			dwBufferResetCount--;
		return 0;
	
	case WM_RBUTTONDOWN:
		bPause = !bPause;
		if (bPause)
			midiStreamPause(hms);
		else
			midiStreamRestart(hms);
		return 0;

	case WM_DESTROY:
		if (hms != NULL) {
			StopMusic(hms, lpBuffer, &dwBufferCount);
			midiStreamClose(hms);
		}
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

DWORD GetBufferCount(DWORD dwMaxBufferSize)
{
	DWORD   dwTotalSize = 0;
	LPEVENT lpEvent = g_lpHeader;

	while (lpEvent != NULL) {
		dwTotalSize += sizeof(SHORTEVENT);
		if (lpEvent->state == 0xF0)
			dwTotalSize += ((lpEvent->nData + 3) / 4 ) * 4;

		lpEvent = lpEvent->lpNext;
	}

	return (dwTotalSize / dwMaxBufferSize) + 1;
}

void SetBuffer(LPMIDIHDR *lplpBuffer, DWORD dwBufferCount, DWORD dwMaxBufferSize)
{
	DWORD        i;
	DWORD        dwSize;
	LPEVENT      lpEvent = g_lpHeader;
	LPMIDIHDR    lpBuffer;
	LPSHORTEVENT lpShort;

	lpBuffer = (LPMIDIHDR)Alloc(sizeof(MIDIHDR) * dwBufferCount);

	for (i = 0; i < dwBufferCount; i++) {
		lpBuffer[i].lpData          = (LPSTR)Alloc(dwMaxBufferSize);
		lpBuffer[i].dwBufferLength  = dwMaxBufferSize;
		lpBuffer[i].dwBytesRecorded = 0;
		lpBuffer[i].dwFlags         = 0;
	}
	
	lpShort = (LPSHORTEVENT)lpBuffer[0].lpData;
	lpShort->dwDeltaTime = 0;
	lpShort->dwStreamID  = 0;
	lpShort->dwEvent     = ((DWORD)MEVT_TEMPO << 24) | 500000;
	lpBuffer[0].dwBytesRecorded = sizeof(SHORTEVENT);

	i = 0;
	
	while (lpEvent != NULL) {
		dwSize = sizeof(SHORTEVENT);
		if (lpEvent->state == 0xF0)
			dwSize += ((lpEvent->nData + 3) / 4 ) * 4;

		if (lpBuffer[i].dwBytesRecorded + dwSize > dwMaxBufferSize)
			i++;
		
		lpShort = (LPSHORTEVENT)(lpBuffer[i].lpData + lpBuffer[i].dwBytesRecorded);
		lpShort->dwDeltaTime = lpEvent->dwDelta;
		lpShort->dwStreamID  = 0;
		
		if (lpEvent->state == 0xFF) {
			if (lpEvent->type == 0x51) {
				lpShort->dwEvent  = (DWORD)(lpEvent->lpData[2] | (lpEvent->lpData[1] << 8) | (lpEvent->lpData[0] << 16));
				lpShort->dwEvent |= ((DWORD)MEVT_TEMPO << 24);
			}
			else
				lpShort->dwEvent = (DWORD)(MEVT_NOP << 24);
		}
		else if (lpEvent->state == 0xF0) {
			int       j;
			int       nData;
			LPBYTE    lp;
			MIDIEVENT *lpSysEx = (MIDIEVENT *)lpShort;

			nData = ((lpEvent->nData + 3) / 4 ) * 4;
			lpSysEx->dwEvent = MEVT_F_LONG | nData;

			lp = (LPBYTE)lpSysEx->dwParms;

			for (j = 0; j < lpEvent->nData; j++)
				lp[j] = lpEvent->lpData[j];
			
			for (; j < nData; j++)
				lp[j] = 0;
		}
		else {
			lpShort->dwEvent = (DWORD)(lpEvent->state | (lpEvent->data1 << 8) | (lpEvent->data2 << 16));
			lpShort->dwEvent |= MEVT_F_SHORT;
		}
		
		lpBuffer[i].dwBytesRecorded += dwSize;

		lpEvent = lpEvent->lpNext;
	}

	*lplpBuffer = lpBuffer;
}

void PlayMusic(HMIDISTRM hms, LPMIDIHDR lpBuffer, DWORD dwBufferCount)
{
	DWORD           i;
	MIDIPROPTIMEDIV mptv;
	
	mptv.cbStruct  = sizeof(MIDIPROPTIMEDIV);
	mptv.dwTimeDiv = g_wTime;
	midiStreamProperty(hms, (LPBYTE)&mptv, MIDIPROP_TIMEDIV | MIDIPROP_SET);

	for (i = 0; i < dwBufferCount; i++) {
		midiOutPrepareHeader((HMIDIOUT)hms, &lpBuffer[i], sizeof(MIDIHDR));
		midiStreamOut(hms, &lpBuffer[i], sizeof(MIDIHDR));
	}

	midiStreamRestart(hms);
}

void StopMusic(HMIDISTRM hms, LPMIDIHDR lpBuffer, LPDWORD lpdwBufferCount)
{
	if (g_bPlayMusic) {
		DWORD i;

		midiStreamStop(hms);
		
		for (i = 0; i < *lpdwBufferCount; i++)
			midiOutUnprepareHeader((HMIDIOUT)hms, &lpBuffer[i], sizeof(MIDIHDR));

		*lpdwBufferCount = 0;
		g_bPlayMusic = FALSE;
	}
	
	if (g_hheap != NULL) {
		HeapDestroy(g_hheap);
		g_hheap = NULL;
	}
}

BOOL ReadMidiFile(LPTSTR lpszFileName)
{
	HMMIO   hmmio;
	WORD    i;
	WORD    wTrack;
	WORD    wFormat;
	DWORD   dwMagic;
	DWORD   dwDataLen;
	LPEVENT *lplpEvent; // 各トラック内の最初のイベントを指すポインタ配列

	hmmio = mmioOpen(lpszFileName, NULL, MMIO_READ);
	if (hmmio == NULL) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}

	mmioRead(hmmio, (HPSTR)&dwMagic, sizeof(DWORD));
	if (dwMagic != *(LPDWORD)"MThd") {
		mmioClose(hmmio, 0);
		return FALSE;
	}

	ReadAndReverse(hmmio, &dwDataLen, sizeof(DWORD));
	if (dwDataLen != 6) {
		mmioClose(hmmio, 0);
		return FALSE;
	}

	ReadAndReverse(hmmio, &wFormat, sizeof(WORD));
	ReadAndReverse(hmmio, &wTrack, sizeof(WORD));
	ReadAndReverse(hmmio, &g_wTime, sizeof(WORD));

	lplpEvent = (LPEVENT *)Alloc(sizeof(DWORD) * wTrack);
	
	for (i = 0; i < wTrack; i++) {
		if (!ReadTrack(hmmio, &lplpEvent[i])) {
			MessageBox(NULL, TEXT("不正なトラックが存在します。"), NULL, MB_ICONWARNING);
			mmioClose(hmmio, 0);
			return FALSE;
		}
	}

	if (wFormat == 0)
		g_lpHeader = lplpEvent[0];
	else
		g_lpHeader = MargeTrack(lplpEvent, wTrack);
	
	mmioClose(hmmio, 0);
	
	return TRUE;
}

BOOL ReadTrack(HMMIO hmmio, LPEVENT *lplpEvent)
{
	BYTE    statePrev = 0; // 前のイベントのステータスバイト
	DWORD   dwLen;
	DWORD   dwMagic;
	LPEVENT lpEvent;
	
	mmioRead(hmmio, (HPSTR)&dwMagic, sizeof(DWORD));
	if (dwMagic != *(LPDWORD)"MTrk")
		return FALSE;
	
	ReadAndReverse(hmmio, &dwLen, sizeof(DWORD));

	lpEvent = (LPEVENT)Alloc(sizeof(EVENT)); // 最初のイベントのメモリを確保

	*lplpEvent = lpEvent; // *lplpEventは常に最初のイベントを指す
	
	for (;;) {
		ReadDelta(hmmio, &lpEvent->dwDelta); // デルタタイムを読み込む
		
		mmioRead(hmmio, (HPSTR)&lpEvent->state, sizeof(BYTE)); // ステータスバイトを読み込む
		if (!(lpEvent->state & 0x80)) { // ランニングステータスか
			lpEvent->state = statePrev; // 一つ前のイベントのステータスバイトを代入
			mmioSeek(hmmio, -1, SEEK_CUR); // ファイルポインタを一つ戻す
		}
		
		switch (lpEvent->state & 0xF0) { // ステータスバイトを基にどのイベントか判別

		case 0x80:
		case 0x90:
		case 0xA0:
		case 0xB0:
		case 0xE0:
			mmioRead(hmmio, (HPSTR)&lpEvent->data1, sizeof(BYTE));
			mmioRead(hmmio, (HPSTR)&lpEvent->data2, sizeof(BYTE));
			break;
		case 0xC0:
		case 0xD0:
			mmioRead(hmmio, (HPSTR)&lpEvent->data1, sizeof(BYTE));
			lpEvent->data2 = 0;
			break;
		
		case 0xF0:
			if (lpEvent->state == 0xF0) { // SysExイベント
				mmioRead(hmmio, (HPSTR)&lpEvent->nData, sizeof(BYTE));

				lpEvent->lpData = (LPBYTE)Alloc(lpEvent->nData + 1); // 先頭の0xF0を含める
				lpEvent->lpData[0] = lpEvent->state; // 可変長データの先頭は0xF0
				mmioRead(hmmio, (HPSTR)(lpEvent->lpData + 1), lpEvent->nData);

				lpEvent->nData++;
			}
			else if (lpEvent->state == 0xFF) { // メタイベント
				DWORD dw;
				DWORD tmp;
				
				mmioRead(hmmio, (HPSTR)&lpEvent->type, sizeof(BYTE)); // typeの取得

				dw = (DWORD)-1;

				switch (lpEvent->type) {

				case 0x00: dw = 2; break;
				case 0x01:
				case 0x02:
				case 0x03:
				case 0x04:
				case 0x05:
				case 0x06:
				case 0x07:
				case 0x08:
				case 0x09: break;
				case 0x20: dw = 1; break; 
				case 0x21: dw = 1; break; 
				case 0x2F: dw = 0; break; // エンドオブトラック
				case 0x51: dw = 3; break; // セットテンポ
				case 0x54: dw = 5; break;
				case 0x58: dw = 4; break;
				case 0x59: dw = 2; break;
				case 0x7F: break;

				default:
					MessageBox(NULL, TEXT("存在しないメタイベントです。"), NULL, MB_ICONWARNING);
					return FALSE;

				}
				
				tmp = dw;

				if (dw != -1) { // データ長は固定か
					ReadDelta(hmmio, &dw);
					if (dw != tmp) {
						MessageBox(NULL, TEXT("固定長メタイベントのデータ長が不正です。"), NULL, MB_ICONWARNING);
						return FALSE;
					}
				}
				else 
					ReadDelta(hmmio, &dw); // 任意のデータ長を取得

				lpEvent->nData  = dw;
				lpEvent->lpData = (LPBYTE)Alloc(lpEvent->nData);
				mmioRead(hmmio, (HPSTR)lpEvent->lpData, lpEvent->nData); // データの取得
				
				if (lpEvent->type == 0x2F) // トラックの終端
					return TRUE;
			}
			else
				;

			break;

		default:
			MessageBox(NULL, TEXT("ステータスバイトが不正です。"), NULL, MB_ICONWARNING);
			return FALSE;

		}
		
		statePrev = lpEvent->state; // 次のイベントが前のイベントのステータスバイトを確認できるように保存する
		
		lpEvent->lpNext = (LPEVENT)Alloc(sizeof(EVENT)); // 次のイベントのためにメモリを確保
		lpEvent = lpEvent->lpNext;
		if (lpEvent == NULL)
			break;
	}

	return FALSE;
}

LPEVENT MargeTrack(LPEVENT *lplpEvent, WORD wTruck)
{
	int     i;
	int     nIndex;         // トラックのインデックス
	DWORD   dwAbsolute;     // 絶対時間
	DWORD   dwPrevAbsolute; // 一つ前の絶対時間
	LPEVENT lpHeader;       // 新しい一連のイベントの先頭を指す
	LPEVENT lpEvent;        // 現在のイベント
	LPDWORD lpdwTotal;      // 各トラックの絶対時間

	lpHeader = (LPEVENT)Alloc(sizeof(EVENT));
	
	lpEvent = lpHeader;

	dwPrevAbsolute = 0;
	
	lpdwTotal = (LPDWORD)Alloc(sizeof(DWORD) * wTruck);

	for (;;) {
		nIndex = -1;
		dwAbsolute = (DWORD)-1; // 0xFFFFFFFF

		for (i = 0; i < wTruck; i++) {
			if (lplpEvent[i]->lpNext == NULL) // トラックの終端まで走査した
				continue;

			if (lpdwTotal[i] + lplpEvent[i]->dwDelta < dwAbsolute) { // 最も絶対時間が低いイベントを見つける
				nIndex = i; // イベントがどのトラックのものかを識別するため
				dwAbsolute = lpdwTotal[i] + lplpEvent[i]->dwDelta;
			}
		}

		if (nIndex == -1) // 全てのトラックを走査した
			break;

		lpEvent->state   = lplpEvent[nIndex]->state;
		lpEvent->data1   = lplpEvent[nIndex]->data1;
		lpEvent->data2   = lplpEvent[nIndex]->data2;
		lpEvent->type    = lplpEvent[nIndex]->type;
		lpEvent->nData   = lplpEvent[nIndex]->nData;
		lpEvent->dwDelta = dwAbsolute - dwPrevAbsolute;

		if (lpEvent->nData != 0) {
			lpEvent->lpData = (LPBYTE)Alloc(lpEvent->nData);
			CopyMemory(lpEvent->lpData, lplpEvent[nIndex]->lpData, lpEvent->nData);
		}
		
		dwPrevAbsolute = dwAbsolute;
		
		lpdwTotal[nIndex] += lplpEvent[nIndex]->dwDelta; // 各トラックの絶対時間を更新

		lplpEvent[nIndex] = lplpEvent[nIndex]->lpNext;
		
		lpEvent->lpNext = (LPEVENT)Alloc(sizeof(EVENT));
		lpEvent = lpEvent->lpNext;
	}

	return lpHeader;
}

void ReadAndReverse(HMMIO hmmio, LPVOID lpData, DWORD dwSize)
{
	BYTE   i;
	BYTE   tmp;
	LPBYTE lp = (LPBYTE)lpData;
	LPBYTE lpTail = lp + dwSize - 1;

	mmioRead(hmmio, (HPSTR)lp, dwSize);
	
	for (i = 0; i < dwSize / 2; i++) {
		tmp = *lp;
		*lp = *lpTail;
		*lpTail = tmp;

		lp++;
		lpTail--;
	}
}

void ReadDelta(HMMIO hmmio, LPDWORD lpdwDelta)
{
	int  i;
	BYTE tmp;
	
	*lpdwDelta = 0;

	for (i = 0; i < sizeof(DWORD); i++) {
		mmioRead(hmmio, (HPSTR)&tmp, sizeof(BYTE));

		*lpdwDelta = ( (*lpdwDelta) << 7 ) | (tmp & 0x7F);

		if (!(tmp & 0x80)) // MSBが立っていないならば、次のバイトはデルタタイムではないので抜ける
			break;
	}
}

LPVOID Alloc(DWORD dwSize)
{
	return HeapAlloc(g_hheap, HEAP_ZERO_MEMORY, dwSize);
}

BOOL SelectMidiFile(HWND hwnd, LPTSTR lpszFileName)
{
	OPENFILENAME ofn;

	lpszFileName[0] = '\0'; // 要初期化

	ZeroMemory(&ofn, sizeof(OPENFILENAME));
	ofn.lStructSize = sizeof(OPENFILENAME);
	ofn.hwndOwner   = hwnd;
	ofn.lpstrFilter = TEXT("MIDI File (*.mid)\0*.mid\0\0");
	ofn.lpstrFile   = lpszFileName;
	ofn.nMaxFile    = MAX_PATH;
	ofn.lpstrTitle  = TEXT("MIDIファイル読み込み");
	ofn.Flags       = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;

	if (!GetOpenFileName(&ofn))
		return FALSE;

	return TRUE;
}

midiStreamOpenの第6引数にCALLBACK_WINDOWを指定し、 第4引数にウインドウハンドルを指定しているため、 アプリケーションにバッファが返されたタイミングをMM_MOM_DONEで知ることができます。 既に述べたように、MM_MOM_DONEはバッファの再生が終了した場合に送られますが、 これはmidiStreamStopの呼び出しによっても送られます。 つまり、MIDIの停止時に一通りバッファがアプリケーションに返されます。 停止という理由で返ってきた場合は、バッファをMIDIストリームに書き込むわけにはいきませんから、 少し処理を工夫することになります。

case MM_MOM_DONE:
	if (dwBufferResetCount == 0) {
		LPMIDIHDR lpMhdr = (LPMIDIHDR)lParam;
		
		midiOutUnprepareHeader((HMIDIOUT)wParam, lpMhdr, sizeof(MIDIHDR));
		PlayMusic(hms, lpMhdr, 1);
	}
	else // midiStreamStopによってバッファが返ってきたため、バッファ数を減らす
		dwBufferResetCount--;
	return 0;

dwBufferResetCountが0の場合は、 再生が終了したことでバッファがアプリケーションに返されたことを意味します。 よって、このときはバッファをMIDIストリームに書き込むべきであるため、 lParamから取得したバッファをPlayMusicに指定するようにしています。 ただし、事前にmidiOutUnprepareHeaderで既に行われていた準備を解除しておく必要があります。 一方、dwBufferResetCountが0でない場合は、 midiStreamStopによってバッファが返されたことを意味します。 このようなことが判断できるのは、次のコードを実行しているからです。

dwBufferResetCount = dwBufferCount;
StopMusic(hms, lpBuffer, &dwBufferCount);

StopMusicは内部でmidiStreamStopを呼び出し、dwBufferCountを0に初期化しますが、 その前に現在の再生に使用しているバッファ数をdwBufferResetCountに保存しておきます。 midiStreamStopによって返されるバッファ数はdwBufferResetCountと同じですから、 これを1つずつ減らしていき、0になった場合は全てのバッファが返ったことを意味します。 そして、このようなdwBufferResetCountが0の場合にMM_MOM_DONEに送られたということは、 バッファの再生が終了したものと解釈することができます。

midiStreamStopには、テンポを自動で初期化しないという問題があります。 たとえば、あるMIDIファイルにテンポを600000にするセットテンポイベントが含まれているとして、 これを再生した場合は、MIDIストリームのテンポが600000に設定されます。 そして、次のMIDIファイルを再生することになった場合、 midiStreamStopで現在再生中のMIDIを停止するわけですが、 この停止と同時にMIDIストリームのテンポはデフォルトである500000に初期化されません。 こうなると、これから再生するMIDIファイルにセットテンポイベントが含まれない場合、 そのMIDIファイルは600000のテンポで再生されることになり、 デフォルトのテンポが適応されていません。 この問題に対処するには、バッファに設定する最初のイベントに、 値が500000であるセットテンポイベントを明示的に設定します。

lpShort = (LPSHORTEVENT)lpBuffer[0].lpData;
lpShort->dwDeltaTime = 0;
lpShort->dwStreamID  = 0;
lpShort->dwEvent     = ((DWORD)MEVT_TEMPO << 24) | 500000;
lpBuffer[0].dwBytesRecorded = sizeof(SHORTEVENT);

dwEventに500000を指定すると共に、最上位バイトにMEVT_TEMPOを指定します。 これにより、セットテンポイベントが必ず考慮されることになります。 1つイベントを設定したので、dwBytesRecordedの更新を忘れずに行っておきます。


戻る