カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

ロングメッセージの送信

ロングメッセージ

MIDIのメッセージで、midiOutShortMsg()関数を用いて送信できるものをショートメッセージと呼び、データを収容するために専用バッファを必要とし、midiOutLongMsg()関数を用いるものをロングメッセージと呼びます。ショートメッセージ、ロングメッセージという分類は、送信に用いるWin32APIの違いに由来し、MIDI規格によるメッセージの分類とは関連ありません。

ロングメッセージに属するメッセージはシステムエクスクルーシブだけです。これ以外のすべてのMIDIメッセージは高々3バイトであるため、ショートメッセージに分類されます。

midiOutShortMsg()関数とmidiOutLongMsg()関数では、送信の手順が大きく異なります。このセクションでは、ロングメッセージの送信に関する問題を解説します。

非同期式

ショートメッセージを送信するmidiOutShortMsg()関数は、同期式で動作します。同期式とは、作業の実行が終了するまで、関数から抜けないことを意味します。言い換えるとmidiOutShortMsg()関数は、実行が終わった時点で、メッセージが無事送信されたことを意味します。

しかし、同期式のAPIには、スレッドをブロックするという問題があります。スレッドをブロックするとは、関数の実行が終わるまでスレッドはずっと待機し、制御が次のステップへ進まないことを意味します。MIDIのボーレートから分かるように、メッセージを3バイト送信するには、約1mSecを要します。1mSecくらいならスレッドがブロックされても許せる範囲だろう、という判断でmidiOutShortMsg()関数は、同期式になっています。

しかしロングメッセージの送信は、事情が異なります。データ量に比例して、送信に長い時間がかかります。同期式にするとスレッドが停止する時間も長くなるため、midiOutLongMsg()関数は同期式にできないのです。

そのためmidiOutLongMsg()関数は、非同期式で動作する仕様になっています。非同期式とは、関数が実行された段階でデバイスドライバーは、「はい、ロングメッセージの送信ですね、受け付けました。」と言うだけで制御を戻します。

midiOutLongMsg()関数の実行が終わっても、メッセージが送信されたわけではなく、バッファのアドレスもMIDIデバイスが参照したままになっています。そのため、不用意にバッファの開放などを行うと、トラブルの原因となります。

送信の手順

デバイスのオープン・クローズ

MIDIメッセージを送信するために、MIDI再生デバイスを用います。MIDIメッセージを送信するときは、デバイスがオープンの状態でなければなりません。

運用開始前に、MIDIデバイスをオープンし、すべての運用が終わったら、デバイスをクローズします。デバイスがオープン中は、いつでもMIDIメッセージを送信することができます。

デバイスのオープン・クローズをいつ行うかについては、セクション「MIDIデバイスの運用」を参照してください。

ロングメッセージの送信

以下にロングメッセージを送信する手順を示します。準備から後処理まで3つの作業が必要です。

バッファのアロケートとMIDIヘッダーの準備

1つのロングメッセージに付き1組のバッファとMIDIヘッダーを用意します。アロケートしたバッファにメッセージをコピーし、MIDIヘッダーに登録します。そしてmidiOutPrepareHeader()関数でMIDIヘッダーを準備します。

バッファのアロケートとMIDIヘッダーの準備は、マルチメディアタイマーを用いた割り込みルーチン内で行うことはできません。この制限さえ守れば、どのスレッドを用いても、またいつ行ってもかまいません。送信直前に行うこともできますし、曲データ(SMF)をロードしたときに準備してしまうことも可能です。(ただし、デバイスはオープンしておく必要があります。)

ロングメッセージの送信

midiOutLongMsg()関数を実行するとロングメッセージが送信されます。MIDIメッセージの送信は、割り込みルーチンの中で行うことが可能なため、厳密な時刻管理を行いながら送信することができます。

一般にシステムエクスクルーシブは、音源の設定などに用いられ、送信のタイミングそのものが重要であることは少ないメッセージです。それでもいつ送信するかという問題は、MIDIトラフィックの緩和の問題もあって考慮しなければなりません。曲の先頭にあって、音源のプリセットを行うものと、曲の途中にあって音源の設定を変更するものでは、送信タイミングの考え方も異なります。

時間管理を行わない送信では、MIDIヘッダーの準備とメッセージの送信は、一連の作業として行うことが可能です。一方、音楽の再生中に送信する場合は、演奏に先立ってMIDIヘッダーを準備するプロセスと、決められた時刻に送信コマンドを実行するプロセスに分ける必要があります。

MIDIヘッダーの準備とロングメッセージの送信については、セクション「MIDIの演奏エンジン」を参照してください。

送信済みMIDIヘッダーの後処理とバッファの開放

ロングメッセージの送信が非同期で行われるため、いつ送信が終了したかを知ることは、重要なテーマです。アプリケーションが送信終了を知る方法には、大きく、MIDIデバイスから教えてもらう方法と、自力で調べる方法の2つがあります。

デバイスからの通知による方法には、ウィンドウメッセージを受け取る方法や、イベントハンドルがシグナルになるのを待つ方法など、いくつか選択肢があります。自力で調べる方法は、MIDIヘッダーのフラグを定期的にチェックする、ポーリング手法を用います。

送信終了を確認したら、MIDIヘッダーの後処理を行い、バッファを開放します。

デバイスからの通知については、セクション「コールバック」を参照してください。

処理の流れ

ロングメッセージの送信では、3つの処理を行います。

これらの処理をどのスレッドで行うか、そのタイミングはいつか、という観点から実装スタイル別に紹介します。

同期処理

3つの処理を同じスレッドで、同期的に行うものです。メッセージを送信した後、送信が終わるまで待機する方法です。送信が終了したかどうかは、MIDIヘッダーのMHDR_DONEフラグをチェックすることで行います。

この処理をコーディングしたものを示します。

<C++>
────────────────
midiOutPrepareHeader(hMidiOut, pMidiHdr, sizeof(MIDIHDR);

midiOutLongMsg(hMidiOut, pMidiHdr, sizeof(MIDIHDR);

while ((pMidiHdr.dwFlags & MHDR_DONE) != MHDR_DONE)
{
    Sleep(1);
}
midiOutUnprepareHeader(hMidiOut, pMidiHdr, sizeof(MIDIHDR);

この方法は、実装が簡単である、というの利点があります。ただし、上記で説明したとおり、送信中はスレッドがブロックされるため、メインスレッド、つまりユーザーインターフェースを担当するスレッドで実行すべきではありません。この手法で実装するときは、必ず作業用スレッドを用いてください。

C#で実装する場合は、スレッドプールやタスクなど、バックグランドで動作するスレッドが簡単に利用できるので、これらの仕組みを利用する方法もあります。詳しくは、セクション「C#によるMIDIメッセージの送信」を参照してください。

このアルゴリズムは、ボタンをクリックすると、音源のリセットコマンドを送信する、などの用途で用いることができます。しかし、決められた時刻にメッセージを送信する用途には向かないため、音楽演奏にこのアルゴリズムを用いることはできません。音楽演奏では、後述するマルチメディアタイマーを用いた割り込み方式を用います。

送信と回収

一般的なアプリケーションソフト

次の例は、ウィンドウメッセージを用いる一般的な方法です。MIDIデバイスをオープンするときに、ウィンドウハンドルを登録しておくと、ロングメッセージの送信が終わったとき、MM_MOM_DONEメッセージがウィンドウに送られます。メッセージは、WindowProc()関数を用いるか、専用のディスパッチ関数を用いて受け取ります。

この方法は、スレッドがブロックされないので、メインスレッドを用いてバッファの準備、および後処理を行うことができます。

以下にMFCのCViewに実装した例を示します。WindowProc()関数の中で、MM_MOM_DONEメッセージをディスパッチしています。

<C++>
────────────────
LRESULT MyView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case MM_MOM_OPEN:       break;
    case MM_MOM_CLOSE:      break;
    case MM_MOM_DONE:
        // MIDIヘッダーの処理
        break;
    }
    return CView::WindowProc(message, wParam, lParam);
}

DLL

DLLにMIDIメッセージの送信サービスを実装するときは、ウィンドウメッセージの代わりにスレッドメッセージを用います。これは、DLLにはメッセージを受け取るウィンドウが存在しないためです。DLL内にスレッドメッセージを受け取るための作業用スレッドを実装し、そこでMM_MOM_DONEメッセージを受け取ります。

バッファの準備と送信は、DLLの送信コマンドをコールしたスレッドで行われ、バッファの後処理は、DLLの作業スレッドで行われます。

スレッドメッセージの受信

作業用スレッドでスレッドメッセージを受け取るには、以下の方法があります。

メッセージキューの作成

メッセージキューは、スレッドメッセージをスレッド自身に送信することで作成することができます。スレッドは、スレッドメッセージを受け取ると、メッセージキューが必要であると判断し、メッセージキューを準備します。

PostThreadMessage()でメッセージを送信し、PeekMessage()で読み出しができるまで、メッセージを送信し続けます。メッセージキューが準備されたら、GetMessage()でメッセージを受け取ります。以下にサンプルコードを示します。

<C++>
────────────────
UINT WINAPI MidiDaemon(LPVOID lpv)
{
MSG     msg;
BOOL    ret;

    while (1)   // Force the system to create the message queue
    {
        PostThreadMessage(自分自信のスレッドアドレス, WM_APP, 0, 0);
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))   break;
        Sleep(0);
    }
    while (1)   // Message Dispatch Loop
    {
        ret = GetMessage(&msg, NULL, 0, 0);
        if (ret ==  0)      break;      // WM_QUIT
        if (ret == -1)      break;      // Error

        switch (msg.message)
        {
        case MM_MOM_DONE:
            // MIDIヘッダーの処理
            break;
        case HOGE:
            // その他の処理
            break;
        }
    }
    return 0;
}

メッセージキューが支援されたら、このスレッドでMM_MOM_DONEなどのメッセージを受け取ることができます。

MsgWaitForMultipleObjects()関数

MsgWaitForMultipleObjects()関数は、本来カーネルオブジェクトがシグナルになるのを待つ関数ですが、同時にスレッドメッセージを受け取る機能も持っています。

以下のサンプルは、カーネルオブジェクト周辺の処理を省略し、スレッドメッセージの受け取りに関連するコードのみを示しています。

<C++>
────────────────
UINT WINAPI MidiDaemon(LPVOID lpv)
{
MSG     msg;
BOOL    run = TRUE;

    while (run)
    {
        DWORD dwResult = MsgWaitForMultipleObjects(
            0, NULL, FALSE, INFINITE, QS_ALLEVENTS);

        if (dwResult == WAIT_OBJECT_0)
        {
            while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            {
                switch (msg.message)
                {
                case MM_MOM_DONE:
                    // MIDIヘッダーの処理
                    break;
                case HOGE:
                    // その他の処理
                    break;
                case WM_QUIT:
                    run = FALSE;
                    break;
                }
            }
        }
    }
    return 0;
}

準備と演奏、そして回収

時間管理を行いながらMIDIメッセージを送信するときは、メッセージの送信をマルチメディアタイマーを用いた割り込み関数の中で行わなければなりません。また、割り込み関数の中では、MIDIヘッダーの準備ができないため、演奏に先立ってMIDIヘッダーの準備を済ませておく必要があります。送信済みのMIDIヘッダーは、ウィンドウメッセージあるいはスレッドメッセージによって回収します。

MIDIヘッダーの準備は、ショートメッセージやテンポ情報の準備も含め、演奏時刻より前に行います。MIDIの演奏エンジンの実装については、セクション「MIDIの演奏エンジン」を参照してください。

その他の回収方法

上記では、ロングメッセージの送信終了を知る手段として、ウィンドウメッセージとスレッドメッセージを紹介しましたが、これ以外にもイベントによる通知と、ポーリングによる探索という方法があります。

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ