DirectMusic でのカスタム ローディング

Peter Donnelly and Jim Geist Microsoft Corporation

June 2002

要約: デフォルトの Microsoft DirectMusic ローダーを置き換えて、暗号化されたファイルからコンテンツをロードできるクラスを実装する方法について説明します。独自のローダーを実装すれば、データを暗号化したり、複数のオブジェクトを独自の形式にパッケージ化できます。また、カスタム ローダーを作成することで、ロードの効率を高めることも可能になります。

MSDN Code Center から CustomLoadingSample.exe をダウンロードする。

はじめに

アプリケーションは、IDirectMusicLoader インターフェイスが公開する Microsoft® DirectMusic® ローダーを使用して、ファイルとリソースからのオブジェクトのロードを管理します。ローダーの仕事はデータを発見し、適切なオブジェクトを作成し、そのオブジェクトにデータをストリームして、データを管理することです。完成したオブジェクトは、音色コレクション、セグメント、およびスタイルなどの DirectMusic パフォーマンスのコンポーネントとなります。

ローダーの重要な機能の 1 つが、他のオブジェクトが参照しているオブジェクトをロードする能力です。たとえば、あるセグメントが、1 つまたは複数の WAV ファイルを参照して、そのウェーブ トラックで使用しているとします。この場合、アプリケーションがそのセグメントをロードするときに、ローダーは参照先の WAV ファイルを発見し、そのファイルもロードします。

デフォルトのローダーは、データが認識可能な形式になっていることを前提とします。通常は、DirectMusic Producer で作成されたファイルか、標準の WAV および MIDI ファイルです。ファイルが標準の DirectMusic コンテナ形式である場合に限り、1 つのファイルから複数のオブジェクトをロードできます。

アプリケーション設計者は、データを暗号化したり、複数のオブジェクトを独自の形式でパッケージ化したいと考えるときがあります。この場合には、独自のローダーを実装しなくてはなりません。この文章ではその方法を解説します。

付属のサンプル アプリケーションは、標準の DirectMusic ファイルのみをロードします。ただし、サンプル コードは簡単に変更して、他のソースからデータをロードさせることができます。カスタム ローダーは、データを発見し、(必要ならば) 復号することができれば、そのデータを標準の DirectMusic ロード可能オブジェクトに渡してロードのプロセスを完了させます。

この文章では以下のトピックを扱います。

  • ロードのしくみ。 オブジェクトをロードするために必要なステップの概要を示します。
  • カスタム ローダーを実装することの利点。 デフォルトのローダーとは異なる機能があると便利な理由を説明します。
  • ローダー クラスの実装。 カスタム ローダーの作成に関わるステップについて説明します。
  • 関連情報。 その他のドキュメンテーションへのリンクを示します。

ロードのしくみ

オブジェクトをロードするためには、DirectMusic ローダーとオブジェクト自身と間での協力が必要となります。ローダーはストリームからデータを読み込みます。オブジェクトはデータ読み込みを要求し、データを解析し、ロードされるオブジェクトが参照している他のオブジェクトを取得するためにローダーを呼び出します。

このプロセスを詳しく見ていきましょう。

オブジェクトのロードを開始するために、アプリケーションは IDirectMusicLoader::GetObject を呼び出し、DMUS_OBJECTDESC 構造体に情報を入れて渡します。この構造体の dwValidData メンバは、オブジェクトの識別と場所に関する情報を提供します。場所には次の 2 つのカテゴリがあります。

  • オブジェクトが、1 つのファイルに格納されているか、既知のメモリ位置にリソースとして置かれている場合。dwValidData メンバは、以下の値のうちの少なくとも1つを含んでいます。

    DMUS_OBJ_OBJECT

    DMUS_OBJ_FULLPATH

    DMUS_OBJ_FILENAME

    DMUS_OBJ_MEMORY

    また、このオブジェクトはグローバル ユニーク識別子 (GUID)、完全または部分ファイル パス、またはメモリ ポインタによって一意に識別します。この場合には、DirectMusic ローダーは IStream インターフェイスをサポートするオブジェクトを作成します。次に、ローダーはストリーム オブジェクトにファイルまたはリソースを関連付けます。

  • オブジェクトが、通常は複数のオブジェクトを含んでいるファイルからロードした結果として、既存のストリームの中に存在しする場合。dwValidData メンバは DMUS_OBJ_STREAM を含んでいます。この場合には、アプリケーションはストリーム オブジェクトを作成しており、DMUS_OBJECTDESC.pStream の中で IStream インターフェイス ポインタを提供します。アプリケーションは、データを正しい位置からロードするように、ストリームのシーク ポインタを管理する責任を負います。

次に、IDirectMusicLoader::GetObject メソッドは、要求された型のオブジェクトを作成し、その IPersistStream インターフェイスを取得します。次に、GetObjectIPersistStream::Load を呼び出し、IStream ポインタを渡します。

オブジェクトは IPersistStream::Load の実装によって自分自身をロードします。IStream::Read を呼び出して、自分に渡されたストリームからデータを取得することによってこれを行います。オブジェクトはこのデータを使って、トラックなどのオブジェクトを作成し、それが参照している他のオブジェクトをロードします。

ロード時に参照に遭遇した場合には、オブジェクトはローダーを呼び出して参照先のオブジェクトをロードしなくてはなりません。参照先のオブジェクトのロードは、ストリーム オブジェクトが IDirectMusicGetLoader インターフェイスをサポートしている場合にのみ可能です。ロードされるオブジェクトは、IPersistStream::Load への呼び出しで渡された IStream インターフェイスからこのインターフェイスを取得します。次に、オブジェクトは IDirectMusicGetLoader::GetLoader を呼び出して IDirectMusicLoader インターフェイスを取得します。このインターフェイスを使って、GetObject を呼び出し、参照オブジェクトを取得します。

カスタム ローダーを実装することの利点

ほとんどのアプリケーションではデフォルトの DirectMusic ローダーで十分ですが、このローダーは汎用的なソリューションとして設計されたものです。独自のローダーを実装すれば、データ ストレージの選択肢の幅を広げ、ロード プロセスの効率を高めることができます。

次に、カスタム ローダー作成の主な利点を示します。

  • ファイル内のデータを圧縮または暗号化できます。カスタム ローダーは、IStream::Read の実装の中で、データの圧縮解除または復号を行います。
  • 再利用されることがわかっているオブジェクトだけをキャッシングすることで、メモリの使用効率を高めることができます。デフォルトのローダーでは、クラス全体のキャッシングをオフにすることはできますが、個々のオブジェクトのキャッシングをオフにすることはできません。
  • 複数のオブジェクトを1つのファイルにパッケージ化できます。DirectMusic コンテナにオブジェクトをパッケージ化することも可能ですが、カスタム パッケージ形式を使用すれば、開発者の好みのファイル形式を使用できますし、GetObject メソッドを最適化してロード時間を短縮できます。

カスタム パッケージ形式のオブジェクトにもデフォルトのローダーを使用できますが、コンテンツが他のオブジェクトへの参照を含んでいる場合には、処理が複雑になります。通常、ローダーは、参照が発見されたストリームの中で、参照先のオブジェクトのデータの場所を知る方法を持っていません。ローダーは、参照先のオブジェクトが別のファイルの中にあると仮定します。ローダーが参照先のオブジェクトをパッケージ ファイルの中で発見できるようにするためには、ロードのプロセスを開始する前に、個々の参照先オブジェクトについて IDirectMusicLoader::SetObject を呼び出す必要があります。個々のオブジェクトについて、DMUS_OBJECTDESC.pStream は、シーク ポインタがオブジェクトのデータの先頭に設定された、IStream のコピーへのポインタを含んでいます。SetObject は、後でファイル名または GUID をストリームとマッチングできるように、すべてのオブジェクト記述を格納します。

これに比べると、ストリーム内の場所を調べるだけで、すべての GetObject 呼び出しを処理できる独自のローダーを実装した方が簡単な場合があります。また、このテクニックではパフォーマンスを最適化できます。たとえば、複数のオブジェクトをカスタム ファイルにパッケージ化するときには、ファイル名 (参照先に含まれているものも含む) を、オブジェクトのデータが始まるファイル内の 16 進オフセットの文字列表現に置き換えることができます。GetObject が呼び出されたら、メイン ストリームを複製して、DMUS_OBJDESC.wszFileName に含まれている場所をシークします。ストリームを複製するのは、メイン ストリームが参照である場合に、元のオブジェクトのデータの中での現在の場所を失わないようにするめです。

**注意   **コンテンツが循環参照を含んでいる場合には、デフォルトのローダーを使用するようにしてください。循環参照では、スクリプトが別のスクリプトを呼び出しており、そのスクリプトが元のスクリプトを呼び出しているような場合に発生します。適切なガーベジ コレクションが行われなければ、このようなオブジェクトはアプリケーションが解放した後もメモリ内に残ることがあります。詳細については、DirectX® Software Development Kit (SDK) のガーベジ コレクションのトピックを参照してください。

ローダー クラスの実装

アプリケーション内にカスタム ローダーを作成するためには、以下のインターフェイスを実装する必要があります。

  • IStream
  • IDirectMusicGetLoader
  • IDirectMusicLoader
  • SetObject (オプション)

以下のセクションでは、各インターフェイスの実装方法について解説します。

IStream

このインターフェイスは、ファイルまたはリソースからデータを読み込みます。以下のメソッドが実装されている必要があります。

  • **AddRef。**COM 参照を追加します。
  • **Release。**COM 参照を解放します。
  • **QueryInterface。**ロードされるオブジェクトが IDirectMusicGetLoader に対するクエリを発行できるようにします。
  • **Read。**ロード可能オブジェクトがその IPersistStream::Load メソッドで要求したデータを読み込みます。
  • **Seek。**ストリーム内の特定のポイントまでシークします。
  • **Clone。**現在のシーク ポインタを使って、ストリームのコピーを作成します。

この文章では IStream については詳しくは解説しません。その実装は、主にアプリケーションがどのようにしてデータを取得しているかに依存するからです。ファイルからデータを読み込む実装の例については、サンプル アプリケーションの Istream.cpp を参照してください。

IDirectMusicGetLoader

このインターフェイスは、IStream と同じクラスに実装されなくてはならず、GetLoader という 1 つのメソッドを持ちます。このメソッドは、ロードを開始したローダー オブジェクトへのポインタを返すので、GetObject を再帰的に呼び出すことで参照先のオブジェクトをロードできます。

IDirectMusicLoader

このインターフェイスは、ローダー クラスの中に実装します。ロード可能なオブジェクトが参照先のオブジェクトをロードするために呼び出す GetObject を実装する必要があります。通常、GetObject は、アプリケーションが参照先のオブジェクト以外のオブジェクトのロードを開始するために呼び出すメソッドともなります。これ以外の IDirectMusicLoader メソッドを実装することも可能ですが、それらのメソッドはアプリケーションからしか呼び出されないので、どうしても実装しなくてはならないというわけではありません。たとえば、EnableCacheClearCache を使用しないキャッシング システムを実装することも可能です。

標準の DirectMusic コンテナからロードしたい場合には、IDirectMusicLoader::SetObject も実装する必要があります。詳細については、「コンテナからのロード」を参照してください。

GetObject の実装

GetObject を実装するには、DMUS_OBJECTDESC 構造体で指定されたオブジェクトを取得し、必要ならばそれをロードするか、キャッシュから取得する必要があります。

サンプル アプリケーションをもとにした次のコードでは、まずキャッシュ内でオブジェクトを探します。キャッシュは、すでにロードされたオブジェクトのファイル名、GUID、および IDirectMusicObject インターフェイスをカプセル化した、アプリケーション定義の CObjectRef オブジェクトのリンク リストです (詳細については、「キャッシング」を参照)。

STDMETHODIMP CMyLoader::GetObject(LPDMUS_OBJECTDESC pDesc, 
   REFIID riid, LPVOID FAR *ppv)
{
    USES_CONVERSION;
 
    HRESULT hr;
    IDirectMusicObject *pObject = NULL;
 
    CObjectRef * pObjectRef = NULL;
 
    // キャッシュ内で GUID をスキャン
 
    if (pDesc->dwValidData & DMUS_OBJ_OBJECT)
    {
        for (pObjectRef = m_pObjectList; pObjectRef; 
            pObjectRef = pObjectRef->m_pNext)
        {
            if (pDesc->guidObject == pObjectRef->m_guidObject)
            {
                break;
            }
        }
    }

    // キャッシュ内でファイル名をスキャン

    else if (pDesc->dwValidData & DMUS_OBJ_FILENAME)
    {
        for (pObjectRef = m_pObjectList; pObjectRef; 
            pObjectRef = pObjectRef->m_pNext)
        {
            if (pObjectRef->m_pwsFileName && 
                !wcscmp(pDesc->wszFileName,pObjectRef->m_pwsFileName))
            {
                break;
            }
        }
    }

オブジェクトがキャッシュ内に見つかった場合、メソッドは QueryInterface を呼び出して、要求されたインターフェイスを取得します。その後、オブジェクトを再ロードする必要はないため、実行を終了します。

    if (pObjectRef)
    {
        hr = E_FAIL;
        if (pObjectRef->m_pObject)
        {
            hr = pObjectRef->m_pObject->QueryInterface( riid, ppv );
        }
        return hr;
    }

オブジェクトがキャッシュ内に見つからなかった場合、メソッドはオブジェクトを作成し、要求されたインターフェイスを取得します。

    hr = CoCreateInstance(pDesc->guidClass, NULL,CLSCTX_INPROC_SERVER,
        IID_IDirectMusicObject, (void **)&pObject);
    if (FAILED(hr))
    {
        return hr;
    }

残りのサンプル メソッドは、次のトピック「ファイルまたはリソースからのロード」で示しています。

すべてのロード可能なオブジェクトは、IDirectMusicObject および IPersistStream インターフェイスをサポートしています。サンプルは、オブジェクトを作成するとき、オブジェクトがキャッシングされていた場合に ParseDescriptor メソッドを通してオブジェクトに関する情報を取得できるように、IDirectMusicObject インターフェイスを取得します。次に、IPersistStream::Load を呼び出してオブジェクトのロードを開始できるように、オブジェクトの IPersistStream インターフェイスを取得します。Load メソッドは、オブジェクトのデータへのアクセスを提供する IStream ポインタを取ります。

IStream ポインタを取得するとき、ローダーのデフォルトの実装は、次の 2 つのケースを処理します。

  • オブジェクトが独自のファイルまたはリソースに含まれており、名前または GUID によって識別されている場合。この場合、GetObject は新しいストリームを作成します。
  • オブジェクトが既存のストリームの中に含まれている場合。これは、アプリケーションがパッケージ ファイルのためのストリームを作成し、ストリームのシーク ポインタを管理するときに起こります。

カスタム ローダーでこれらのシナリオをどうサポートすべきかは、コンテンツの性質とその格納方法に左右されます。この2つのケースを、さらに細かく見てみることにしましょう。

ファイルまたはリソースからのロード

DMUS_OBJECTDESC.dwValidData に DMUS_OBJ_STREAM フラグを含んでいない GetObject の呼び出しは、アプリケーションが直接行うか (通常は、別のファイルまたはリソースに格納されているオブジェクトをロードするため)、または参照を含んでいるロード可能なオブジェクトが行います。コンテンツがカスタム パッケージ形式になっており、参照を含んでいない場合 (たとえば、WAV サウンドだけから構成されている場合) には、このケースをサポートする必要はありません。

サンプル アプリケーションの GetObject の実装から取った次のコードは、ファイル名によってオブジェクトを識別するケースを処理しています。

if (pDesc->dwValidData & DMUS_OBJ_FILENAME)
{
    // ファイルをベースにしたストリームを作成し、
    // そこからロードする。
 
    WCHAR wzFileName[MAX_PATH];
    WCHAR wzExt[_MAX_EXT];
 
    _wmakepath(wzFileName, NULL, m_wzSearchPath, 
            pDesc->wszFileName, NULL);
    _wsplitpath(wzFileName, NULL, NULL, NULL, wzExt);
 
    CMyIStream *pStream = new CMyIStream;
    if (pStream == NULL)
    {
        hr = E_OUTOFMEMORY;
    }
 
    if (SUCCEEDED(hr))
    {
        hr = pStream->Attach(W2CT(wzFileName), this);
    }

CMyIStream::Attach メソッドは、ファイルを作成し、ハンドルをローダーへのポインタとともに保存します。

次に示すサンプルは、自分が作成したオブジェクトの IPersistStream インターフェイスを取得し、Load を呼び出して、オブジェクトに自分のデータをストリームから読み込ませています。

    IPersistStream *pPersistStream = NULL;
    if (SUCCEEDED(hr))
    {
        hr = pObject->QueryInterface(IID_IPersistStream,
               (void**)&pPersistStream);
    }
    if (SUCCEEDED(hr))
    {
        hr = pPersistStream->Load(pStream);
    }
    RELEASE(pStream);
    RELEASE(pPersistStream);
}

GetObject は、ストリームへの参照を解放しますが、オブジェクトは参照を持ち続けることがあります。たとえば、WAV サウンドのストリーミングを行うような場合です。参照カウントがゼロになった時点で、ファイルはクローズされ、ストリームは破棄されます。

ストリームからのロード

DMUS_OBJECTDESC.dwValidData に DMUS_OBJ_STREAM フラグを持つ GetObject の呼び出しは、アプリケーションが既存のストリームからオブジェクトのロードを開始したときに起こります。通常、これは、独自のストリームをアプリケーションが保持しているパッケージ ファイルに格納されているオブジェクトで生じます。

GetObject を呼び出す前に、アプリケーションはストリームのシーク ポインタが正しい場所にあることを確認する必要があります。次に、GetObject はストリームのコピーを、オブジェクトの IPersistStream::Load メソッドに渡します。元のストリームを渡してはなりません。ロード可能なオブジェクトは、ロード可能なオブジェクトまたはアプリケーションが他のシーク ポインタと干渉することがないように、独自のコピーを使用する必要があります。

次に、サンプル アプリケーションがストリームからのロード要求をどのように処理するのかを示します。

else if (pDesc->dwValidData & DMUS_OBJ_STREAM)
{
    IStream *pClonedStream = NULL;
    IPersistStream *pPersistStream = NULL;
 
    hr = pObject->QueryInterface(IID_IPersistStream,
           (void**)&pPersistStream);
    if (SUCCEEDED(hr))
    {
        hr = pDesc->pStream->Clone(&pClonedStream);
    }
 
    if (SUCCEEDED(hr))
    {
        hr = pPersistStream->Load(pClonedStream);
    }

    RELEASE(pPersistStream);
    RELEASE(pClonedStream);
}
else
{
    hr = E_FAIL;
}

コンテナからのロード

DirectMusic コンテナは、ローダーにとっての特殊なケースとなります。コンテナのロードの際には、そのコンテナに含まれているすべてのオブジェクトが、後にアプリケーションからロードできるようになっていなくてはならないからです。

コンテナの GetObject を呼び出すと、コンテナは自分が持っているすべてのオブジェクトを列挙します。コンテナ オブジェクトは、まず DMUS_OBJECTDESC 構造体の pStream および guidClass メンバに値を入れて、自分の全オブジェクトの DirectMusicLoader::SetObject を呼び出します。SetObject を実装する際には、適切なクラスのオブジェクトを作成し、IDirectMusicObject インターフェイスを取得し、同じ DMUS_OBJECTDESC ポインタを IDirectMusicObject::ParseDescriptor に渡さなくてはなりません。IDirectMusicObject::ParseDescriptor は、オブジェクトに関する残りの情報をストリームから読み込みます。次に、そのオブジェクトの DMUS_OBJECTDESC 構造体を格納し、オブジェクトを解放します。

**注意   **アプリケーションが DMUS_OBJECTDESC.pStream の中で IStream を渡す場合には、IStream::Clone を呼び出して、元の ParseDescriptor ではなくコピーを渡すように注意してください。元のポインタを渡した場合、アプリケーションがストリームを使い続けると、シーク ポインタは変化し、それ以降のロードの試みは失敗します。

すると、アプリケーションがコンテナ内のオブジェクトを要求するか、ロードされたオブジェクトが他のオブジェクトへの参照を含んでいた場合、GetObject はオブジェクトの Load メソッドにストリームを渡す前に、オブジェクトの場所をシークできなくてはなりません。これは、指定されたファイル名、オブジェクト名、または GUID が見つかるまで、格納されているオブジェクト記述をスキャンすることによって行います。次に、オブジェクト記述の中の IStream ポインタを複製し、このポインタを Load に返します。

キャッシング

ローダーは、オブジェクトが複数回ロードされるのを防ぐために、任意の種類のキャッシング方式を実装できます。キャッシング方式は、オプションではあるものの、特にコンテンツに他のオブジェクトを参照するオブジェクトが含まれている場合には、必ず実装するようにしてください。たとえば、Gm.dls をキャッシングしないと、コレクションを使用するセグメントをロードするたびに、コレクションの新しいインスタンスが作成されてしまいます。

サンプル アプリケーションは、スタイル、WAV サウンド、およびコレクションのキャッシングを実装しています。このキャッシュは、ロードしたオブジェクトのファイル名、GUID、および IDirectMusicObject インターフェイス ポインタをそれぞれ格納した、アプリケーション定義の CObjectRef オブジェクトのリンク リストを保持しています。オブジェクトのロードの前に、このリストがどのようにスキャンされるかという点については、すでに「GetObject の実装」の項で説明しました。

次に示す、サンプルの GetObject メソッドのコードは、「ストリームからのロード」の項のコードの続きです。オブジェクトのロードは終わっています。次は、その GUID とファイル名を記述子から取得し、オブジェクトをキャッシュに追加します。

if (SUCCEEDED(hr))
{
    if ((pDesc->guidClass == CLSID_DirectMusicStyle) ||
        (pDesc->guidClass == CLSID_DirectSoundWave) ||
        (pDesc->guidClass == CLSID_DirectMusicCollection))
    {
        DMUS_OBJECTDESC DESC;
        memset((void *)&DESC,0,sizeof(DESC));
        DESC.dwSize = sizeof (DMUS_OBJECTDESC); 
        pObject->GetDescriptor(&DESC);
        if ((DESC.dwValidData & DMUS_OBJ_OBJECT) || 
            (pDesc->dwValidData & DMUS_OBJ_FILENAME))
        {
            CObjectRef * pObjectRef = new CObjectRef;
            if (pObjectRef)
            {
                pObjectRef->m_guidObject = DESC.guidObject;
                if (pDesc->dwValidData & DMUS_OBJ_FILENAME)
                {
                    pObjectRef->m_pwsFileName = 
                        new WCHAR[wcslen(pDesc->wszFileName)+1];
                    wcscpy(pObjectRef->m_pwsFileName,
                        pDesc->wszFileName);
                }
                pObjectRef->m_pNext = m_pObjectList;
                m_pObjectList = pObjectRef;
                pObjectRef->m_pObject = pObject;
                pObject->AddRef();
            }
        }
    }

デフォルトの Gm.dls コレクションは、コンパイルの際に NEED_GM_SET が定義されていた場合にのみ、ローダーの Init メソッドの中で別途キャッシングされることに注意してください。コレクションの GUID が格納されるのは、ロードされるコンテンツ内の参照を処理するために GetObject が呼び出されたときに、オブジェクトを発見できるようにするためです。この例では、DMUS_OBJECTDESC.dwValidData には DMUS_OBJ_OBJECT が入っています。

WAV サウンドの長さの設定

サンプル アプリケーションの GetObject メソッドには、WAV ファイルを処理するためのコードも含まれています。

if (SUCCEEDED(hr) && _wcsicmp(wzExt, L".wav") == 0 
        && riid == IID_IDirectMusicSegment8)
{
    mt = MusicTimeFromWav(pStream);
}

アプリケーション定義の MusicTimeFromWav 関数のコメントからわかるように、ここでは、波形の長さをミュージック タイム単位で計算することになっています。これは、アプリケーションが DirectMusic Producer セグメントに変換されていない WAV サウンドを再生し、これらのサウンドを次々に再生していくためのキューを発行したいような場合に必要となります。

サンプルは、ファイルを解析し、120 ビート/分のテンポに基づいて WAV の長さを計算します。次に、IDirectMusicSegment8::SetLength を呼び出します。アプリケーションは IDirectMusicSegment8::GetLength を呼び出してこの値を取得でき、必要ならば現在のテンポに合わせて値を調整することができます。

関連情報

DirectMusic でのオブジェクトのロードの詳細については、DirectX SDK を参照してください。