EternalWindows
MIDI / MIDIプログラミング

何らかのファイルをマルチメディアプレイヤーで開いて音声が再生された場合、 そのファイルに音声が格納されていると考えるのは当然の事です。 事実、WAVEファイルにはデジタル化された波形データが格納されていますし、 MP3ファイルにはMP3の規格で圧縮された波形データが格納されています。 しかし、MIDIファイルには、音声(波形データ)は格納されていません。 このファイルに格納されているのはいわば楽譜であり、 「xxの楽器を使ってドの音を鳴らしなさい」というような命令が無数に格納されているのです。 このため、MIDIファイルのサイズは、WAVEやMP3と比べて非常に軽くなる傾向があり、 特定の音を鳴らすという直観的な感覚で再生できる利点があります。

Windows MultimediaのAPIを使用してMIDIを再生するには、次に示す3通りの方法があります。

MIDIの再生方法 説明
MCI この方法は、MIDIファイルのパスを関数に指定するだけでMIDIを再生できるため、 単純にMIDIを再生したいだけの場合はこの方法を利用する。 ただし、関数を呼び出してから実際に再生されるまでの時間が長い。
MIDI API この方法は、MIDIファイルを独自に解析して命令を表すイベントを取得し、 そのイベントを1つずつMIDIデバイスに送信していくことになる。 この方法の最大の難関は、ファイルの解析と再生の両方をアプリケーションが明示的に行うことから、 MIDIについての知識と多くのコード量が必要になるという点である。 しかし、音量や音色を変更するなど、最も柔軟な操作が可能である。
MIDIストリーム この方法は、MIDI APIと同じくMIDIファイルを独自に解析することになるが、 MIDIの再生自体はMIDIストリーム関数を使用して行うことになる。 MIDI APIと比べて再生が容易になるわけでもなく、かえって柔軟性がなくなっている。

本章では、MIDI APIとMIDIストリームを使用した方法について説明します。 まず、MIDI APIを通じてMIDIメッセージの基本を理解し、 その後にMIDIファイルの詳細について取り上げていきます。 また、実践編というものを設け、MCIでは実現できないいくつかのテクニックについても触れます。

MIDI APIによるプログラミングでは、まずMIDIデバイスをオープンすることになります。 これには、midiOutOpenを呼び出します。

UINT midiOutOpen(
  LPHMIDIOUT lphmo,         
  UINT uDeviceID,           
  DWORD dwCallback,         
  DWORD dwCallbackInstance, 
  DWORD dwFlags             
);

lphmoは、MIDIデバイスのハンドルを受け取る変数のアドレスを指定します。 uDeviceIDは、オープンしたいMIDIデバイスの識別子を指定します。 MIDI_MAPPERを指定すると、デフォルトのデバイスがオープンされます。 dwCallbackは、コールバック機能を利用するためのハンドルを指定します。 たとえば、ウインドウハンドルを指定すると、再生の終了時に通知を受けることができます。 dwCallbackInstanceは、dwCallbackにコールバック関数のアドレスを指定した場合、 その関数に渡したいデータを指定します。 dwFlagsは、コールバックフラグを指定します。 CALLBACK_WINDOWを指定した場合は、dwCallbackがウインドウハンドルであると解釈されます。

MIDIデバイスをオープンしたら、midiOutShortMsgでMIDIメッセージを送信します。

MMRESULT midiOutShortMsg(
  HMIDIOUT hmo, 
  DWORD dwMsg   
);

hmoは、MIDIデバイスのハンドルを指定します。 dwMsgは、MIDIメッセージを指定します。

midiOutShortMsgを呼び出すことによってどのようなことが起きるかは、 送信するMIDIメッセージによって異なります。 既に述べたように、MIDIファイルに格納されているのは特定の音を鳴らすといったような命令であり、 こうした命令はイベントと呼ばれています。 メッセージは、このイベントを実際にMIDIデバイスに送信する場合に用いる言葉ですが、 基本的に両者は同じ意味であると解釈して問題ありません。 MIDIメッセージは、次のような情報で構成されています。

0x 00 70 3C 90

MIDIメッセージの第1バイトはステータスバイトと呼ばれ、命令の種類を表します。 上記の例では0x90となっていますが、これは発音のイベントであることを表しています。 つまり、ステータスバイトが0x9n(nは0からfの値)であるメッセージを送信した場合は、 何らかの音が鳴ることを意味しています。 第2バイトと第3バイトはデータバイトと呼ばれ、 これはスタータスバイトによって意味が異なります。 スタータスバイトが0x9nである場合は第1データバイトが音階になり、 第2データバイトは音量になります。 上記の例では、第2バイト(第1データバイト)が0x3cとなっており、 これはドの音を鳴らすことを意味しています。 第3バイト(第2データバイト)は0x70となっており、 これは音量が0x70であることを意味しています(音量の最高値は0x7f)。 つまり、上記のメッセージは、ドの音を音量0x70で再生するメッセージということになります。

ステータスバイトに0x9nを指定することで音を鳴らせるといっても、 音を鳴らすという行為には音階や音量以外にも様々な要素が必要になるはずです。 たとえば、先の例で取り上げたドの音はどのような楽器を使って再生されることになるのでしょうか。 そもそも、MIDI APIによるMIDIの再生とは、 midiOutShortMsgを1回呼び出して終わるわけではありません。 つまり、再生に必要なデータを全てmidiOutShortMsgに指定する設計にはなっていません。 midiOutShortMsgに指定するのは、MIDIファイルに格納されている1つのイベントの ステータスバイト及びデータバイトであり、これをDWORD型にまとめたものがメッセージです。 メッセージには、使用する楽器をMIDIデバイスに伝えることができるものもあるため、 これを0x9nの前に送信しておけば、音は指定された楽器で鳴らされることになります。

不要になったMIDIデバイスは、midiOutCloseで閉じることになります。

MMRESULT midiOutClose(
  HMIDIOUT hmo 
);

hmoは、MIDIデバイスのハンドルを指定します。

今回のプログラムは、0x9nの発音メッセージをMIDIデバイスに送信します。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HMIDIOUT hmo;
	
	if (midiOutOpen(&hmo, MIDI_MAPPER, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR)
		return 0;

//	midiOutShortMsg(hmo, 0x000013C0); // 音色を変える
	
	midiOutShortMsg(hmo, 0x00703C90); // 発音

	Sleep(1000); // 1秒休む
	
	midiOutShortMsg(hmo, 0x00703C80); // 消音
	
	MessageBox(NULL, TEXT("消音しました。"), TEXT("OK"), MB_OK);
	
	midiOutClose(hmo);

	return 0;
}

midiOutOpenの第5引数にCALLBACK_NULLを指定しているため、コールバック機能は利用しません。 したがって、第3引数と第4引数は0になります。 0x00703C90を指定しているmidiOutShortMsgで音が鳴り始め、 0x00703C80を指定しているmidiOutShortMsgで音が消えます。 音を消す場合は、ステータスバイトに0x8nを指定します。 この2つの呼び出しの間にSleepで1秒待機しているため、 1秒間音が聞こえることになります。 コメントになっているmidiOutShortMsgを実行した場合、 第1データバイトの0x13で表される楽器が使用されることになります。 ステータスバイトが0xCnであるメッセージは、楽器や音色の変更に使用します。

これまでの話により、MIDI APIが音を直観的に扱えることが分りましたが、 それと同時に多少使いづらい面も見えてしまったと思われます。 たとえば、ステータスバイトやデータバイトに指定された値は、 MIDIに関する知識がなければどのような意味を持つかのかが分かりませんが、 これについてはそれほど深く理解しておく必要はないと思われます。 アプリケーションはMIDIファイルから1つずつイベントを取得し、 それを1つずつmidiOutShortMsgに指定するだけですから、 取得したイベントがどのような意味を持っているかは重要でないのです。 重要なのは、そのイベントをMIDIデバイスに送信しなければならないという点です。 そして、本当に理解しておかなければならないことは、MIDIファイルの構造です。 この構造を理解しておかなければ、midiOutShortMsgに指定するためのイベントを取得できませんから、 次節以降はMIDIファイルの構造について言及していきます。


戻る