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

コールバック方式

コールバック方式の選択は、録音・再生とも同様の方法で行うことができます。デバイスをオープンする関数の6番目の引数DWORD fdwOpenに方式に応じてフラグを設定します。コールバック方式には以下の種類があります。

CALLBACK_WINDOW

コールバックウィンドウ方式は、ユーザーインターフェースを持つアプリケーション、言い換えるとウィンドウを持つアプリケーションで用いることができます。ウィンドウを持つアプリケーションはシステムから様々なメッセージを受け取ります。メッセージの受け取りには、WindowProc()関数を用いる方法と、メッセージ固有のディスパッチ関数を用いる方法があります。

再生デバイスは、WindowProc()関数に以下のメッセージを送ります。

メッセージ 意味
MM_WOM_OPEN デバイスのオープンが終了した。
MM_WOM_DONE バッファの再生が終了した。
MM_WOM_CLOSE デバイスのクローズが終了した。

以下に、WindowProc()関数のサンプルを示します。

LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
  switch (message)
  {
  // 様々なメッセージの処理ルーチン
  ........
  case MM_WOM_OPEN:   /* OPEN終了の処理 */    break;
  case MM_WOM_DONE:   /* 再生終了の処理 */    break;
  case MM_WOM_CLOSE:  /* CLOSE終了の処理 */   break;
  }
  ........

メッセージ固有のディスパッチ関数は以下の要領で実装します。ここではMFCアプリケーションでCViewクラスを用いていることを想定しています。

CMyViewクラスのヘッダーファイルにON_MESSAGEマクロを挿入します。挿入する位置は以下のサンプルを参照してください。このマクロにはメッセージ名と呼び出す関数名を設定します。関数名は自由に付けることができます。

BEGIN_MESSAGE_MAP(CMyView, CView)
  //{{AFX_MSG_MAP(CMyView)
  ON_WM_CREATE()
  ON_WM_DESTROY()
  ......
  //}}AFX_MSG_MAP
  ON_MESSAGE(MM_WOM_DONE, OnWavePlayDone)
END_MESSAGE_MAP()

CMyViewクラスの実装ファイルに以下の関数を挿入します。

LRESULT CMyView::OnWavePlayDone(
  WPARAM    wParam,
  LPARAM    lParam)
{
  // 再生済みバッファが戻ってきた
  return 0;
}

メッセージの受け取りは、WindowProc()関数とディスパッチ関数のどちらを用いても構いません。

メッセージの処理

WPARAM wParam、LPARAM lParamは、それぞれのメッセージで以下の意味を持ちます。

変数 意味
HWAVEOUT 再生デバイスを認識するハンドル
LPWAVEHDR ヘッダー情報を格納した構造体へのポインター
 
メッセージ wParam lParam
MM_WOM_OPEN HWAVEOUT 未使用
MM_WOM_DONE HWAVEOUT LPWAVEHDR
MM_WOM_CLOSE HWAVEOUT 未使用

MM_WOM_DONEはサウンドバッファの再生が終了したことを示します。MM_WOM_DONEメッセージを処理する場所は、再生済みバッファの後処理と次のバッファを用意するには最適な場所です。

WPARAM wParam、LPARAM lParamは、それぞれ以下のキャストで目的の変数に変換します。

    HWAVEOUT hWaveOut = (HWAVEOUT)wParam;
    LPWAVEHDR pWaveHdr = (LPWAVEHDR)lParam;
WindowProc()関数やディスパッチ関数には、CALLBACK_FUNCTIONで用いられる割り込み関数についての制限事項が存在しません。そのためここでは再生済みバッファの後処理を含め様々な処理を行うことができます。

MM_WOM_OPEN、MM_WOM_CLOSEはデバイスがオープン・クローズされたことを示すものですが、特に行うべき作業が無い場合は空文とします。

CALLBACK_FUNCTION

コールバックファンクッション方式は、アプリケーションで定義した関数が、イベントが起った時に呼び出されます。waveOutProcは仮の名前で、プログラマーは自分の好きな名前を付けることができます。ただし引数並びは変更してはいけません。この関数は呼び出し規約をCALLBACKにする必要があります。また戻り値は無いのでvoidとします。

void CALLBACK waveOutProc(
    HWAVEOUT    hwo,
    UINT        uMsg,
    DWORD       dwInstance,
    DWORD       dwParam1,
    DWORD       dwParam2);

HWAVEOUT hwo

再生デバイスを認識するハンドルです。waveOutOpen()関数で得られたものと同じものが入っています。

UINT uMsg

メッセージ内容に応じて次の値がセットされます。

メッセージ 意味
WOM_OPEN デバイスのオープンが終了した。
WOM_DONE バッファの再生が終了した。
WOM_CLOSE デバイスのクローズが終了した。

DWORD dwInstance

waveOutOpen()関数で指定したユーザインスタンスデータDWORD dwCallbackInstanceが入っています。

DWORD dwParam1

メッセージパラメータが入ります。

DWORD dwParam2

メッセージパラメータが入ります。

戻り値

なし

コールバック関数の制限

コールバック関数は、Windows95/98では割り込み関数として実装されています。割り込みという名の由来は、システムがどういう状態でも処理を中断して割り込むというところから来ています。

システムには安全のため分割できない一連の作業があります。ところが割り込み関数にはそれら重要な作業の途中でも実行されるという性質があります。そのためコールバック関数は実行される時刻は正確なのですが、その瞬間システムが不安定になっている可能性があります。

そこでシステムの安全を考えコールバック関数の内部で使用できるAPIは極めて数が限られています。

Windows2000以降、コールバック関数はリアルタイムプライオリティのスレッドとして実装されています。スレッドであれば割り込みよりも制限は少ないはずですが、マイクロソフトはその点について言及していません。いずれにしても安全のため、コールバック関数の内部から呼び出すことができる関数は制限されると考えた方が良いでしょう。

以下にコールバック関数の内部で使用できるAPIを示します。これ以外のシステム定義関数は呼び出さないでください。alloc()はもちろんstrcpy()すら禁止されています。

問題点はこの表の中にサウンドバッファの準備や後処理を行う関数が含まれていないということです。つまりコールバック関数内で直接WOM_DONEメッセージの処理を行うことはできません。そのため、メッセージを受け取ったらPostMessageやPostThreadMessage、SetEventなどを用いて安全な環境にメッセージを転送することになります。

しかしこれらの転送を行うのであれば、始めから対応するコールバック方式を選んだ方が話が早いということになります。あえてコールバックファンクッション方式のメリットを言えば、転送先をダイナミックに変更したり、メッセージIDを別のものに差し替えるなど柔軟なプログラミングが可能になる点でしょう。

転送手段 コールバック方式
PostMessage CALLBACK_WINDOW
PostThreadMessage CALLBACK_THREAD
SetEvent CALLBACK_EVENT

CALLBACK_THREAD

コールバックスレッド方式はメッセージのディスパッチ機能を持ったスレッドにスレッドメッセージを送ります。一般的なスレッドは外部からメッセージを受け取る機能はありませんが、以下の方法でメッセージを受け取る機能を追加することができます。

スレッドを起動します。ここではDaemonThreadというクラスに実装しています。

void DaemonThread::BeginThread(void)
{
    m_hThread = (HANDLE)_beginthreadex(     // Begin daemon thread
        NULL,
        0,
        ThreadFunc,
        this,
        0,
        &m_uThrdAddr);
}

スレッドに入ったら自身にスレッドメッセージを送り続けます。システムはこれを受けてメッセージのディスパッチ機能をスレッドに追加します。以降、GetMessage()関数によってデバイスからのメッセージを待機します。

UINT WINAPI DaemonThread::ThreadFunc(LPVOID lpv)
{
MSG msg;

    DaemonThread* pDaemonThread = (DaemonThread*)lpv;

    while (1)   // Force the system to create the message queue
    {
        PostThreadMessage(pDaemonThread->m_uThrdAddr, WM_APP, 0, 0);
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))   break;
        Sleep(0);
    }

    while (TRUE)        // Message Dispatch Loop
    {
        GetMessage(&msg, NULL, 0, 0);

        switch (msg.message)
        {
        case MM_WOM_OPEN:           // NOP
            break;
        case MM_WOM_CLOSE:          // NOP
            break;
        case MM_WOM_DONE:
            // 再生済みバッファが戻ってきた
            break;
        case WM_APP:
            _endthreadex(0);
            return 0;
        }
    }
    return 0;
}

MSGは以下に示す構造体です。UINT message, WPARAM wParam, LPARAM lParamなどのパラメータを取り出すことができます。

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

別の方法としてMsgWaitForMultipleObjects()関数を用いる方法があります。この関数は本来イベントハンドルを監視するためのものですが、併せてスレッドメッセージを受け取ることも可能です。イベントハンドルの準備など処理が複雑ですので、イベントハンドルの監視を主目的としたときに用いると良いでしょう。

MsgWaitForMultipleObjects()関数はハンドルがシグナルになるかスレッドメッセージを受信するまで処理をブロックします。戻り値がハンドル数より小さいとき、ハンドルがシグナルになったことを示し、戻り値がハンドル数に等しいとき、スレッドメッセージを受信したことを示します。

ここでメッセージを受け取るためにPeekMessage()関数を用いていることに注意してください。それはここに制御がきたときメッセージを受信していることが確実だからです。

DWORD WINAPI DaemonThread::ThreadFunc(LPVOID lpv)
{
MSG msg;

    DaemonThread* pDaemonThread = (DaemonThread*)lpv;

    while (TRUE)
    {
        DWORD dwResult = MsgWaitForMultipleObjects(
            NUMHANDLE, pDaemonThread->m_hEventHandle, FALSE, INFINITE, QS_ALLEVENTS);

        if (dwResult < (WAIT_OBJECT_0 + NUMHANDLE))
        {
            // イベントハンドルの処理
            continue;
        }
        if (dwResult == (WAIT_OBJECT_0 + NUMHANDLE))
        {
            while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            {
                switch (msg.message)
                {
                case MM_WOM_OPEN:           // NOP
                    break;
                case MM_WOM_CLOSE:          // NOP
                    break;
                case MM_WOM_DONE:
                    // 再生済みバッファが戻ってきた
                    break;
                case WM_APP:
                    _endthreadex(0);
                    return 0;
                }
            }
        }
    }
    return 0;
}

CALLBACK_EVENT

コールバックイベント方式は、資料が少なく実装例もほとんど紹介されていません。筆者もこの方式だけは実装したことがありません。詳しいことが分かればまた紹介したいと思います。

CALLBACK_NULL

コールバックを用いない設定です。この設定を選択すると、サウンドバッファの再生が終わってもデバイスからの連絡はありません。そのためアプリケーション側ではポーリング手法を用いてバッファが再生済みかどうかチェックしなければなりません。

WAVEHDR構造体のメンバーdwFlagsにWHDR_DONEフラグが立てば、そのバッファは再生が終了したことを示します。アプリケーションは運用中のすべてのWAVEHDR構造体について定期的にdwFlagsにWHDR_DONEフラグが立っていないかチェックします。フラグが立っているものは再生済みとして後処理を行います。

ドキュメントの先頭へ

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