Web Audio API を使用したゲーム用オーディオの開発

はじめに

オーディオは、マルチメディア エクスペリエンスの魅力の大部分を占めています。音声なしで映画を観たことがある方なら お気づきでしょう

ゲームも例外ではありません。ビデオゲームの一番の思い出は音楽と効果音です。お気に入りの曲をプレイしてから 20 年近く経った今でも、近藤公司 のゼルダの楽曲Matt Uelmen の雰囲気が漂うディアブロ サウンドトラックが頭から離れないことがよくあります。ウォークラフトのユニット クリック レスポンスをすぐに認識できる、任天堂のクラシック作品のサンプルなど、サウンド エフェクトにも同じキャッチネスが適用されます。

ゲーム オーディオには興味深い課題がいくつかあります。説得力のあるゲームミュージックを作成するには、デザイナーは、プレーヤーがどのようなゲーム状態になるかは予測しづらい状況に合わせて調整する必要があります。実際には、ゲームの一部は継続時間が不明で、サウンドは環境と相互作用し、ルーム エフェクトや相対的なサウンド ポジショニングなどの複雑な方法でミキシングされます。最後に、多数のサウンドが同時に再生されることがあります。これらはすべて、パフォーマンスを損なうことなく、サウンドがうまく調和してレンダリングされる必要があります。

ウェブ版ゲーム音声

単純なゲームの場合は、<audio> タグで十分です。しかし、多くのブラウザは実装が不十分であるため、音声の乱れやレイテンシの増加につながります。ベンダーはそれぞれの実装の改善に取り組んでいるため、これはおそらく一時的な問題です。<audio> タグの状態は、areweplayingyet.org に便利なテストスイートが掲載されています。

しかし、<audio> タグの仕様を詳しく見てみると、単純にできないことがたくさんあることがわかります。これはメディア再生用に設計されているため、驚くことではありません。次のような制限事項があります。

  • 音声信号にフィルタを適用できません
  • 元の PCM データにアクセスする方法がない
  • ソースとリスナーの位置と方向の概念がない
  • 詳細なタイミングはありません。

この記事の残りの部分では、Web Audio API で記述されたゲーム音声のコンテキストについて、これらのトピックの一部を詳しく見ていきます。この API の簡単な概要については、スタートガイド チュートリアルをご覧ください。

バックグラウンド ミュージック

ゲームでは、バックグラウンド ミュージックがループ再生されることがよくあります。

ループが短く、予測可能な場合は、非常に煩わしいものになります。プレーヤーが特定のエリアやレベルに行き詰まり、同じサンプルがバックグラウンドで継続的に再生されている場合は、トラックを徐々にフェードアウトしてフラストレーションを感じないようにすることをおすすめします。もう 1 つの戦略は、ゲームのコンテキストに応じて、段階的に交差するさまざまな強度を組み合わせることです。

たとえば、プレーヤーが壮大なボス戦が開催されるゾーンにいる場合は、雰囲気が異なるものから予兆的、激しいものまで、感情の範囲がさまざまに異なる複数のミックスが考えられます。音楽合成ソフトウェアでは、多くの場合、エクスポートで使用するトラックのセットを選択することで、同じ長さの複数のミックスをエクスポートできます。これにより、ある程度の内部整合性を確保し、トラック間でクロスフェードする際の不快な遷移を避けることができます。

ガレージバンド

その後、Web Audio API を使用して、XHR を介して BufferLoader クラスなどを使用して、これらのサンプルをすべてインポートできます(詳細については、Web Audio API の概要に関する記事をご覧ください)。サウンドの読み込みには時間がかかるため、ゲームで使用されるアセットは、ページの読み込み時、レベルの冒頭、またはプレーヤーのプレイ中に段階的に読み込む必要があります。

次に、各ノードのソースと各ソースのゲインノードを作成し、グラフを接続します。

これにより、これらのソースをすべて同時にループ再生できるようになります。また、ソースの長さはすべて同じであるため、Web Audio API によってアライメントが維持されます。キャラクターが最後のボスバトルから遠ざかるにつれて、ゲームは次のようなゲイン量アルゴリズムを使用して、チェーン内の各ノードのゲイン値を変更できます。

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

上記のアプローチでは、2 つのソースが同時に再生され、(はじめにで説明したように)同じ電力曲線を使用してソース間でクロスフェードします。

昨今の多くのゲーム デベロッパーは、バックグラウンド ミュージックに <audio> タグがストリーミング コンテンツに適していることから使用しています。これで、<audio> タグのコンテンツをウェブ オーディオのコンテキストに取り込めるようになりました。

<audio> タグはストリーミング コンテンツで機能するため、すべてのダウンロードを待たずにバックグラウンド ミュージックをすぐに再生できるため、この手法は便利です。ストリームを Web Audio API に取り込むことで、ストリームを操作または分析できます。次の例では、<audio> タグを通じて再生される音楽にローパス フィルタを適用しています。

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

<audio> タグを Web Audio API と統合する方法については、こちらの短い記事をご覧ください。

効果音

ゲームでは多くの場合、ユーザー入力やゲーム ステータスの変化に応じて効果音が再生されます。ただし、バックグラウンド ミュージックと同様に、効果音もすぐにイライラします。これを回避するには、多くの場合、再生するサウンドの類似しているが、異なるプールを用意すると便利です。これは、Warcraft シリーズでユニットのクリックに応じて見られるような、足のステップのサンプルの軽度なバリエーションから大幅なバリエーションまで、さまざまです。

ゲーム内の効果音のもう一つの重要な特長は、同時に多くの効果を適用できることです。銃撃戦の最中に複数の俳優が機関銃を撃つと想像してください。各マシンガンが 1 秒間に何度も発射されるため、同時に数十個の効果音が再生されます。Web Audio API の真価を発揮するのは、複数の正確なタイミングをもつ複数の音源のサウンドを同時に再生できる点です。

次の例では、再生が時間的にずらされる複数の音源を作成することで、複数の個別の箇条書きサンプルからマシンガンのラウンドを作成します。

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

すべてのマシンガンの音がこんな感じだったら、なかなか退屈です。もちろん、ターゲットからの距離と相対位置(これについては後述します)によってサウンドによって異なりますが、それだけでは不十分な場合があります。Web Audio API を使用すると、次の 2 つの方法で上記の例を簡単に調整できます。

  1. 弾が発射される間隔がわずかにずれる
  2. 現実世界のランダム性をより適切にシミュレートするために、各サンプルの PlaybackRate を変更する(またピッチも変更する)。

これらの手法の実際の例については、プールテーブルのデモをご覧ください。このデモでは、ランダム サンプリングを使用し、PlaybackRate を変化させて、より興味深いボール衝突音を作成します。

3D ポジショナル サウンド

ゲームは多くの場合、2D または 3D の幾何学的特性を持つワールドに設定されます。その場合は、ステレオ ポジショニング オーディオによって臨場感が大幅に向上します。幸いなことに、Web Audio API にはハードウェア アクセラレーションによるポジショナル オーディオ機能が組み込まれており、簡単に使用できます。ちなみに、次の例にはステレオ スピーカー(できればヘッドフォン)が必要です。

上記の例では、キャンバスの中央にリスナー(人アイコン)があり、マウスがソース(スピーカー アイコン)の位置に影響を与えています。上記は、AudioPannerNode を使用してこのような効果を実現する簡単な例です。上記のサンプルの基本的な考え方は、音源の位置を次のように設定することで、マウスの動きに対応することです。

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

ウェブ オーディオでの空間化の取り扱いに関する注意事項:

  • デフォルトでは、リスナーは原点(0, 0, 0)にあります。
  • Web Audio Positional API は単位がないため、デモの音声を改善するために乗数を導入しました。
  • ウェブ オーディオは y 座標のデカルト座標を使用します(ほとんどのコンピュータ グラフィック システムとは逆です)。そのため上記のスニペットの y 軸を入れ替えていますが

上級: 音声コーン

位置モデルは、主に OpenAL に基づく非常に強力で高度です。詳しくは、上記のリンク先の仕様のセクション 3 と 4 をご覧ください。

接点モデル

Web Audio API のコンテキストにアタッチされた単一の AudioListener は、位置と向きを使用して空間内で設定できます。各ソースは、入力オーディオを空間化する AudioPannerNode を介して渡すことができます。パナーノードには、位置と向きに加え、距離と方向のモデルがあります。

距離モデルはソースへの近さに応じてゲイン量を指定します。一方、方向モデルは、内側と外側のコーンを指定して構成できます。これにより、リスナーが内側コーン内、内側コーンと外側コーンの間、または外側コーンの外側にある場合に、(通常は負の)ゲイン量を決定します。

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

この例は 2 次元ですが、このモデルは 3 次元に簡単に一般化できます。3D で空間化された音声の例については、こちらの位置サンプルをご覧ください。ウェブオーディオのサウンドモデルには、位置に加えて、ドップラー シフトの速度も必要に応じて含まれます。この例は、ドップラー効果をより詳細に示しています。

このトピックの詳細については、[位置オーディオと WebGL の混合][webgl]に関する詳細なチュートリアルをご覧ください。

部屋のエフェクトとフィルタ

実際には、音が聞こえる方法は、その音が聞こえる部屋によって大きく異なります。同じきしむ音がするドアでも、地下室と大きなオープンホールでは音が異なります。プロダクション バリューが高いゲームでは、このような影響を真似る必要があります。これは、環境ごとに個別のサンプルセットを作成するのは法外なコストがかかり、アセットの数とゲームデータの量が増えることになるためです。

大まかに言うと、生の音と実際の音の違いを表す音声用語は、インパルス応答と呼ばれます。こうしたインパルス応答は入念に録音できますが、実際には、便宜上、事前に録音されたインパルス応答ファイル(音声として保存されています)の多くをホストしているサイトがあります。

特定の環境からインパルス応答を作成する方法について詳しくは、Web Audio API 仕様の畳み込みに関するセクションの「Recording Setup」をご覧ください。

ここでの目的上さらに重要な点として、Web Audio API では、ConvolverNode を使用してこれらのインパルス応答を音声に簡単に適用できます。

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

また、Web Audio API の仕様ページのルーム エフェクトのデモや、優れた Jazz 基準のドライ(生)とウェット(コンボルバーによる処理)のミキシングを制御できるこちらの例もご覧ください。

最後のカウントダウン

これで、ゲームを作成し、位置オーディオを構成できました。これにより、グラフに多数の AudioNode が用意され、すべてが同時に再生されます。それでは、留意すべき点がもう 1 つあります。

複数のサウンドが正規化されずに積み重ねられてしまうため、スピーカーの能力のしきい値を超える場合があります。キャンバス境界を超える画像と同様に、波形が最大しきい値を超えるとサウンドがクリップされ、明らかな歪みが生じることがあります。波形は次のようになります。

クリッピング

クリッピングの実際の例を見てみましょう。波形に問題があります。

クリッピング

上記のような激しいディストーションや、逆にリスナーに音量を上げさせる過度に控えめなミックスを聴くことが重要です。そのような状況にあるなら、本当に修正する必要があります。

クリッピングを検出する

技術的な観点からは、いずれかのチャネルの信号の値が有効な範囲(-1 ~ 1)を超えると、クリッピングが発生します。これが検出されたら、それが行われていることを視覚的にフィードバックすると便利です。これを確実に行うには、JavaScriptAudioNode をグラフに配置します。この場合、オーディオ グラフは次のように設定されます。

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

また、次の processAudio ハンドラでクリッピングを検出できます。

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

一般に、パフォーマンス上の理由で JavaScriptAudioNode を過剰に使用しないように注意してください。この場合、別のメータリングの実装で、レンダリング時に getByteFrequencyData のオーディオ グラフ内の RealtimeAnalyserNode をポーリングできます(これは requestAnimationFrame によって決まります)。このアプローチはより効率的ですが、レンダリングは最大 1 秒間に 60 回行われるのに対し、オーディオ信号の変化ははるかに速くなるため、信号の大部分(クリップされる可能性のある場所を含む)が失われます。

クリップ検出は非常に重要であるため、今後、組み込みの MeterNode Web Audio API ノードが導入される可能性があります。

クリッピングの防止

マスター AudioGainNode でゲインを調整することで、クリッピングを防止できるレベルにミックスを抑制できます。ただし、実際には、ゲームで再生されるサウンドはさまざまな要因に左右されるため、すべての状態でクリッピングを防ぐマスター ゲイン値を決定することは困難です。一般的には、最悪の事態に備えるために利益の調整を行う必要がありますが、これは科学というよりも高度な技術です。

砂糖を少し加える

コンプレッサーは音楽やゲームの制作で、信号を平滑化して信号全体の急増を制御するためによく使用されます。この機能は、DynamicsCompressorNode を介してウェブ オーディオで利用できます。この DynamicsCompressorNode をオーディオ グラフに挿入して、より大きく、豊かで豊かなサウンドを実現したり、クリッピングしたりすることもできます。仕様を直接引用する、このノード

一般的には、ダイナミクス圧縮の使用が推奨されます。特に、前述のように、どのサウンドがいつ再生されるか正確にわからないゲーム設定ではなおさらです。DinahMoe Labs の Plink はその良い例です。再生されるサウンドはあなたと他の参加者によって完全に依存します。コンプレッサーはほとんどのケースで役立ちますが、「ぴったり」なサウンドに調整された、骨の折れるマスターされたトラックを扱うまれなケースを除きます。

これを実装するには、DynamicsCompressorNode をオーディオ グラフに含めるだけでよく、通常はデスティネーションの前の最後のノードになります。

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

ダイナミクス圧縮について詳しくは、Wikipedia のこちらの記事をご覧ください。

まとめると、クリッピングに注意しながら、マスター ゲイン ノードを挿入してクリッピングを回避します。次に ダイナミクス コンプレッサー ノードを使用してミックス全体を締めくくります。オーディオ グラフは次のようになります。

最終結果

おわりに

ここでは、Web Audio API を使用したゲーム用オーディオ開発で最も重要な側面について説明します。これらの手法により、真に魅力的なオーディオ エクスペリエンスをブラウザで構築できます。終了する前に、ブラウザ固有のヒントをお伝えしておきます。ページ可視性 API を使用してタブがバックグラウンドに移動する場合は、必ず音声を一時停止してください。そうしないと、ユーザーに不快感を与える可能性があります。

ウェブ オーディオについて詳しくは、入門レベルのスタートガイドをご覧ください。また、質問がある場合は、ウェブオーディオに関するよくある質問で回答済みかどうかをご確認ください。最後に、不明な点がある場合は、Stack Overflowweb-audio タグを使用して質問します。

最後に、実際のゲームでの Web Audio API の優れた使い方をいくつか紹介します。