MIDIデバイスへのメッセージ送信

 MCIの機能を使うと、MIDIデバイスを使って手軽にSMFファイルを再生することができました。ただ、MCIを使った再生は「すでに用意されたファイルを再生する」ことしかできないので、自分でMIDIを操作して自由な演奏を実現するシーケンサなどを開発するのは無理があり、ゲームのBGMに使うのもちょっと不便ですね。
 そのような場合は、自分でMIDIデバイスを直接制御することで自由な処理を行うことができます。幸い、現在の多くのパソコンには音色や演奏に関する制御機能の最低限の標準を定義するGM規格に沿ったMIDIデバイスが装備されているので、デバイスの互換性もそれほど心配する必要はありません。少なくとも「楽器や音の高さ・強さを指定して音楽を演奏する」レベルでは(^^;。

 今回は、APIMIDIデバイスにメッセージを送ってMIDIデバイスを初期化したり音を出す、という処理を試してみます。

MIDIデバイスへのメッセージ送信

 WindowsアプリケーションからMIDIデバイスを制御する処理は、最初にデバイスをオープンし、続いてデバイスの設定や音色変更、発音・消音といったいくつかのコマンドを送ってデバイスを使用した後に解放する、という形で行います。オープンや解放は専用のAPIがあり、また発音や消音、音色の変更などのコマンドもコマンドを32ビットの数値にまとめて送るAPIがあるので、特に複雑な処理は必要ありません。

 実際に処理を行う時に中心になるのは、発音と消音でしょう。MIDIでは、16本のチャネルが用意され、チャネルごとに音色を設定し発音することが可能です。発音(ノートオン)を行う場合は、チャネルと音程、音の強さ(ベロシティ)を組み込んだ32ビットのメッセージを作成し、それをmidiOutShortMsg()で送信します。音色やベロシティに指定できる数値の範囲は7ビット(0-127)で、チャネル番号は実際の番号から1を引いて0-15で指定します。

 発音メッセージの実際の32ビットの数値は、以下のようになっています。

24-31ビット 0
23-16ビット ベロシティ
15- 8ビット ノート番号
 8- 1ビット 0xC0 + チャネル番号(-1)

 もともとのMIDIの規格では、チャネル番号n(+1)のノートオンを行う0xCnというコマンドがあり、そのコマンドの配列が、0xCn、ノート番号、ベロシティ、というものでした。MIDI機器につないだケーブルを通してこのコマンドを送ると、音を出すことができたのです。midiOutShortMsg()に渡す32ビットの数値も、メモリ上の配置では、0xCn、ノート番号、ベロシティ、0、とMIDIコマンドの配列とまったく同じになる(バイトオーダーに注意)ので、midiOutShortMsg()はちょうどMIDIコマンドをそのままMIDIデバイスに流すケーブルの役割を果たすわけですね。

 MIDIデバイスにメッセージを送るには、事前にデバイスをオープンし、デバイスのハンドルを取得する必要があります。ハンドルを取得したら、あとはそのハンドルとメッセージを引数にmidiOutShortMsg()を呼び出し、デバイスを使い終わったら、midiOutClose()でデバイスを閉じる、というのが処理の流れです。
 たとえば、MIDIデバイスを初期化してオクターブ4のCの音を0x40の強さで発音、1000ミリ秒後に消音し、デバイスを解放、という処理は以下のようになります。

  MIDIHDR mhMidi;

  /* MIDIデバイスオープン */
  midiOutOpen(&g_hMidi, MIDIMAPPER, 0, 0, 0);

  /* MIDIチャネル1でノート番号0x3cの音をベロシティ0x40で発音 */
  midiOutShortMsg(g_hMidi, 0x00403c90);

  Sleep(1000);

  /* MIDIチャネル1でノート番号0x3cの音を消音 */
  midiOutShortMsg(g_hMidi, 0x00003c90);

  /* MIDIデバイスクローズ */
  midiOutClose(g_hMidi);

 消音に関しては専用のメッセージもありますが、ベロシティを0にすることでも実現できます。

 MIDIデバイスに送るメッセージには、音を鳴らすショートメッセージのほかに、デバイスの設定を行うシステムエクスクルーシブメッセージというものがあります。といっても、このメッセージを送る機会は通常それほどあるわけではなく、デバイスを初期化するときに念のためGM音源として初期化するGMリセットメッセージを送る、という場面で使うことがある程度でしょう。
 ただ、このメッセージはデバイスにやや長いバイト列を送信することもあって手順が少し複雑になります。まず、バッファを用意してそこに送信するメッセージを書き込み、そのバッファのアドレスやメッセージの長さを格納したMIDIHDR構造体を作成。続いてmidiOutPrepareHeader()midiOutLongMsg()midiOutUnprepareHeader()の順に呼び出してメッセージを送信します。
 注意が必要なのは、midiOutLongMsg()で実際にメッセージを送信した後、メッセージ送信を行うデバイスドライバの処理が終わるのを待つ必要がある点。送信終了時に呼び出されるコールバック関数を登録するか、送信終了時にセットされるMIDIHDR構造体のフラグを監視して終了を待ちます。

エクスクルーシブメッセージ送信例

  MIDIHDR mhMidi;

  /* GMリセット用データ */
  BYTE abyGMReset[] = {0xf0, 0x7e, 0x7f, 0x09, 0x01, 0xf7};

  ZeroMemory(&mhMidi, sizeof(mhMidi));

  /* GMリセット送信用バッファ設定 */
  mhMidi.lpData = (LPSTR)abyGMReset;
  mhMidi.dwBufferLength = 6;
  mhMidi.dwBytesRecorded = 6;

  midiOutPrepareHeader(g_hMidi, &mhMidi, sizeof(mhMidi));

  /* GMリセットメッセージ送信 */
  midiOutLongMsg(g_hMidi, &mhMidi, sizeof(mhMidi));

  /* GMリセット完了待機 */
  while ((mhMidi.dwFlags & MHDR_DONE) == 0);

  midiOutUnprepareHeader(g_hMidi, &mhMidi, sizeof(mhMidi));

 上の例では、メッセージ送信後デバイスドライバの処理が終わると設定されるMIDIHDR構造体のMHDR_DONEフラグが立つのを待って、次の処理に移っています。

プログラム

プログラムソース表示

 プログラムを実行すると、MIDIデバイスをオープン・GMリセットします。その後、C/Dボタンをクリックすると、500ms音を鳴らします。


プログラミング資料庫 > Windowsプログラミング研究室