EternalWindows
MIDI / MIDI再生サンプル(timeSetEvent編)

MIDIの再生は、一定の間隔でメッセージをMIDIデバイスに送信することで行われます。 これまでのプログラムでは、独自のスレッドを作成して そこでSleepを呼び出すことで送信のタイミングを調整していましたが、 決してこれ以外に方法がないわけではありません。 マルチメディアタイマ関数のtimeSetEventを呼び出せば、 指定した時間にコールバック関数が呼ばれることになっているため、 その関数内でメッセージを送信することが可能です。 timeSetEventは、次のように定義されています。

MMRESULT timeSetEvent(
  UINT uDelay,               
  UINT uResolution,          
  LPTIMECALLBACK lpTimeProc, 
  DWORD dwUser,              
  UINT fuEvent               
);

uDelayは、コールバック関数を呼び出す時間をミリ秒単位で指定します。 uResolutionは、タイマイベントの分解能をミリ秒単位で指定します。 ここに指定する値が低いほど、コールバック関数が呼ばれるタイミングが正確になります。 lpTimeProcは、コールバック関数のアドレスを指定します。 dwUserは、コールバック関数に渡したいデータを指定します。 fuEventは、TIME_ONESHOTを指定するようにします。 これにより、uDelayに指定した時間にコールバック関数が一度だけ呼ばれます。 戻り値は、タイマイベントの識別子になります。 コールバック関数の型は、次のようになります。

typedef void (CALLBACK TIMECALLBACK)(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2);

uTimerIDは、タイマイベントの識別子が格納されます。 uMsgは、使用されません。 dwUserは、timeSetEventの第3引数が格納されます。 dw1とdw2は、使用されません。

不要になったタイマイベントは、timeKillEventでキャンセルすることになります。

MMRESULT timeKillEvent(
  UINT uTimerID  
);

uTimerIDは、timeSetEventの戻り値を指定します。

Windows Multimediaにおけるコールバック関数には、いくつかの制約が課せられていることがあります。 たとえば、timeSetEventにおけるコールバック関数や、midiOutOpenに指定できるMidiOutProcでは、 次に示す関数を呼び出してはならないことになっています。

EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, 
PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, timeSetEvent

これらの関数の中にmidiOutShortMsgやmidiOutLongMsgが含まれていることを考えると、 timeSetEventはMIDI再生に特化した関数であるといえるかもしれません。 実際のところ、timeSetEventにおけるタイマは非常に精度がよいため、 ミリ秒単位の動作が問われるMIDIの再生では便利な存在といえます。

timeSetEventの第2引数に高精度な分解能を指定するためには、 事前にその値をtimeBeginPeriodに指定しておく必要があります。 これにより、第2引数に指定した分解能が保障されることになります。 ところで、このtimeBeginPeriodですが、 これを呼び出した場合はSleepが制御を返す精度も変化することになっています。 こうなってくると、Sleepの精度とtimeSetEventの精度にそこまで違いが生じるとは一概に言えないため、 timeSetEventを利用したコードを好まない場合は、 独自にスレッドを作成したコードでも全く問題ないと思われます。

今回のプログラムは、timeSetEventによって呼び出されるコールバック関数で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;

WORD    g_wTime = 0;
DWORD   g_dwTempo = 0;
HANDLE  g_hheap = NULL;
LPEVENT g_lpHeader = NULL;
UINT    g_uTimerId = 0;

void StopMusic(HMIDIOUT hmo);
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);
double DeltaToMilliSecond(DWORD dwDelta);
LPVOID Alloc(DWORD dwSize);
BOOL SelectMidiFile(HWND hwnd, LPTSTR lpszFileName);
void CALLBACK TimeCallback(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2);
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 HMIDIOUT hmo = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		MMRESULT mr;

		mr = midiOutOpen(&hmo, MIDIMAPPER, 0, 0, CALLBACK_NULL);

		return mr == MMSYSERR_NOERROR ? 0 : -1;
	}
	
	case WM_LBUTTONDOWN: {
		TCHAR szFileName[MAX_PATH];

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

		StopMusic(hmo);

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

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

		g_dwTempo = 500000; // テンポのデフォルト値

		timeBeginPeriod(1);
		g_uTimerId = timeSetEvent(1, 1, TimeCallback, (DWORD_PTR)hmo, TIME_ONESHOT);

		return 0;
	}

	case WM_DESTROY:
		if (hmo != NULL) {
			StopMusic(hmo);
			timeEndPeriod(1);
			midiOutClose(hmo);
		}
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

void CALLBACK TimeCallback(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2)
{
	static HMIDIOUT hmo = NULL;
	MIDIHDR mh;
	LPEVENT lpEvent;

	if (hmo == NULL) {
		hmo = (HMIDIOUT)dwUser;
		lpEvent = g_lpHeader;
	}
	else
		lpEvent = (LPEVENT)dwUser;

	for (;;) {
		if (lpEvent->state == 0xFF) { // メタイベント
			if (lpEvent->type == 0x51) // セットテンポ
				g_dwTempo = (DWORD)(lpEvent->lpData[2] | (lpEvent->lpData[1] << 8) | (lpEvent->lpData[0] << 16));
		}
		else if (lpEvent->state == 0xF0) { // SysExイベント
			mh.lpData         = (LPSTR)lpEvent->lpData;
			mh.dwBufferLength = lpEvent->nData;
			mh.dwFlags        = 0;
		
			midiOutPrepareHeader(hmo, &mh, sizeof(MIDIHDR));
			midiOutLongMsg(hmo, &mh, sizeof(MIDIHDR));

			while ((mh.dwFlags & MHDR_DONE) == 0);

			midiOutUnprepareHeader(hmo, &mh, sizeof(MIDIHDR));
		}
		else { // MIDIイベント
			DWORD dwMsg = (DWORD)(lpEvent->state | (lpEvent->data1 << 8) | (lpEvent->data2 << 16));
			
			midiOutShortMsg(hmo, dwMsg);
		}
		
		lpEvent = lpEvent->lpNext;
		if (lpEvent == NULL)
			lpEvent = g_lpHeader;
		else if (lpEvent->dwDelta > 0) {
			g_uTimerId = timeSetEvent((DWORD)DeltaToMilliSecond(lpEvent->dwDelta), 1, TimeCallback, (DWORD_PTR)lpEvent, TIME_ONESHOT);
			break;
		}
		else
			;
	}
}

void StopMusic(HMIDIOUT hmo)
{
	if (g_uTimerId != 0) {
		int   i, j;
		DWORD dwMsg;

		timeKillEvent(g_uTimerId);
		g_uTimerId = 0;
		
		for (i = 0; i < 16; i++) {
			for (j = 0; j < 128; j++) {
				dwMsg = (0x80 + i) | (j << 8);
				midiOutShortMsg(hmo, dwMsg); // 全てのノートnoの音を消す
			}
		}
	}
	
	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;
	}
}

double DeltaToMilliSecond(DWORD dwDelta)
{
	return (dwDelta * ((double)g_dwTempo / 1000) ) / g_wTime;
}

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

今回は明示的にスレッドを作成するわけではないので、 スレッドのハンドルを表すhThreadがなくなっています。 また、スレッドが現在動作しているかを表すg_bPlayThreadという変数もなくなり、 代わりにg_uTimerIdというタイマイベントを格納する変数が宣言されています。 WM_LBUTTONDOWNではtimeSetEventを呼び出し、 これによりコールバック関数が呼ばれることになります。 最初の呼び出し時では、MIDIデバイスのハンドルを渡すべく第4引数にhmoを指定しています。 コールバック関数の実装は、次のようになっています。

void CALLBACK TimeCallback(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2)
{
	static HMIDIOUT hmo = NULL;
	MIDIHDR mh;
	LPEVENT lpEvent;

	if (hmo == NULL) {
		hmo = (HMIDIOUT)dwUser;
		lpEvent = g_lpHeader;
	}
	else
		lpEvent = (LPEVENT)dwUser;

	for (;;) {
		if (lpEvent->state == 0xFF) { // メタイベント
			if (lpEvent->type == 0x51) // セットテンポ
				g_dwTempo = (DWORD)(lpEvent->lpData[2] | (lpEvent->lpData[1] << 8) | (lpEvent->lpData[0] << 16));
		}
		else if (lpEvent->state == 0xF0) { // SysExイベント
			mh.lpData         = (LPSTR)lpEvent->lpData;
			mh.dwBufferLength = lpEvent->nData;
			mh.dwFlags        = 0;
		
			midiOutPrepareHeader(hmo, &mh, sizeof(MIDIHDR));
			midiOutLongMsg(hmo, &mh, sizeof(MIDIHDR));

			while ((mh.dwFlags & MHDR_DONE) == 0);

			midiOutUnprepareHeader(hmo, &mh, sizeof(MIDIHDR));
		}
		else { // MIDIイベント
			DWORD dwMsg = (DWORD)(lpEvent->state | (lpEvent->data1 << 8) | (lpEvent->data2 << 16));
			
			midiOutShortMsg(hmo, dwMsg);
		}
		
		lpEvent = lpEvent->lpNext;
		if (lpEvent == NULL)
			lpEvent = g_lpHeader;
		else if (lpEvent->dwDelta > 0) {
			g_uTimerId = timeSetEvent((DWORD)DeltaToMilliSecond(lpEvent->dwDelta), 1, TimeCallback, (DWORD_PTR)lpEvent, TIME_ONESHOT);
			break;
		}
		else
			;
	}
}

最初にTimeCallbackが呼ばれた時は、hmoがNULLになっているため第3引数に格納されている MIDIデバイスのハンドルを指定することになります。 また、lpEventが一連のイベントの先頭を表すようにg_lpHeaderを指定します。 1つのイベントの処理を終え、lpEvent->lpNextから次のイベントを参照したら、 まずこれがNULLであるかを調べます。 NULLである場合は、次に処理するイベントが存在しないことを意味するため、 g_lpHeaderを指定することにより最初のイベントから処理することになります。 デルタタイムが0より大きい場合は、その時間だけ待機しなければなりませんから、 時間をtimeSetEventの第1引数に指定し、ループ文から抜けて関数を終了します。 これでどうなるかというと、指定した時間にTimeCallbackが呼ばれることになります。 このときは、dwUserから今回処理すべきイベントを参照することができます。 デルタタイムが0より多くない場合、つまり0である場合は待機する必要がないということなので、 ループすることで次のイベントを処理します。


戻る