EternalWindows
MIDI / ヘッダチャンクとトラック

前節で示したチャンクタイプとデータ長は、 ヘッダチャンクの一部となっています。 ヘッダチャンクとは、MIDIファイルの先頭14バイトのことで、 次に示す情報が格納されています。

データ内容 バイト数 データ値
チャンクタイプ 4byte MThd
データ長 4byte 6
フォーマット 2byte 0,1,2
トラック数 2byte 0...n
時間単位 2byte Time

チャンクタイプは、ヘッダチャンクを表すMThdになります。 データ長は、フォーマット、トラック数、時間単位の合計バイトで常に6です。 フォーマットは、MIDIファイルのフォーマットを表します。 0の場合はトラック数が1つだけですが、1の場合は複数存在します。 トラック数は、トラックの数が格納されます。 時間単位は、イベントをMIDIデバイスに送るタイミングを算出する場合に利用します。 フォーマットから時間単位までは全てビッグエンディアンで保存されているため、 リトルエンディアンに変換する処理が必要になります。

トラックとは、一連のイベント(及びデルタタイム)を格納した領域のことです。 プログラムでは、トラックからイベントを取得し、それをメッセージとしてMIDIデバイスに送信することになります。 フォーマットが1である場合はトラックが複数存在することがありますが、 これはイベントの管理を容易にするためです。 たとえば、1番目のトラックにピアノの音に関するイベントを格納し、 2番目のトラックにバイオリンの音に関するイベントを格納するといった工夫ができるようになります。 トラックの構造は、次のようになっています。

データ内容 バイト数 データ値
チャンクタイプ 4byte MTrk
トラックのサイズ 4byte n
一連のイベント nbyte ...

トラックはトラックチャンクと呼ばれることもあり、 先頭にはチャンクを識別するチャンクタイプが格納されています。 トラックの場合、これはMTrkになります。 その後の4バイトには、トラックのサイズが格納されています。 そしてこのサイズだけ、一連のイベントが続きます。

今回のプログラムは、ヘッダチャンクと各トラックを読み取ります。

#include <windows.h>

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

BOOL ReadMidiFile(LPTSTR lpszFileName);
BOOL ReadTrack(HMMIO hmmio);
void ReadAndReverse(HMMIO hmmio, LPVOID lpData, DWORD dwSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	if (!ReadMidiFile(TEXT("sample.mid")))
		MessageBox(NULL, TEXT("MIDIファイルの読み込みに失敗しました。"), NULL, MB_ICONWARNING);

	return 0;
}

BOOL ReadMidiFile(LPTSTR lpszFileName)
{
	HMMIO hmmio;
	WORD  i;
	WORD  wTrack;
	WORD  wFormat;
	WORD  wTime;
	DWORD dwMagic;
	DWORD dwDataLen;
	TCHAR szBuf[256];

	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));
	
	for (i = 0; i < wTrack; i++) {
		if (!ReadTrack(hmmio)) {
			MessageBox(NULL, TEXT("不正なトラックが存在します。"), NULL, MB_ICONWARNING);
			mmioClose(hmmio, 0);
			return FALSE;
		}
	}

	wsprintf(szBuf, TEXT("フォーマット %d, トラック数 %d, 時間単位 %d"), wFormat, wTrack, wTime);
	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

	mmioClose(hmmio, 0);
	
	return TRUE;
}

BOOL ReadTrack(HMMIO hmmio)
{
	DWORD dwLen;
	DWORD dwMagic;
	
	mmioRead(hmmio, (HPSTR)&dwMagic, sizeof(DWORD));
	if (dwMagic != *(LPDWORD)"MTrk")
		return FALSE;
	
	ReadAndReverse(hmmio, &dwLen, sizeof(DWORD));
	
	mmioSeek(hmmio, dwLen, SEEK_CUR);
	
	return TRUE;
}

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

今回のReadMidiFileでは、フォーマット、トラック数、時間単位を読み取るために、 ReadAndReverseを3回続けて呼ぶようにしています。 その後、トラックの数だけReadTrackを呼び出し、 そのトラックのチャンクタイプが不正でないかを確認すると共に、 ファイルポインタを次のトラックの先頭を移動さしています。 dwLenにはトラックのサイズが格納されているため、 この数だけmmioSeekでファイルポインタを進めれば、 現在のトラックを抜けることになります。 もちろん、実際のプログラムでは、 一連のイベントを取得しつつファイルポインタを進める必要があります。


戻る