BGM用MIDI演奏システム

フリーソフトのゲームのBGMは、容量などの関係でMIDIが使われることが多いようです。ちょっとしたゲームならMIDIファイルを同梱してMCIで再生、でも良いのですが、これだと再生開始時に処理が止まったりMIDIファイルを同梱したくない、という時には不便ですよね。そこで、マルチメディアタイマを使って独自のMIDI再生システムを作ってみることにしましょう。

MIDIデータの作成と再生

今回のMIDI再生システムは、再生する音を時間ごとに記録した独自の演奏データをマルチメディアタイマのタイマ関数で順次再生するものです。演奏データ内では、各音を32ビット整数で保持し上位16ビットに時間、続く8ビットに再生するノート(音)番号、最下位8ビットにベロシティ(強さ)を設定します。そして、この32ビットの音データをを配列にしたものが演奏データになるわけです。

音データの「時間」は、タイマ関数が呼び出された回数をもとにしました。たとえば、タイマ関数を10ms毎に呼び出すようにするなら、タイマ関数が呼び出される毎に0,1,2…と時間カウントが増えていくわけです。音データの時間は16ビットで保持するので、タイマ関数の呼び出し周期が10msなら最大で11分程度(65535 * 10ms ≒ 655秒)の演奏データを表現できますね。音を表すノート番号はMIDIのノート番号をそのまま使うことにしましょう。

というわけで、実際の演奏データは最初の100msにO4C(0x3c)の音を64(0x40)の強さで鳴らすなら以下のようになります(音を止める時は、ベロシティ0を設定)。

0x00003c40
0x000a3c00

このようにして作成した演奏データは、マルチメディアタイマのコールバック関数内で順次再生していきます。あらかじめデータが「時間順」にならんでいるなら、dwCountを現在のカウントとして

  while(((adwData[iIndex] & 0xffff0000) >> 16) <= dwCount) {

      /* カウンタ位置のデータからノート番号とベロシティ取得 */
      byArg1 = (BYTE)(adwMIDI[iIndex] & 0x000000ff);
      byArg2 = (BYTE)((adwMIDI[iIndex] & 0x0000ff00) >> 8);

      /* midiOutShortMsgで送る32ビットデータ作成 */
      dwData = ((DWORD)byArg1 << 16) + ((DWORD)byArg2 << 8);

      /* チャネル1のノートオン指定 */
      dwData += 0x90;

      /* MIDIデバイスにメッセージ送信 */
      midiOutShortMsg(g_hMidi, dwData);

      /* カウンタ更新 */
      iIndex++;

  }

といった処理を行えばMIDIチャネル1で演奏データを再生することができます。これは、データの配列adwMIDIからiIndexの位置にある要素を取り出し、その時間が現在の時間以下なら再生する、という流れですね(データの配列は「時間順」になっているので、これで「現在までに再生すべきデータ」を処理できる)。

ただ、単純に音を出すだけではちょっとさびしいので、これに音色の変更とボリューム設定といったコマンドも追加しましょう。幸いノート番号は0x7fまでなので、ノート番号が0x80以上なら、ノートオンではなくコマンド実行、という形にすることができます。
コマンドは、MIDIのコントロールチェンジにあわせて音色変更を0xC0、ボリューム変更を0xb7としました。パラメータ(音色番号やボリュームの値)は、最下位8ビットで指定します。

さらに、複数のチャネルを扱えるようにMIDIの演奏データをLPDWORD型変数配列g_lpdwMIDI[4]に格納するようにしました。g_lpdwMIDI[1]g_lpdwMIDI[3]がチャネル1〜3、g_lpdwMIDI[0]が制御用チャネルのデータを示すポインタです。たとえばチャネル1の最初のデータは、g_lpdwMIDI[1][0]としてアクセスすることになります。
各チャネルのデータ数は最大1024とし、最初に0xffffffffでクリアしておきましょう。演奏時には、「データ位置が1024以上になるか、取り出したデータが0xffffffffになる」とそのチャネルの全データを再生したものとみなすようにします。

 以上のことから、複数チャネル(1〜3)の演奏・コマンドデータを再生する処理は以下のようになります。

  /* MIDIチャネル1-3の演奏データ再生 */
  for (i = 1;i < 4;i++) {

      /* 有効な演奏データあり */
      if (aiIndex[i] < 1024 && g_lpdwMIDI[i][aiIndex[i]] != 0xffffffff) {

          /* 現カウントまでに未処理のデータをすべて再生 */
          while(((g_lpdwMIDI[i][aiIndex[i]] & 0xffff0000) >> 16) <= dwCount) {

              byCommand = (BYTE)((g_lpdwMIDI[i][aiIndex[i]] & 0x0000ff00) >> 8);
              byArg = (BYTE)(g_lpdwMIDI[i][aiIndex[i]] & 0x000000ff);

              switch (byCommand) {

              /* ボリューム設定 */
              case 0xb7:

                  dwData = ((DWORD)byArg << 16) + (7 << 8) + 0xb0 + (i - 1);

                  midiOutShortMsg(g_hMidi, dwData);

                  break;

              /* 音色変更 */
              case 0xc0:

                  dwData = ((DWORD)byArg << 8) + 0xc0 + (i - 1);

                  midiOutShortMsg(g_hMidi, dwData);

                  break;

              default:

                  /* ノートオン */
                  if (byCommand < 0x80) {

                      dwData = ((DWORD)byArg << 16) + ((DWORD)byCommand << 8);
                      dwData += 0x90 + (i - 1);

                      midiOutShortMsg(g_hMidi, dwData);

                  }

                  break;

              }

              /* チャネルの現在位置カウンタ更新 */
              aiIndex[i]++;

          }

      }

  }

以上で、複数チャネルのMIDIの演奏データを保持し演奏する基本的な処理ができるようになりました。最後に、制御用チャネルを追加してみましょう。これはリピートなど全演奏チャネルに対する制御を行うためのチャネルで、g_lpdwMIDI[0]を割り当てることにします。このチャネルでは、リピートコマンド(0xa0)を処理できるようにしました。

  /* 制御チャネル処理 */
  if (aiIndex[0] < 1024 && g_lpdwMIDI[0][aiIndex[0]] != 0xffffffff) {

      /* 現カウントまでに未処理のデータをすべて再生 */
      while(((g_lpdwMIDI[0][aiIndex[0]] & 0xffff0000) >> 16) <= dwCount) {

          byCommand = (BYTE)((g_lpdwMIDI[0][aiIndex[0]] & 0x0000ff00) >> 8);
          byArg = (BYTE)(g_lpdwMIDI[0][aiIndex[0]] & 0x000000ff);

          switch (byCommand) {

          /* リピート */
          case 0xa0:

              dwCount = 0;

              midiOutReset(g_hMidi);

              return;

              break;

          }

          aiIndex[0]++;

      }

  }

マルチメディアタイマによるBGM再生

以上で独自に作成した演奏データを順次MIDIデバイスに送信して再生するシステムができたので、これをマルチメディアタイマで一定間隔ごとに呼び出してみましょう。

  /* マルチメディアタイマ起動 */
  g_dwMMTimerID = timeSetEvent(10, 1, timerFunc, 0, TIME_PERIODIC);

マルチメディア関数の周期は10msとし、マルチメディアコールバック関数timerFunc()内で演奏データg_lpdwMIDI[4](制御チャネル1、演奏チャネル3)を再生するようにしました。

  /* マルチメディアタイマコールバック関数 */
  void CALLBACK timerFunc(UINT uiID, UINT uiNo, DWORD dwCookie, DWORD dwNo1, DWORD dwNo2) {

      static DWORD dwCount = 0;
      static int aiIndex[4];
      int i;

      BYTE byCommand, byArg;
      DWORD dwData;

      if (dwCount == 0) {

          for (i = 0;i < 4;i++) {
              aiIndex[i] = 0;
          }

      }

      /* 制御チャネル処理 */
      if (aiIndex[0] < 1024 && g_lpdwMIDI[0][aiIndex[0]] != 0xffffffff) {

          /* 現カウントまでに未処理のデータをすべて再生 */
          while(((g_lpdwMIDI[0][aiIndex[0]] & 0xffff0000) >> 16) <= dwCount) {

              byCommand = (BYTE)((g_lpdwMIDI[0][aiIndex[0]] & 0x0000ff00) >> 8);
              byArg = (BYTE)(g_lpdwMIDI[0][aiIndex[0]] & 0x000000ff);

              switch (byCommand) {

              /* リピート */
              case 0xa0:

                  dwCount = 0;

                  midiOutReset(g_hMidi);

                  return;

                  break;

              }

              aiIndex[0]++;

          }

      }

      /* MIDIチャネル1-3の演奏データ再生 */
      for (i = 1;i < 4;i++) {

          /* 有効な演奏データあり */
          if (aiIndex[i] < 1024 && g_lpdwMIDI[i][aiIndex[i]] != 0xffffffff) {

              /* 現カウントまでに未処理のデータをすべて再生 */
              while(((g_lpdwMIDI[i][aiIndex[i]] & 0xffff0000) >> 16) <= dwCount) {

                  byCommand = (BYTE)((g_lpdwMIDI[i][aiIndex[i]] & 0x0000ff00) >> 8);
                  byArg = (BYTE)(g_lpdwMIDI[i][aiIndex[i]] & 0x000000ff);

                  switch (byCommand) {

                  /* ボリューム設定 */
                  case 0xb7:

                      dwData = ((DWORD)byArg << 16) + (7 << 8) + 0xb0 + (i - 1);

                      midiOutShortMsg(g_hMidi, dwData);

                      break;

                  /* 音色変更 */
                  case 0xc0:

                      dwData = ((DWORD)byArg << 8) + 0xc0 + (i - 1);

                      midiOutShortMsg(g_hMidi, dwData);

                      break;

                  default:

                      /* ノートオン */
                      if (byCommand < 0x80) {

                          dwData = ((DWORD)byArg << 16) + ((DWORD)byCommand << 8);
                          dwData += 0x90 + (i - 1);

                          midiOutShortMsg(g_hMidi, dwData);

                      }

                      break;

                  }

                  /* チャネルの現在位置カウンタ更新 */
                  aiIndex[i]++;

              }

          }

      }

      dwCount++;

  }

プログラム

プログラムソース

今回のプログラムは、背景をスクロールさせながらMIDI音源でBGMを再生します。

演奏データは配列g_lpdwMIDIに格納していますので、適当な演奏データを作ってみてください。ソース中の演奏データはデータライブラリで公開している「落葉の月夜」です。


プログラミング資料庫 > ゲーム制作研究室