トップ 戻るWindowsプロフェッショナルゲームプログラミング2【CD-ROM付】

マルチメディアタイマとPerformanceCounter

プログラムの中で一定時間でちょっとした処理を繰り返したい、という時にはタイマを使用して指定ウインドウに一定時間毎にメッセージを送るようにすると手軽に実現できます。ただ、このタイマは精度が低いため、本当に時間(周期)をあまり気にしない処理にしか使えないのが難点です。
今回は、この通常のタイマより精度が高いマルチメディアタイマを使ってみることにします。また、このマルチメディアタイマの精度を計るために、マイクロ秒(環境によってはナノ秒?)単位で時間を測定できるPerformanceCounterも試してみました

マルチメディアタイマ

マルチメディアタイマは、起動されると指定した時間ごとにコールバック関数を呼び出します。コールバック関数は別スレッドで呼び出され、(後ほど測定しますが)周期はかなり安定しているようです。ほぼ1ミリ秒(以下)単位の精度が得られることが多いので、用途としてはゲームのBGM演奏などが考えられるでしょう。
というか、実はマルチメディアタイマから呼び出されるコールバック関数内では、できることがMIDIデバイスの操作とウインドウへのメッセージ送信程度に制限されているので、ほとんどMIDI再生専用の機能だったりします(^^;。まあ、(今回のプログラムのように)コールバック関数内からウインドウにメッセージを送れば、「高精度の汎用タイマ」として使えないこともないんでしょうが……。

マルチメディアタイマを起動するには、呼び出し周期と精度(ともにミリ秒単位で指定)、コールバック関数のポインタ、コールバック関数に渡すデータのポインタ、フラグを引数にtimeSetEvent()を呼び出します。フラグの指定によっては、指定時間経過後に一度だけ動作するタイマを作成したり、イベントをセットしたりすることもできます。

StretchDIBits
MMRESULT timeSetEvent(
  UINT           uDelay,
  UINT           uResolution,
  LPTIMECALLBACK lpTimeProc,
  DWORD_PTR      dwUser,
  UINT           fuEvent
);
uDelayタイマの周期(一度だけの場合は遅延時間)
uResolutionタイマの分解能
lpTimeProcコールバック関数のアドレス
DWORD_PTRコールバックデータ
fuEventタイマの種類を指定
  /* マルチメディアタイマ起動 */
  dwTimerID = timeSetEvent(5, 4, timerFunc, 0, TIME_PERIODIC);

こうしてタイマを起動すると、指定した周期でtimerFunc()が呼び出されるので、そこにMIDIデバイスの制御などのタイマを使って行いたい処理を書きます。タイマを使い終わったら、timeKillEvent()で終了します。

今回は、このタイマイベントでマルチメディアタイマの周期を測定するために「100回呼び出させる度にメインウインドウにメッセージを送る」ようにしました。

マルチメディアコールバック関数
/* マルチメディアタイマコールバック関数 */
void CALLBACK timerFunc(UINT uiID, UINT uiNo, DWORD dwCookie, DWORD dwNo1, DWORD dwNo2) {

	static int i = 0;

	/* 呼び出しカウンタ更新 */
	i++;

	 /* 100回呼び出される度にメッセージ送信 */
	if (i == 100) {

		PostMessage(g_hwMain, WM_TIMER_100, 0, 0);

		/* 呼び出しカウンタクリア */
		i = 0;

	}

}

PerformanceCounter

マルチメディアタイマのコールバック関数から送られてきたメッセージの処理では、メッセージが送られてくる間隔を高精度で測定するためにQueryPerformanceCounter()を使用します。このAPIは、timeGetTime()以上の高精度カウンタで周波数をQueryPerformanceFrequency()で取得できますが、私の環境(i845G+Pentium4 2.4B)では350万Hz程度でした。この周波数どおりなら、マイクロ秒単位まではほぼ正確に取得できる、といえそうですね。

ただ、このカウンタの値、「64ビット」で返ってくるのでちょっと面倒かもしれません。今回はLARGE_INTEGER型に入れて処理しましたが、DWORD/intとの間で計算をする時は、計算順位やキャストに注意が必要でしょう。

メッセージ処理
case WM_TIMER_100:

	i++;

	/* 現カウント値取得 */
	QueryPerformanceCounter(&liNow);

	/* 前回からの経過時間をミリ秒単位で算出 */
	dwTime = (DWORD)(((liNow.QuadPart - liPrev.QuadPart) * 1000) / liFreq.QuadPart);

	/* 表示用文字列作成 */
	wsprintf(aszStr, "%d - %I64dHz\r\n周期%dms", i, liFreq.QuadPart, dwTime);

	/* 文字列を表示 */
	SetWindowText(g_hwView, aszStr);

	/* 今回のカウントを保存 */
	liPrev = liNow;

	return 0;

PerformanceCounterは、CPUの周波数が動的に変わる場合や複数のCPUがある場合などはそのままでは正確なカウントが行えず注意が必要なようです。最近は省電力のため動的にCPU周波数を変えるCPUが増えてきたので、この点も意識しておかないと思わぬ結果につながりそうですね。

プログラム

起動すると、マルチメディアタイマを起動し、PerformanceCounterの周波数と100回処理が行われるのに要した時間を表示します。タイマ起動時の周期や精度指定をいろいろ変えて試してみましょう。できれば、OSCPUが違う環境で試してみると面白いかもしれません。

プログラムソース表示 プログラムダウンロード


トップ 戻る