EternalWindows
MIDI / MIDIストリームによる再生

低水準な方法でMIDIを再生したいと考えた場合、MIDI API(midiOutShortMsg)の他に、 MIDIストリームによる再生方法があります。 この再生方法でも、トラックからイベントを読み込みマージするという手順は踏みますが、 再生を関数側に任せるという点で、MIDI APIと一線を画しています。 この場合の任せるというのは、個々のイベントをアプリケーションが再生しないという意味で、 アプリケーションが行うのは複数のイベントをバッファという単位にまとめ、 それをMIDIストリーム関数に指定することです。 MIDIストリーム関数は、このバッファに格納されたイベントを順次デバイスに送信していきます。 MIDIストリーム関数に再生を任せることによって、アプリケーション側に次の利点が生じます。

・再生をするためのスレッドを作成する必要がない。
・デルタタイムからミリ秒への変換を行う必要がない。
・一時停止や現在位置を取得が容易である(専用の関数が用意されている)。
・リセット(midiStreamStop)により、発音中の音声が確実に消音される。

このように利点だけ注目すればMIDIストリームは便利に思えますが、 これを利用するまでの手続きの量は相当なものです。 具体的には、バッファにイベントを設定する処理と、 ループ再生を行うための処理が非常に複雑なのです。 私的な見解ではありますが、MIDIファイルからイベントを取得する方法を理解したのにも関わらず、 最後の再生部分をMIDIストリームに任せる必要は全くないと思います。 MIDI APIでもMIDIストリームが可能なことは実現できますし、 柔軟性という点でもMIDI APIの方が優れているといえます。

MIDIストリームを利用するには、まずmidiStreamOpenでMIDIストリームをオープンすることになります。 MIDIストリームは、オープンした時点では一時停止状態となっています。

MMRESULT midiStreamOpen(
  LPHMIDISTRM lphStream, 
  LPUINT puDeviceID,     
  DWORD cMidi,           
  DWORD dwCallback,      
  DWORD dwInstance,      
  DWORD fdwOpen          
);

lphStreamは、MIDIストリームのハンドルを受け取る変数のアドレスを指定します。 ここ述べているMIDIストリームとは、MIDIデバイスの事を意味すると考えて差し支えありません。 puDeviceIDは、MIDIデバイスの識別子を指定します。 cMidiは、予約されており、必ず1を指定します。 dwCallbackは、コールバック機能を利用するためのハンドルを指定します。 dwInstanceは、dwCallbackに関数のアドレスを指定した場合に利用します。 fdwOpenは、コールバックに関する定数を指定します。 CALLBACK_WINDOWを指定すると、dwCallbackにウインドウハンドルを指定することができます。

MIDIストリームのハンドルを取得したら、 midiStreamOutでバッファ(ストリームとも呼ぶ)をMIDIストリームに書き込みます。

MMRESULT midiStreamOut(
  HMIDISTRM hMidiStream, 
  LPMIDIHDR lpMidiHdr,   
  UINT cbMidiHdr         
);

hMidiStreamは、MIDIストリームのハンドルを指定します。 lpMidiHdrは、MIDIHDR構造体のアドレスを指定します。 cbMidiHdrは、lpMidiHdrのサイズを指定します。

MIDIを再生するためには、MIDIイベントやSysイベント以外の情報も必要になります。 たとえば、デルタタイムから秒への変換には時間単位が必要ですから、 これをMIDIストリームに伝えておく必要があります。 これには、midiStreamPropertyを呼び出します。

MMRESULT midiStreamProperty(
  HMIDISTRM hm,      
  LPBYTE lppropdata, 
  DWORD dwProperty   
);

hmは、MIDIストリームのハンドルを指定します。 lppropdataは、MIDIPROPTIMEDIV構造体のアドレスを指定します。 dwPropertyは、プロパティの取得を表すMIDIPROP_GETか、 プロパティの設定を表すMIDIPROP_SETを指定します。 また、時間単位を示すMIDIPROP_TIMEDIVも指定します。

オープンされたMIDIストリームは既定で一時停止状態になっているため、 これを再開するためにmidiStreamRestartを呼び出します。

MMRESULT midiStreamRestart(
  HMIDISTRM hms 
);

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

再生されているMIDIは、midiStreamStopで停止することができます。

MMRESULT midiStreamStop(
  HMIDISTRM hms 
);

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

不要になったMIDIストリームのハンドルは、midiStreamCloseで閉じることになります。

MMRESULT midiStreamClose(
  HMIDISTRM hStream 
);

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

今回のプログラムは、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;

	switch (uMsg) {

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

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

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

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

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

まず、今回のプログラムのコードが、MIDI APIを使用したコードと似ている点に注目してください。 MIDIストリームを使用する場合でも、MIDIファイルからイベントを取得するコードは必要になりますから、 ReadTrackやMargeTrackといった関数は依然として必要です。 ただし、MIDIストリームでは、再生をアプリケーションが明示的に行うわけではないため、 ThreadProcやDeltaToMilliSecondという関数はなくなっています。 代わりとして存在するのが、イベントの総数から必要なバッファ数を求めるGetBufferCount、 バッファにイベントを設定するSetBuffer、バッファをmidiStreamOutに指定するPlayMusicとなっています。 各関数の実装を順に見ていきます。

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

バッファ(MIDIHDR構造体)にイベントを設定するためには、 全てのイベントのサイズの合計値が必要になります。 理由は、バッファのサイズに上限があるため、全てのイベントをバッファに指定することができないためです。 よって、lpEventがNULLになるまでイベントを走査し、サイズをdwTotalSizeに加算していきます。 1つのイベントに必要なサイズは自作構造体のSHORTEVENT構造体で表され、 このサイズは最低限どのイベントにも必要になります。 また、0xF0であるSysExイベントはサイズが可変長であるため、 可変長サイズの分もdwTotalSizeに加算する必要があります。 可変長サイズは4の倍数になっていなければならない決まりがあるため、 nDataを直接指定しないようにしています。 イベントの合計サイズを取得したら、これをバッファの上限サイズで除算します。 これにより、必要なバッファ数を求めることができます。 バッファ数は1以上でなければならないため、除算結果に1を足すことになります。

バッファのサイズは、64Kbを超えてはならないことになっています。 したがって、dwMaxBufferSizeには65536(64*1024)を指定しなければならないように思えますが、 実際にはこの値からsizeof(MIDIHDR)を引いた値を指定しています。 一連のイベントはMIDIHDR.lpDataに指定することになりますが、 このデータのサイズとMIDIHDR構造体の残りのメンバを合わせて64Kbを超えてはならないということなので、 lpData単体で64Kbのサイズを使用するわけにはいきません。 よって、他のメンバも考慮してsizeof(MIDIHDR)を引くようにしています

SetBufferでは、MIDIHDR.lpDataに一連のイベントを設定する処理を行っています。 まず、必要なバッファを確保する処理を見てみます。

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

dwBufferCountは、GetBufferCountが返した値です。 この数だけMIDIHDR構造体を確保し、各構造体のメンバを初期化します。 lpDataには、一連のイベントを設定することになるので、 dwMaxBufferSize文のメモリを確保しておきます。 また、dwBufferLengthはlpDataのサイズを指定します。 dwBytesRecordedとdwFlagsは0に初期化しておきます。

MIDIHDR.lpDataのメモリを確保したら、そこに一連のイベントを設定することになります。 まずは、どのようなイベントにも共通して行われる処理を見てみます。

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;
	
	// イベント固有の処理を行う
	
	lpBuffer[i].dwBytesRecorded += dwSize;

	lpEvent = lpEvent->lpNext;
}

まず、1つのイベントに必要なサイズをdwSizeに指定します。 続いて、このサイズ分のデータをlpBuffer[i]のlpDataに指定する空きがあるかを確認します。 dwBytesRecordedは、現在まででlpDataに設定したデータのサイズを格納しており、 これと今回のデータのサイズを足してdwMaxBufferSizeを超えてなかった場合、 lpDataにはまだイベントを設定する空きがあることになります。 逆に超えている場合は、lpDataにこれ以上イベントを設定できたないため、 i++によって次のバッファを参照できるようにします。 イベントを設定する位置はlpDataにdwBytesRecordedを足すことによって特定できるため、 その位置をSHORTEVENT構造体で表すようにします。 そして、イベントのデルタタイムを指定し、ストリームのIDには0を指定します。 注意しなければならないのは、SHORTEVENT構造体のメンバの並び方が、 デルタタイム、ストリームID、イベント値順になっていなければならないという点です。 この構造体に相当するものが予め定義されていればよいのですが、 残念ながら定義されていないため明示的に定義する必要があります。 イベント固有の処理が終われば、lpDataに1つのイベントを設定したということで、 dwBytesRecordedを更新し、lpEvent->lpNextから次のイベントを参照することになります。

イベント固有の処理を順に見ていきます。

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

このコードは、メタイベントを検出した場合の処理です。 タイプが0x51であるセットテンポは、再生スピード変化するために必要なイベントですから、 他のタイプと異なる処理を実行しています。 まず、テンポの値をdwEventに指定し、次にMEVT_TEMPOという値をdwEventの最上位バイトに指定します。 これは一種のフラグであり、このフラグが設定されていることで、 関数側はこのイベントがテンポを格納しているものと判断します。 セットテンポでないメタイベントについては、特に考慮する必要がないため、 dwEventの最上位バイトにMEVT_NOPを指定します。 これにより、このイベントは再生時に考慮されなくなります。

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

このコードは、SysExイベントを検出した場合の処理です。 まず、可変長データのサイズを4の倍数に変換し、これをdwEventに指定します。 MEVT_F_LONGはイベントがSysExイベントであることを示すフラグであり、 これを指定することでSysExイベントが考慮されることになります。 MEVT_F_LONGのようなMEVT_F_XXXの定数は、最初から最上位バイトのみにビットが立つようになっているため、 最上位バイトまでシフトさせる必要はありません。 実際のデータはdwParmsに指定することになっており、 データのサイズだけこれをコピーします。 ただし、可変長データのサイズは4の倍数で指定しているため、 lpEvent->nDataが4の倍数でない場合は、データが格納されていない空白の領域できてしまいます。 これを0で埋めるのが2つ目のfor文です。

else {
	lpShort->dwEvent = (DWORD)(lpEvent->state | (lpEvent->data1 << 8) | (lpEvent->data2 << 16));
	lpShort->dwEvent |= MEVT_F_SHORT;
}

このコードは、MIDIイベントを検出した場合の処理です。 dwEventは、midiOutShortMsgの第2引数と同じように指定します。 MEVT_F_SHORTという定数は、イベントがMIDIイベントであることを示すフラグですが、 これは0として定義されているため、指定しなくても問題ありません。

バッファにイベントを設定したら、後はバッファをMIDIストリームに書き込むだけです。 これは、PlayMusicで行います。

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

まず、midiStreamPropertyで時間単位の設定を行います。 これには、MIDIPROPTIMEDIV.dwTimeDivに時間単位を指定し、 midiStreamPropertyの第3引数にMIDIPROP_TIMEDIVとMIDIPROP_SETを指定します。 続いて、バッファの数だけmidiStreamOutを呼び出し、バッファをMIDIストリームに書き込みます。 ただし、このためにはmidiOutPrepareHeaderを呼び出して、バッファを事前に準備しておく必要があります。 MIDIストリームは一時停止状態でオープンされているため、 midiStreamOutを呼び出しても直ちにMIDIが再生されることはありません。 midiStreamRestartで一時停止状態を解除してから再生されることになります。

MIDIストリームにおける停止処理は、StopMusicで次のように行われています。

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

midiStreamStopを呼び出せば、現在再生中の音は消音されることになります。 midiOutResetと違って、この関数はどのようなデバイスでも音を確実に消音できるように思えます。 続いて、midiOutPprepareHeaderで準備していたバッファを、 midiOutUnprepareHeaderを呼び出して準備を解除します。 そして、バッファの数を0に指定し、MIDIの再生フラグをFALSEにします。


戻る