EternalWindows
MIDI / デルタタイム

今回は、トラック内に格納されているイベントを実際に取得するコードを示します。 まず、イベントがどのような順序でトラック内に格納されているかを示します。

[デルタタイム][イベント] -> [デルタタイム][イベント] -> ・・・ [デルタタイム][イベント]

上記に示すように、1つのイベントは1つのデルタタイムとセットになって格納されています。 デルタタイムは、このイベントを前のイベントの何秒後にMIDIデバイスに送信すればよいかという値です。 正確には、これはチックという単位なのですが、簡単のためここでは秒と考えます。 デルタタイムで非常に厄介なのは、サイズが最高で4バイトという点です。 つまり、1バイトのときもあれば4バイトのときもあり、サイズが可変長になっています。 取得したバイトのMSB(最上位ビット)が1ならば、次のバイトもデルタタイムであり、 MSBが0ならばデルタタイムはこのバイトで最後であることを意味するため、 この仕様を意識しながら読み取っていく必要があります。

今回のプログラムは、前節で作成したReadTruck関数にデルタタイムとイベントを取得するコードを加えたものです。 不正なイベントが含まれている場合などは、警告を表示します。

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

HANDLE g_hheap = NULL;

BOOL ReadMidiFile(LPTSTR lpszFileName);
BOOL ReadTrack(HMMIO hmmio, LPEVENT *lplpEvent);
void ReadAndReverse(HMMIO hmmio, LPVOID lpData, DWORD dwSize);
void ReadDelta(HMMIO hmmio, LPDWORD lpdwDelta);
LPVOID Alloc(DWORD dwSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	g_hheap = HeapCreate(0, 4096, 0);
	if (g_hheap == NULL)
		return 0;

	if (!ReadMidiFile(TEXT("sample.mid")))
		MessageBox(NULL, TEXT("MIDIファイルの読み込みに失敗しました。"), NULL, MB_ICONWARNING);

	HeapDestroy(g_hheap);

	return 0;
}

BOOL ReadMidiFile(LPTSTR lpszFileName)
{
	HMMIO   hmmio;
	WORD    i;
	WORD    wTrack;
	WORD    wFormat;
	WORD    wTime;
	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, &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;
		}
	}

	MessageBox(NULL, TEXT("全てのトラックのイベントを取得しました。"), TEXT("OK"), MB_OK);

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

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

今回のReadMidiFileには、lplpEventという変数が宣言されています。 これは、トラックに格納されている一連のイベントの先頭を指すポインタであり、 ReadTruckで取得することになります。 1つのイベントはEVENT構造体で表され、次のように定義されています。

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

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

stateは、ステータスバイトを表します。 data1は1つ目のデータバイトを表し、 data2は2つ目のデータバイトを表しています。 typeは、メタイベントの種類を表すものであり、MIDIデバイスに送信されることはありません。 nDataとlpDataは可変長データのサイズとデータを表し、 SysExイベントやメタイベントで初期化されます。 SysExイベントの場合は、データをMIDIデバイスに送信することになります。 dwDeltaは、イベントのデルタタイムを表します。 lpNextは、次のイベントへのポインタを表します。 このメンバを参照することで、一連のイベントを参照できるようになります。

ReadTrackでは最初のイベントを表すためのメモリを確保し、 ループに入って実際にEVENT構造体を初期化します。 その後、次のイベントを参照できるようにlpNextメンバを初期化し、 lpEvent = lpEvent->lpNextとすることによって、 次のイベントを表すようにします。 メモリ確保に使われているAllocという関数は、 HeapCreateで明示的に作成されたヒープからメモリを確保しています。 このようにしている理由は、 一連のイベントのために確保したメモリを1つずつ開放するのが煩わしいと思ったからです。 HeapCreateが返したヒープを使用して確保されたメモリは、 HeapDestroyの呼び出しで全て開放されることになります。 ReadTrackのループ内で行われる主な処理は、次の3つとなります。

・デルタタイムの取得
・ランニングステータスの確認
・イベントの取得(MIDIイベント、SysExイベント、メタイベントのいずれか)

今回は、デルタタイムの取得について説明します。 デルタタイムはReadDeltaという自作関数で取得され、 第2引数にそのデルタタイムが返ることになっています。 関数の内部は、次のようになっています。

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

既に述べたように、デルタタイムのサイズは可変で、その範囲は1から4バイトです。 最高4バイトであるため、lpdwDeltaの型はDWORDのポインタになります。 デルタタイムのサイズを見極めるために、まずmmioReadで1バイト読み取ります。

mmioRead(hmmio, (HPSTR)&tmp, sizeof(BYTE));

これで、tmpにデルタタイムを構成する1バイトが格納されることになります。 このtmpのMSBが1ならば、次のバイトはデルタタイムとなり、 0ならばデルタタイムはこのバイトで最後となります。 これを確認するコードは、次のようになっています。

if (!(tmp & 0x80))
	break;

この式が本当にMSBを確認できるかどうかを確認してみましょう。 tmpを0x83とし、2進数で表現します。

  10000011 // 0x83 (MSBが立っている)
& 10000000 // 0x80
----------
  10000000 // 0x80

この式ではtmpが0x83ですが、0x84であれ、0xFFであれ、結果は0x80となります。 しかし、tmpが0x80以下である0x7Fなどの場合はMSBが立っていませんから、 次のように結果は0になります。

  00001111 // 0x7F (MSBが立っていない)
& 10000000 // 0x80
----------
  00000000 // 0x00

よって、先の式はMSBが立っているかを正しく確認できているといえます。

1バイトずつ読み取ったデルタタイムは、最終的に1つのDWORD型にまとめなければなりません。 それを行うのは、次の処理です。

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

一回目のループでは、*lpdwDeltaはどうなるでしょうか。 まず、(*lpdwDelta) << 7 )の結果は0になります(lpdwDeltaは事前に0を代入している)。 (tmp & 0x7F)は以下のようになります。

  10000011 // 0x83
& 01111111 // 0x7F (MSBが必ず消えることになる)
----------
  00000011 // 0x03 (MSB以外は変化していない)

0x03と0の論理和は0x03なので、1回目の*lpdwDeltaは0x03となります。 続いて2回目のループで*lpdwDeltaがどうなるのか検討します。 0x03 << 7 はどうなるでしょうか。

   0000000 0000000 0000000 00000011 // 0x03
<<                                7
------------------------------------
   0000000 0000000 0000001 10000000 // 0x00000180

これは、デルタタイムを1つ先のバイトにシフトさせる処理です。 シフトすることにより、空きができますから、 そこに今回取得したデルタタイムを格納する狙いです。 しかし、上の式では最後の1ビットが前のバイトの最上位ビットに含まれており、 完全にシフトできていないように思えますが、これは問題ありません。 取り敢えず、先に(tmp & 0x7F)を確認しましょう。 2回目に取得したtmpは、0x72とします。

  01110010 // 0x72 (MSBが立っていないためデルタタイムはこのバイトで最後)
& 01111111 // 0x7F
----------
  01110011 // 0x72

この0x72と先に算出した0x00000180の論理和は以下のようになります。

  0000000 0000000 0000001 10000000 // 0x00000180
|                         01110010 // 0x72
------------------------------------
  0000000 0000000 0000001 11110010 // 0x000001F2

今行おうとしているのは、1回目に取得したデルタタイム(0x00000180)と2回目に取得したデルタタイムを組み合わすことです。 しかし、上の式では0x00000180の1ビットが0x72のMSBを上書きしてしまうように思えますが、 これは問題ありません。 なぜなら、MSBの値は後続のバイトがデルタタイムであるかを示すための情報であり、 デルタタイムそのものを構成する値ではないからです。 つまり、この値が完成したデルタタイムに含まれることはあってはならないのです。 よって、0x7FでMSBを予め除去しておき、これで確実にMSBが0になるため、 既に取得したバイトで上書きされても問題ないことになります。


戻る