DirectMusicツール

DirectMusicツールというのは、送られてくるパフォーマンスメッセージを横取りして操作する仕組みのことで、音符一つ一つに対する操作や、パラメータの変化などを見ることができます。
DirectMusicツールを作るには、IDirectMusicToolインターフェイスを継承してクラスを作ります。

ちょっとしたサンプル

今回は例として、音程を変えて曲の雰囲気を変えてしまうツールを作ってみます。
いや、本当はゲーム作ってるときにできたものの流用なんですけど。

class CBWBGMModulatorTool : public IDirectMusicTool
{
public:
    CBWBGMModulatorTool()
    {
        m_cRef = 1;
        InitializeCriticalSection(&m_CrSec);

        for (int i=0; i<12; i++) Mobility[i] = 0;
    }

    virtual ~CBWBGMModulatorTool()
    {
        DeleteCriticalSection(&m_CrSec);
    }

    virtual STDMETHODIMP QueryInterface(const IID &iid, void **ppv)
    {
        if (iid == IID_IUnknown || iid == IID_IDirectMusicTool)
        {
            *ppv = static_cast<IDirectMusicTool*>(this);
        } else
        {
            *ppv = NULL;
            return E_NOINTERFACE;
        }

        reinterpret_cast<IUnknown*>(this)->AddRef();
        return S_OK;
    }

    virtual STDMETHODIMP_(ULONG) AddRef()
    {
        return InterlockedIncrement(&m_cRef);
    }

    virtual STDMETHODIMP_(ULONG) Release()
    {
        if( 0 == InterlockedDecrement(&m_cRef) )
        {
            delete this;
            return 0;
        }
        return m_cRef;
    }

    HRESULT STDMETHODCALLTYPE Init(IDirectMusicGraph* pGraph)
    {
        return E_NOTIMPL;
    }

    HRESULT STDMETHODCALLTYPE GetMsgDeliveryType(DWORD* pdwDeliveryType)
    {
        *pdwDeliveryType = DMUS_PMSGF_TOOL_IMMEDIATE;
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE GetMediaTypeArraySize(DWORD* pdwNumElements)
    {
        *pdwNumElements = 1;
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE GetMediaTypes(DWORD** padwMediaTypes,DWORD dwNumElements)
    {
        if (dwNumElements == 1) {
            (*padwMediaTypes)[0] = DMUS_PMSGT_NOTE;
            return S_OK;
        }else {
            return E_FAIL;
        }
    }

    HRESULT STDMETHODCALLTYPE ProcessPMsg(IDirectMusicPerformance* pPerf,DMUS_PMSG* pDMUS_PMSG)
    {
        if ((NULL == pMsg->pGraph) || FAILED(pMsg->pGraph->StampPMsg(pMsg))) return DMUS_S_FREE;

        HRESULT ret = DMUS_S_REQUEUE;

        EnterCriticalSection(&m_CrSec);
        if (pMsg->dwType==DMUS_PMSGT_NOTE && pMsg->dwPChannel!=9) {
            DMUS_NOTE_PMSG* pNote = (DMUS_NOTE_PMSG*)pMsg, *pNew;
            int m = Mobility[pNote->wMusicValue%12];
            if (pNote->bPlayModeFlags==DMUS_PLAYMODE_FIXED&&m) {
                if( SUCCEEDED( pPerf->AllocPMsg( sizeof(DMUS_NOTE_PMSG),
                    (DMUS_PMSG**)&pNew )))
                {
                    // オリジナルの音符をこのメッセージにコピーする。
                    memcpy( pNew, pNote, sizeof(DMUS_NOTE_PMSG) );

                    // オブジェクトへのポインタを保持または保持している
                    // 可能性のあるフィールドの Addref か消去を実行する。
                    if( pNew->pTool ) pNew->pTool->AddRef();
                    if( pNew->pGraph ) pNew->pGraph->AddRef();
                    pNew->punkUser = NULL;

                    pNew->wMusicValue += m;
                    pNew->bMidiValue += m;

                    // メッセージを送信する。
                    pPerf->SendPMsg( (DMUS_PMSG*)pNew );
                    ret = DMUS_S_FREE;
                }
            }
        }
        LeaveCriticalSection(&m_CrSec);

        return ret;
    }

    HRESULT STDMETHODCALLTYPE Flush(IDirectMusicPerformance* pPerf,DMUS_PMSG* pDMUS_PMSG,REFERENCE_TIME rtTime)
    {
        return E_NOTIMPL;
    }

private:
    long m_cRef; // 参照カウンタ。
    CRITICAL_SECTION m_CrSec; // メンバ変数をスレッドセーフにする。

public:
    char Mobility[12];
};

コンストラクタ・デストラクタ

これ自体はC++の基本なんで別に説明する必要も無いと思いますが、一応ここではメンバ変数の初期化と、それに対応する終了処理を行っています。
DirectMusicはマルチスレッドで曲を演奏しているので、その対策にクリティカルセクションを使ってます。

QueryInterface、AddRef、Release

IUnknownインターフェースの関数です。
ほぼ定型文なので気にせずコピーでよいでしょう。

Init

関数名の通り、初期化を行いますが、今回はコンストラクタで初期化を済ませてしまったので、E_NOTIMPL(未実装)を戻り値として返しています。
クラスの概念のないC言語などで組むときに必要になるのかなーと勝手に考えています。

GetMsgDeliveryType

ツールにメッセージが配信される時期を決めるそうです。
元々DirectMusicは何か処理をするときにもたついても演奏に影響が出ないように、遅延時間の分だけ早め早めにメッセージを送り出すようになっているため、早めに処理しておきたい場合やぎりぎりまで待ってから処理したい場合など、時と場合によってタイミングを制御するといいことがある…と思うのです。
即座に、すなわち遅延時間の分だけ早めに処理するDMUS_PMSGF_TOOL_IMMEDIATE、ぎりぎりまで待ってから素早く処理することになるDMUS_PMSGF_TOOL_QUEUE、たった今終わった瞬間のメッセージに対して処理をするDMUS_PMSGF_TOOL_ATTIME、の3つが選べます。
今回は、DMUS_PMSGF_TOOL_ATTIMEだと手遅れになるのでDMUS_PMSGF_TOOL_IMMEDIATEにしました。
こっちが早くて出力ツールが定刻通りなので若干演奏結果の反映に時間がかかりますが、既に鳴り始めた音までは操作できませんからどっちみち多少遅れることを考えると、余裕のあるDMUS_PMSGF_TOOL_IMMEDIATEが良いのだろうと思います。
DMUS_PMSGF_TOOL_ATTIMEにすると出力ツールにメッセージが届くのも遅れてしまうため特別なケースでもなければ使う機会はなさそうな気がします。

GetMediaTypeArraySize

GetMediaTypesとセットで使います。
GetMediaTypesで設定する要素の数を決めます。
今回はDMUS_PMSGT_NOTE(音符メッセージ)だけを処理するので1にしました。
あらゆる種類のメッセージを全部処理するときは0にするとGetMediaTypesを使わなくて済むそうです。

GetMediaTypes

GetMediaTypeArraySizeとセットで使います。
ここで設定した種類のメッセージだけを私のツールにくださいね、ということです。
GetMediaTypeArraySizeが0のときは未実装でかまわないそうです。

ProcessPMsg

ここが一番大事!
実際にメッセージが送られてきて、それに対して処理します。
メッセージの内容がツールを通るときに変化する場合は、古いメッセージを破棄して新しいメッセージを作ります。
消して作り直さなくても大丈夫みたいです。
戻り値は、古いメッセージを破棄する場合はDMUS_S_FREE、素通りさせる場合はDMUS_S_REQUEUE、とします。
今回は、音程に変化があるときにDMUS_S_FREE、変わらないときはDMUS_S_REQUEUEにしました。
新しいメッセージを作るほうに関してはまたの機会に。

Flush

パフォーマンスが停止したときに何かできるらしいです。
今回は来たメッセージを加工して送り出すだけで停止時にすべきことは特にないので未実装にしています。

おわりに

今回やったのはDirectMusicツールの作り方だけなので、このツールを登録してやらないと使えるようにはなりません。
それはまた、次の機会に。