スクリプティングのテクニック(中級): 可変再生と自己変更型ミュージック

Scott Selfon
Audio Content Consultant, Xbox Advanced Technology Group

November 2001

要約: 本ドキュメントは、Microsoft DirectMusic 用のコンテンツ作成ツールである Microsoft DirectMusic Producer のさまざまな側面を扱った一連の記事のうちの一編です。このドキュメントでは、Microsoft DirectX オーディオ スクリプティングに関する中級レベルのトピックを扱います。Microsoft DirectX オーディオ スクリプティングは、コンテンツを使用しているアプリケーションとは独立に、再生の細かい制御に使用できる単純なプログラミング言語です。作曲者やサウンド デザイナの立場から焦点を当てています。

はじめに

Microsoft(R) DirectX(R) オーディオ スクリプティング言語 AudioVBScript は高度な柔軟性を備えており、本シリーズの他の記事では、この言語のほんの表面にしか触れることができませんでした。この記事では、典型的なコンテンツ再生シナリオに沿って、高度な機能をいくつか紹介します。まず、複数セグメントのランダム再生を扱います。その後で、ゲームの状態に合わせてミュージックをいつどのように変化させるかを決めるためのポーリング変数を含むて、自己変更型のミュージックについていくつかの作成方法を紹介します。最後に、DirectX Software Development Kit (SDK) に収録されている "Baseball" スクリプティング サンプルについて簡単に説明します。

この記事では以下のトピックを扱っています。

  • ランダムなセグメント再生
  • 自己変更型のミュージック
  • 自己ポーリング型のミュージック
  • Baseball サンプル スクリプト
  • 関連情報

ランダムなセグメント再生

Microsoft DirectMusic(R) はいくつかのレベルで可変性を備えており、再生のたびにコンテンツに微妙な、または劇的な変化を加えることができます。

一番下のレベルでは、パターン内の個々のノートが、異なる開始時刻、時間幅、ベロシティを持つことができます。この種の細かい可変性は、Microsoft DirectMusic(R) パフォーマンスに任されており、サウンド デザイナや作曲者が特定のノートを 80 プラスマイナス 20 のベロシティで再生するように指示すると、DirectMusic はそのノートの再生時に、60~100 の範囲の値をランダムに選択します。つまり、値の範囲を決定した後は、DirectMusic パフォーマンスに特定の値の選択を任せることになります。

より上のレベルでの可変性として、ウェーブ トラックとパターンのパートごとに、32 種類のバリエーションを用意できます。音楽的アイデアを作成する作曲者は、これらのバリエーションをどのように選択するかをより細かく制御できます。たとえば、完全にランダムな選択を行うこともできますし、すべてのバリエーションが一通り再生されるまでは同じバリエーションを二度再生しないように指定することも可能です。

上記のどちらの場合でも、コンテンツがその可変性を指定します。アプリケーション開発者は、バリエーションの選択や、ノートのランダムなベロシティを生成するためにプログラミング作業を行う必要はありません。しかし、これよりもさらに上位レベルの可変性が必要な場合もあります。たとえば、セグメントの長さは固定されています。しかし、長さが異なる複数の音楽的アイデアがある場合にはどうすればいいのでしょうか? このような、従来は開発者の関与が必要だった場合においては、DirectX オーディオ スクリプティングが解決方法を提供できます。

まずシナリオを設定しましょう。アクション キューが複数のセグメントに分割されており (Action1.sgp、Action2.sgp など)、各セグメントは他のセグメントが終了した時点で開始されるものとします。話を単純にするために、セグメントをランダムに選択してミュージックを再生し、1 つのセグメントが終了したら別のセグメントをランダムに選択して再生を開始することにします。セグメントの長さはそれぞれ異なるので、複数のウェーブ トラック バリエーションを1つのセグメントに入れるという方法では対応できません(ただし、長さが同じならば、この方法でスクリプティングの量をある程度減らせます)。

では、最初に再生するセグメントを選択するルーチンを作成してみましょう。乱数ジェネレータである Performance.Rand メソッドを使用します。このメソッドの形式は Rand (x) で、メソッドは 1 以上 x 以下の整数値を返します。4 つのセグメントがある場合、ルーチンは次のようになります。


Sub PickAndPlaySegment
    OurRandomValue = Rand (4)
    If (OurRandomValue = 1) Then
        Action1.play
    ElseIf (OurRandomValue = 2) Then
        Action2.play
    ElseIf (OurRandomValue = 3) Then
        Action3.play
    ElseIf (OurRandomValue = 4) Then
        Action4.play
    End If
End Sub

ここでは、ランダム再生を実装するために、ローカル変数 OurRandomValue を作成し、これに 1~4 の乱数を代入しています。この値に基づいて、4 つのセグメントのいずれかを再生することになります。各セグメントは、毎回 25% の確率で再生されます。次の例のように、乱数ジェネレータの範囲を制御することで、選択確率に重み付けをすることもできます。


Sub PickAndPlaySegment_Weighted
    OurRandomValue = Rand (6)
    If (OurRandomValue = 1) Then
        Action1.play
    ElseIf (OurRandomValue >= 2 and OurRandomValue <= 4) Then
        Action2.play
    ElseIf (OurRandomValue = 5) Then
        Action3.play
    ElseIf (OurRandomValue = 6) Then
        Action4.play
    End If
End Sub

この例では 1~6 の数値を選択しています。ただし、値 2、3、および 4 は Action2 を再生し、その他の 3 つは残りの 3 つのセグメントにそれぞれ対応します。この場合、Action2 は 50% の確率で再生されることになります。

次に、もう少し高度な処理を行います。同じセグメントを 2 度続けて再生するのは避けたいとします。このためには、グローバル変数によって最後にどのバリエーションが再生されたのかを追跡し、次の数値を選択するときに、それが前回と同じ数値でないことを確認します。次の例では、均等な重み付けに戻しています。


Dim VariationPlayed ' Dim は変数をグローバルにします。
 
Sub PickAndPlaySegment_NoRepeats
    LocalValue = Rand (3)
 
    If (LocalValue >= VariationPlayed) Then
        VariationPlayed = LocalValue + 1
    Else
        VariationPlayed = LocalValue
    End If
 
    If (VariationPlayed = 1) Then
        Action1.play
    ElseIf (VariationPlayed = 2) Then
        Action2.play
    ElseIf (VariationPlayed = 3) Then
        Action3.play
    ElseIf (VariationPlayed = 4) Then
        Action4.play
    End If
End Sub

グローバル変数 VariationPlayed は、バリエーションの選択に使われた最後の数値を格納しています。ローカル変数 LocalValue は 1~3 のランダム値を取ります。このランダム値が VariationPlayed よりも小さければ、その値を使って VariationPlayed の古い値と置き換えます。そうでなければ、 VariationPlayed の古い値を 1 だけインクリメントします。VariationPlayed はつねに新しい値を取り、4 を超えることはありません。

Rand が現在再生中のバリエーションの値を選択した場合には、ループを使って乱数ジェネレータに戻ればいいと思う方もいるかもしれません。こうすれば、明らかにコードは理解しやすくなります。しかし、AudioVBScript には、Microsoft Visual Basic(R) の Do WhileFor...Next のようなループ メカニズムがないのです。このようなメカニズムが省略されているのは、スクリプト ルーチンは高速に動作しなくてはならず、ループに入り込んで DirectMusic パフォーマンスに遅れを生じさせるようなことがあってはならないからです (スクリプト ルーチンは同期的です。つまり、呼び出し元のアプリケーションは、ルーチンが終了するのを待ってから、次の処理に進みます)。

ループが必要になるような高度な動作については、少し後で、ルーチンを何度でもトリガできる、スクリプト トラックを含んだセグメントをセットアップする方法について説明します。また、希望する動作を開発者に伝え、それをゲーム コードの中で実装してもらうことも検討してみてください。スクリプトとアプリケーションの間での情報の受け渡しはきわめて簡単に行うことができます。アプリケーションは任意のグローバル変数の取得と設定を行うことができ、スクリプト内で参照されているセグメントやオーディオパス構成などの任意のオブジェクトをりようできます、またセグメントの再生やオーディオパスの実行など、コンテンツ作成者が作成した任意のオブジェクトも利用できます。

グローバル変数を使うときには、注意しなくてはならない点がもう 1 つあります。グローバル変数は最初に初期化しておかなくてはなりません。たとえば、OurRandomValue は、上のルーチンが初めて呼び出された時点では、不定の値を含んでいます。ここでは、アプリケーションに変数を初期化させるか、独自の初期化ルーチンを作成するかの 2 つの選択肢があります。後者の方法がおそらく簡単でしょう。アプリケーションが初めてスクリプトをロードするときに、開発者に次のようなルーチンをトリガしてもらうのです。


Sub InitValues
    OurRandomValue = 0
    ' ここで他のグローバル変数の初期化を行う。
End Sub

自己変更型のミュージック

ここまでで、リストからランダムなセグメントを開始し、同じセグメントが連続して再生されないように最後に再生したセグメントを追跡する機能を実現できました。しかし、われわれのシナリオの後半では、1 つのセグメントの終わりに達したら、別のセグメントを再生するという機能が必要となります。

残念なことに、DirectX オーディオ スクリプティングは通知手段を提供していません。たとえば、あるセグメントが終わりつつあることをパフォーマンスが検知したときに、あるルーチンを自動的にトリガするようなことはできません。しかし、コンテンツの作成にあたっては、このような制限を回避する方法が存在します。

このためにはセグメント トラックを使用します。セグメント トラックをセグメントの中に配置することで、そのセグメントの再生中の特定のタイミングで、1 つまたは複数のスクリプト ルーチンをトリガできます。このトラックにより、自己変更型のミュージックを作成できます。つまり、アプリケーション コードによる操作なしで、自らを変更するミュージックです。

われわれが求めている動作、つまりこのセグメントが終わりに達したときに新しいセグメントにキューを出す簡単な方法の 1 つは、スクリプト トラックの中の各セグメントの最後のバー付近にルーチン呼び出しを置くというものです。セグメントの最後のティックにスクリプト ルーチン呼び出しを追加することも可能ですが、音色のダウンロードや、ウェーブのストリーミングの準備などの操作のために時間的な余裕を持たせておいた方がいいでしょう。クロック タイム セグメントでは、リード タイムとしては2-3秒で十分だと思われます。

Dd188498.dmp_script_intermediate_fig1(ja-jp,MSDN.10).gif

図 1. スクリプト トラックから、セグメントの終わり近くでトリガする

もちろん、新しいセグメントにキューを出すには、AtFinish フラグを使用します。そうしないと、現在のセグメントが終了する前に割り込みが生じます。次の例は、PickAndPlaySegment_NoRepeats ルーチンの中の、セグメントの切り替えを行っている箇所を示しています。


If (OurRandomValue = 1) then
    Action1.play AtFinish
ElseIf (OurRandomValue = 2) then
    Action2.play AtFinish

これで準備が完了しました。開発者は InitValues ルーチンを、続けて PickAndPlaySegment を呼び出すだけで、その後のセグメントの選択 (および連続再生) はコンテンツ自身が決定することになります。もちろん、ミュージックを停止するためには、別のルーチンを用意しなくてはなりません。ただしこれは、DirectMusic パフォーマンスや、スクリプトが参照しているすべてのオブジェクトに自由にアクセスできるアプリケーションに任せてしまうことが可能です。

自己ポーリング型のミュージック

スクリプティングの典型的な用途として、アプリケーション開発者に対し、ミュージックまたは環境音のインテンシティを抽象化して提供するというものがあります。アプリケーションは、トランジションやグルーブ レベルなどに注意を払わずに、ゲーム内での出来事に基づいて上下に調整できる「ノブ」のようなものを利用できるようになります。

ここでもやはり、AudioVBScript に通知手段が組み込まれてないため、スクリプトは変数の値が変化したことを自動的に検知できないという問題があります。スクリプトに対して変更点をアラートする方法は 2 つあります。

  • 変数の値を変更した後で、開発者に特定のスクリプト ルーチンを呼び出してもらう。このルーチンの中では、変更された変数をチェックし、必要に応じて対応を行うことになります。もちろん、この方法では余分な呼び出しが必要となり、開発者が 1 箇所で呼び出しを忘れると、ミュージックは正常に反応せず、さらに悪ければ、次に反応すべきときに混乱してしまう可能性があります。
  • アプリケーションが変数の値を変更したかどうかを定期的にチェックまたはポーリングし、変更されていた場合には適切に反応するコンテンツを作成する。

ここでは、開発者にとっての負担が少ない後者のオプションに焦点を当てることにします。これには 2 つのステップが含まれます。どの変数をチェックし、それが変更されていた場合には何をすべきかを指示するルーチンの作成と、ポーリング ルーチンを定期的に呼び出すループ セグメントの開始です。

ここでも、前述のサンプル シナリオを使用します。開発者は、1 (平穏)から 3 (狂乱) までの範囲を持つ Intensity という名前の変数を通して、ミュージックのインテンシティをいつ変更すべきかをコンテンツ作成者に指示するものとします。話を単純にするために、個々のインテンシティに合わせて、別々のミュージックを用意することにします (Music1、Music2、および Music3)。下のレベルへのトランジションは次のメジャーで行われ、上のレベルへのトランジションは次のビートで行われることにしましょう。

まず、現在再生中のミュージックを変更する必要があるかどうかを確認するルーチンを作成します。現在再生中の音楽のインテンシティを格納するためのものを 1 つ、アプリケーションが設定するためのものを 1 つ、合わせて 2 つのグローバル変数を宣言します。新しいミュージックに対するキューは、これらの値が異なる場合にのみ発行されます。


Dim LastPlayedIntensity  ' 現在再生中のインテンシティ。
Dim Intensity            ' アプリケーションが変更できる値。
 
Sub CheckIntensity
    If (Intensity <> LastPlayedIntensity) Then
    ' インテンシティが変更された。
        If  (Intensity = 1) Then
            Music1.Play AtMeasure
        ElseIf (Intensity = 2) Then
            Music2.Play AtMeasure
        ElseIf (Intensity = 3) Then
            Music3.Play AtBeat
        End If
        LastPlayedIntensity = Intensity
    End If
End Sub

次のステップでは、このルーチンを定期的にトリガするセグメントを作成します。通常一番簡単なのは、セグメントを無限ループするセカンダリ セグメントにするという方法です。セカンダリ セグメントにするのは、プライマリ コンテンツの上にレイヤ化され、プライマリ コンテンツとの間で干渉が生じないからです。

Dd188498.dmp_script_intermediate_fig2(ja-jp,MSDN.10).gif

図 2. ポーリング セグメントとその [プロパティ] ウィンドウ

このループ ポイントでは、スクリプト ルーチンは個々のグリッドで、つまり 1 ビートごとにトリガされます。

これで、ループを行い、インテンシティの変化をポーリングによって確認するセグメントができました。あとは、スクリプトの実行準備ができたときに、このセグメントの実行を開始する必要があります。この種の動作は、スクリプトのロード時にミュージックを開始したい場合には、初期化ルーチンの中に置くのが適しています。次にスクリプト全体を示します。


Dim LastPlayedIntensity
Dim Intensity 
 
Sub InitPlayback
    LastPlayedIntensity = 0
    PollingSeg.Play IsSecondary
End Sub
 
Sub CheckIntensity
    If (Intensity <> LastPlayedIntensity) Then
        If  (Intensity = 1) Then
            Music1.Play AtMeasure
        ElseIf (Intensity = 2) Then
            Music2.Play AtMeasure
        ElseIf (Intensity = 3) Then
            Music3.Play AtBeat
        End If
        LastPlayedIntensity = Intensity
    End If
End Sub

開発者に対しては、スクリプトをロードし、 Intensity の初期値を設定した後に、InitPlayback ルーチンを呼び出すように依頼します。これでミュージックの再生が開始されます。それ以降は、開発者はゲームの状況に応じて Intensity の値を変更するだけです。われわれのポーリング セグメントとルーチンは、これらの変更に合わせてミュージックを適切に変更します。

Baseball サンプル スクリプト

これまでのシナリオを確認したら、DirectX Software Development Kit (SDK) に収録されている AudioScripts サンプル アプリケーションもチェックしてみてください。このサンプルのコンテンツは、これまでに説明した概念を使って、ベースボール ゲームのためのサウンドスケープを作成しています。群衆は、スコアの変化に基づいて歓声を上げたりブーイングを発したりします。このコンテンツは、スクリプト オーサリング一般の例としても優れています。DirectMusic Producer で Baseball.spt を開き、スクリプト ルーチンと、それを構成している埋め込みコンテンツを確認してください。

関連情報

DirectX オーディオ スクリプトの作成の詳細については、DirectMusic Producer のヘルプを参照してください。

アプリケーション内でのスクリプトの使用方法の詳細については、DirectX SDK のヘルプを参照してください。

このシリーズの以下の記事では、DirectMusic Producer の使い方に関する詳しい情報を提供しています。