MIDI の基本
MIDI と電子楽器
波形オーディオは、実世界の物理的な空気振動を数値化したものです
実世界の音声を正確に、極めて忠実に表現するにはこれが最適の方法です
国会中継や THE YELLOW MONKEY の魂に響くロックミュージック
カラヤン指揮のベルリンフィルハーモニーの演奏などを再生するには
波形データとして音を記録し、波形データファイルとして保存する方法が最適です
しかし、この方法が適確ではない分野も存在します
シンセサイザ、キーボードなどに代表される電子楽器の世界です
電子楽器は、波形データとは異なり、音階と音質が明確に定められています
このキーを叩いたら、この音を出力するという処理に、波形データは冗長です
そこで、シンセサイザメーカーのコンソーシアムが開発したのが
音声データの通信規格 MIDI(Musical Instrument Digital Interface) です
MIDI は、異なるコンピュータを接続する通信プロトコルの様に
異なる電子楽器の接続や、電子楽器とコンピュータの接続に使う音楽用のプロトコルです
MIDI は 31,205bps の片方向通信でデータを送受信します
ケーブルを介して、電子楽器同士やコンピュータと接続し、音声データをやり取りします
通信カラオケと呼ばれるものも、MIDI に属しています
ここで重要なことは、MIDI が送受信するデータは音声データではないことです
ケーブルを流れるデータは、音の情報を表した 1 〜 3 バイトのメッセージなのです
簡単な MIDI システムでは、MIDI 出力デバイスと MIDI 入力デバイスがあり
これらは、MIDI 互換ハードウェアとしてケーブルで接続されます
MIDI 出力デバイスは、音声を生成せずに、簡単な 3 バイト程度のメッセージを送信します
入力デバイスがデータ受け取ると、出力デバイスが要請した音声を再生する仕組みになります
どのような音を、どのような質で再生するかは MIDI ハードウェアの機能に左右されます
例えば、MIDI キーボードは鍵盤がキーボニストに押されると
「鍵盤が押された」「鍵盤が表す音」「鍵盤を叩いた強さ」という3つの情報を送信します
シンセサイザの入力ポートにこのデータが届くと、情報を元に適切な電子音を再生します
逆に、キーボードが「鍵盤を離した」という情報を渡せば、再生は中断されることでしょう
キーボード
| MIDI 出力ポート
| →
| MIDI 入力ポート
| シンセサイザ
| →
| 再生
|
ここで言う MIDI キーボードとは、白と黒の鍵盤があるキーボードのことで
文字やテンキーなどがある、PC 用のキーボードではないので、注意してください
シンセサイザが生成する音について、MIDI は明確に定義していません
実際に再生される音の波形データは、シンセサイザのメモリなどに格納されていることでしょう
MIDI にとって、音声がどのような形で、どこに格納されているかは興味はありません
さらに、MIDI は簡単なメッセージの集合なので、これに時間軸の情報を付加し
コンピュータのメモリに記録することで、電子楽器の演奏を保存することができます
これが、いわゆる *.mid 形式で保存されている MIDI ファイルです
しかし、シンセサイザごとに生成する音が定義されていないのは重要な問題です
異なるコンピュータで同じ MIDI ファイルを再生しても
シンセサイザが異なるために、作者が作った音楽が意図通りに再生できません
これを解決するために General MIDI と呼ばれる標準が存在します
この規格を、一般的に GM 音源と呼び、Windows などは標準でこれをサポートしています
これで、GM 音源を対象に書かれた MIDI データは
GM をサポートする全てのコンピュータで、再生が保証されるということを表します
MIDI の短所は、実世界の音声をリアルに再現できないということです
すでに用意された音しか使うことができないという事が最大の弱点です
しかし、波形データに比べ極めて少ない容量で長時間の音楽が表現でき
低速回線での配信も容易であるという点で、波形データよりも数倍有利です
(もっとも、現代の高速回線時代では MP3 ファイルの受信も容易になりましたが…)
MIDI データをシンセサイザに出力するデバイスは、キーボードだけではありません
MIDI メッセージを生成するデバイスを総称して MIDI コントローラと呼びます
また、メッセージを受け音声を再生するシンセサイザのことは
サウンドモジュールとかトーンジェネレータと呼ぶこともあります
MIDI コントローラが生成する MIDI メッセージ は
最大で 3 バイトで、メッセージを表す値と、メッセージに関連したデータで構成されます
この関係は、ウィンドウメッセージと WPARAM、LPARAM に似ています
MIDI メッセージを表す 1 バイトをステータスバイトと呼びます
ステータスバイトは、必ず上位 1 ビットが 1でなければなりません
逆にステータスバイトに付加するデータバイトの上位 1 ビットは 0 になります
これによって、プログラムはステータスバイトとデータバイトを区別する事ができます
ステータスバイト | データバイト1 | データバイト2
|
---|
メッセージ値 | チャネル番号
|
---|
1XXX | xxxx | 0yyy yyyy | 0zzz zzzz
|
---|
これが、MIDI メッセージの構造を表した図です
データバイトの数は、メッセージによっては 1 つしか持たないこともあります
ステータスバイトは、上位 4 ビットと下位 4 ビットにさらに分割して考えます
MIDI メッセージの意味を表す値は、このうちの上位 4 ビットで表現されます
では、下位 4 ビットのチャネル番号とは、一体何を表すのでしょうか
チャネル番号は、異なる音質を(シンセサイザ上の異なる楽器)を
同時に鳴らすために、このチャネル番号を使って各楽器を指定します
つまり、MIDI には 16 人のバンド奏者がいると考える事ができ
チャネル番号はそれぞれの奏者を表すインデックスであると考えれば想像しやすいでしょう
Windows でプログラムから MIDI デバイスを操作するには、MIDI API を用います
これも、低レベルマルチメディア API に属します
まず、MIDI メッセージを送信するには、MIDI 出力デバイスを開きます
デバイスを開くには midiOutOpen() 関数を使います
UINT midiOutOpen(
LPHMIDIOUT lphmo, UINT uDeviceID ,
DWORD dwCallback , DWORD dwCallbackInstance ,
DWORD dwFlags
);
lphmo には、MIDI 出力デバイスのハンドルへのポインタを指定します
MIDI 出力デバイスのハンドルは HMIDIOUT 型の変数です
midiOutOpen() はここに開いた出力デバイスのハンドルを格納します
uDeviceID はオープンする MIDI 出力デバイスの識別子を指定します
この値は、常に「0 〜 インストールされているデバイスの数 - 1」までになります
MIDI_MAPPAR 定数を使えば、優先されるデバイスをマッパが選択してくれます
dwCallback は、MIDI 出力に関連したメッセージを受け取る
コールバック関数、ウィンドウハンドル、スレッドハンドルのいずれかを指定します
dwCallbackInstance には、コールバック関数に渡す追加データを指定できます
dwFlags には、コールバックのタイプをフラグで指定します
ここには、以下の値のいずれかを指定する事ができます
定数 | 解説
|
---|
CALLBACK_EVENT | dwCallback はイベントハンドルである
|
CALLBACK_FUNCTION | dwCallback はコールバック関数のポインタである
|
CALLBACK_NULL | コールバックは使用しない
|
CALLBACK_THREAD | dwCallback はスレッドハンドルである
|
CALLBACK_WINDOW | dwCallback はウィンドウハンドルである
|
関数が成功すれば MMSYSERR_NOERROR が、そうでなければエラー値が返ります
この関数は、以下のエラー値を返す可能性があります
定数 | 解説
|
---|
MIDIERR_NODEVICE | MIDI ポートが見つからない
|
MMSYSERR_ALLOCATED | 指定のリソースはすでに割り当てられている
|
MMSYSERR_BADDEVICEID | 指定のデバイス識別子が不正である
|
MMSYSERR_INVALPARAM | 指定のポインタ、構造体は無効である
|
MMSYSERR_NOMEM | 必要なメモリを割り当てる事ができなかった
|
関数が成功すれば、開いた MIDI デバイスのハンドルを用いて
midiOut プリフィックスを持つ MIDI 出力関数を使うことができます
この考え方は、ほとんど WAVE の入出力関数と同じであることに注目してください
dwCallback にコールバック関数を指定する場合は
midiOutProc() プロトタイプ関数と同じ型でなければなりません
void CALLBACK MidiOutProc(
HMIDIOUT hmo , UINT wMsg ,
DWORD dwInstance,
DWORD dwParam1, DWORD dwParam2
);
hmo には MIDI 出力デバイスのハンドルです
wMsg には MIDI 出力の状態を表すメッセージを指定します
dwInstance は関数で提供されたユーザー定義のインスタンスデータ
dwParam1 と dwParam2 はメッセージの追加情報を指定します
開いたデバイスに適切な MIDI メッセージを送信すれば、電子音が再生されます
メッセージの送信には midiOutShortMsg() 関数を使います
MMRESULT midiOutShortMsg(HMIDIOUT hmo , DWORD dwMsg);
hmo には、MIDI 出力デバイスのハンドルを
dwMsg には、MIDI メッセージを指定します
関数が成功すれば MMSYSERR_NOERROR が、そうでなければエラー値が返ります
この関数は、以下のエラー値を返す可能性があります
定数 | 解説
|
---|
MIDIERR_BADOPENMODE | メッセージにステータスバイトがない
|
MIDIERR_NOTREADY | デバイスが別のスレッドで使われている
|
MMSYSERR_INVALHANDLE | 指定のデバイスハンドルが無効である
|
MIDI 出力デバイスが不用になれば、解放処理を行う必要があります
まず、midiOutReset() 関数で再生を完全に停止します
MMRESULT midiOutReset(HMIDIOUT hmo);
hmo には MIDI 出力デバイスのハンドルを指定します
関数が成功すれば MMSYSERR_NOERROR が、そうでなければエラー値が返ります
この関数が返す可能性のあるエラー値は MMSYSERR_INVALHANDLE だけです
その後 midiOutClose() 関数でデバイスを閉じます
MMRESULT midiOutClose(HMIDIOUT hmo);
hmo には MIDI 出力デバイスのハンドルを指定します
関数が成功すれば MMSYSERR_NOERROR が、そうでなければエラー値が返ります
この関数は、以下のエラー値を返す可能性があります
定数 | 解説
|
---|
MIDIERR_STILLPLAYING | データバッファブロックが MIDI デバイスキューに入っている
|
MMSYSERR_INVALHANDLE | 指定のデバイスハンドルが無効である
|
MMSYSERR_NOMEM | 必要なメモリを割り当てる事ができなかった
|
さて、これらの関数を使えば MIDI 出力ポートからメッセージを送信できます
今回は、次のような音声を再生する意味を持つ Note On というステータスバイトを送ります
9x yy zz
x には、先ほど説明したようにチャネル番号を指定します
yy にはキー番号を指定します、キー番号は 0x3C が中央の ド に相当します
zz は打鍵速度を表します
データバイトの上位1ビットは常に 0 でなければならないので
yy も zz も、その範囲は 0 〜 127 までとなります
逆に、音声を停止するには Note Off というステータスバイトを送ります
Note Off を送信すれば、シンセサイザは鍵盤が離されたと解釈します
これには、打鍵速度を 0 としてメッセージを送信すればよいでしょう
9x yy 0
残念ながら、この場は MIDI ではなく Win32 API について説明する場です
MIDI や MIDI をディスクファイルとして保存した、SMF と呼ばれる形式、GM 音源など
MIDI に関連した解説を網羅すれば、それだけでひとつの本ができてしまうでしょう
そのため、この場では MIDI メッセージなどについては解説をしません
MIDI メッセージはひとつの DWORD 型として送信しなければなりません
そこで、筆者は次のような MIDI メッセージ用のマクロを定義しました
#define MIDIMSG(stat , data1 , data2) (DWORD)(stat | (data1 << 8) | (data2 << 16))
stat にはステータスバイトを、data1 と data2 にはデータバイトを指定します
マクロは、各データをシフトして DWORD 型の MIDI メッセージを返します
次のような共用体を用いるという方法も考えられます
typedef union {
DWORD midiMsg;
BYTE midiData[4];
} MIDIMSG;
メッセージを設定する時は、midiData メンバを使って
それぞれの配列に適切な値を代入し、送信する時は midiMsg メンバを使います
#include <windows.h>
#define TITLE TEXT("Kitty on your lap")
#define MIDIMSG(stat , data1 , data2) (DWORD)(stat | (data1 << 8) | (data2 << 16))
LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
static HMIDIOUT hMidi;
switch (msg) {
case WM_DESTROY:
midiOutReset(hMidi);
midiOutClose(hMidi);
PostQuitMessage(0);
return 0;
case WM_CREATE:
midiOutOpen(&hMidi , MIDIMAPPER , 0 , 0 , 0);
return 0;
case WM_LBUTTONDOWN:
midiOutShortMsg(hMidi , MIDIMSG(0x90 , 0x45 , 40));
return 0;
case WM_LBUTTONUP:
midiOutShortMsg(hMidi , MIDIMSG(0x90 , 0x45 , 0));
return 0;
}
return DefWindowProc(hWnd , msg , wp , lp);
}
int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
PSTR lpCmdLine , int nCmdShow) {
HWND hWnd;
MSG msg;
WNDCLASS winc;
winc.style = CS_HREDRAW | CS_VREDRAW;
winc.lpfnWndProc = WndProc;
winc.cbClsExtra = winc.cbWndExtra = 0;
winc.hInstance = hInstance;
winc.hIcon = LoadIcon(NULL , IDI_APPLICATION);
winc.hCursor = LoadCursor(NULL , IDC_ARROW);
winc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
winc.lpszMenuName = NULL;
winc.lpszClassName = TEXT("KITTY");
if (!RegisterClass(&winc)) return 1;
hWnd = CreateWindow(
TEXT("KITTY") , TITLE ,
WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
CW_USEDEFAULT , CW_USEDEFAULT ,
CW_USEDEFAULT , CW_USEDEFAULT ,
NULL , NULL , hInstance , NULL
);
if (hWnd == NULL) return 1;
while (GetMessage(&msg , NULL , 0 , 0 )) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
このプログラムは、ウィンドウのクライアント領域をマウスの左ボタンを押すと
GM 音源ならば、おそらくピアノの音で ラ の音が再生されるでしょう
ボタンを離せば、再生は停止します
プログラマから見れば、このプログラムがシンセサイザを制御する MIDI コントローラですが
ユーザーから見ると、マウスが MIDI コントローラであるかのように動作します
この性質を利用すれば、PC のキーボードを MIDI コントローラにすることもできるでしょう
つまり、画面のウィンドウや入力装置を電子楽器に変身させる事ができるのです
midiOutOpen()
UINT midiOutOpen(
LPHMIDIOUT lphmo, UINT uDeviceID ,
DWORD dwCallback , DWORD dwCallbackInstance ,
DWORD dwFlags
);
MIDI 出力デバイスを開きます
lphmo - MIDI 出力デバイスのハンドルへのポインタを指定します
uDeviceID - オープンする MIDI 出力デバイスの識別子を指定します
dwCallback - メッセージを受け取るコールバック機構を指定します
dwCallbackInstance - コールバック関数に渡す追加データを指定できます
dwFlags - コールバックのタイプをフラグで指定します
戻り値 - 成功すれば MMSYSERR_NOERROR、そうでなければエラー値
dwFlags には、以下の値のいずれかを指定する事ができます
定数 | 解説
|
---|
CALLBACK_EVENT | dwCallback はイベントハンドルである
|
CALLBACK_FUNCTION | dwCallback はコールバック関数のポインタである
|
CALLBACK_NULL | コールバックは使用しない
|
CALLBACK_THREAD | dwCallback はスレッドハンドルである
|
CALLBACK_WINDOW | dwCallback はウィンドウハンドルである
|
この関数は、以下のエラー値を返す可能性があります
定数 | 解説
|
---|
MIDIERR_NODEVICE | MIDI ポートが見つからない
|
MMSYSERR_ALLOCATED | 指定のリソースはすでに割り当てられている
|
MMSYSERR_BADDEVICEID | 指定のデバイス識別子が不正である
|
MMSYSERR_INVALPARAM | 指定のポインタ、構造体は無効である
|
MMSYSERR_NOMEM | 必要なメモリを割り当てる事ができなかった
|
MidiOutProc()
void CALLBACK MidiOutProc(
HMIDIOUT hmo , UINT wMsg ,
DWORD dwInstance,
DWORD dwParam1, DWORD dwParam2
);
midiOutOpen() 関数で指定するコールバック関数のプロトタイプです
hmo - MIDI 出力デバイスのハンドルを指定します
wMsg - MIDI 出力の状態を表すメッセージを指定します
dwInstance - 関数で提供されたユーザー定義のインスタンスデータを指定します
dwParam1 - メッセージの追加情報を指定します
dwParam2 - メッセージの追加情報を指定します
midiOutShortMsg()
MMRESULT midiOutShortMsg(HMIDIOUT hmo , DWORD dwMsg);
MIDI 出力ポートに MIDI メッセージを送信します
hmo - MIDI 出力デバイスのハンドルを指定します
dwMsg - MIDI メッセージを指定します
戻り値 - 成功すれば MMSYSERR_NOERROR、そうでなければエラー値
midiOutReset()
MMRESULT midiOutReset(HMIDIOUT hmo);
全てのノートをオフにします
hmo - MIDI 出力デバイスのハンドルを指定します
戻り値 - 成功すれば MMSYSERR_NOERROR、そうでなければエラー値
この関数におけるエラーは MMSYSERR_INVALHANDLE 以外は考えられません
不正なハンドルを渡した場合、この値がかえります
midiOutClose()
MMRESULT midiOutClose(HMIDIOUT hmo);
MIDI 出力デバイスを閉じます
hmo - MIDI 出力デバイスのハンドルを指定します
戻り値 - 成功すれば MMSYSERR_NOERROR、そうでなければエラー値
この関数は、以下のエラー値を返す可能性があります
定数 | 解説
|
---|
MIDIERR_STILLPLAYING | データバッファブロックが MIDI デバイスキューに入っている
|
MMSYSERR_INVALHANDLE | 指定のデバイスハンドルが無効である
|
MMSYSERR_NOMEM | 必要なメモリを割り当てる事ができなかった
|