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

はじめに

マルチメディア エクスペリエンスを魅力的なものにしているのは、オーディオの大きな部分です。音声をオフにして映画を視聴したことがあれば、このことに気づいたことがあるでしょう。

ゲームも例外ではありません。ビデオゲームの一番の思い出は音楽と効果音です私のお気に入りのゲームをプレイしてから 20 年近く経った今でも、Koji Kondo のゼルダの楽曲Matt Uelmen の雰囲気のあるDiablo のサウンドトラックが頭から離れません。同じキャッチーさは、Warcraft のユニットのクリック音や、任天堂のクラシック ゲームのサンプルなど、効果音にも当てはまります。

ゲーム音声には、興味深い課題がいくつかあります。説得力のあるゲーム音楽を作成するには、プレーヤーが直面する予測不可能なゲーム状態に調整する必要があります。実際には、ゲームの一部は不明な時間続く可能性があり、音は環境と相互作用し、部屋の効果や相対的な音の配置など、複雑な方法で混ざり合うことがあります。最後に、一度に再生されるサウンドの数が多く、それらすべてが一緒に良い音を鳴らし、パフォーマンスの低下を招くことなくレンダリングする必要があります。

ウェブでのゲーム音声

シンプルなゲームの場合は、<audio> タグを使用して十分な場合もあります。ただし、多くのブラウザでは実装が不十分なため、音声のグリッチやレイテンシの増加が発生します。ベンダーはそれぞれの実装の改善に取り組んでいるため、これは一時的な問題であると期待されます。<audio> タグの状態を確認するには、areweplayingyet.org にある優れたテストスイートをご覧ください。

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

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

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

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

ゲームでは、バックグラウンド ミュージックがループ再生されることが多いため、

ループが短く、予測可能な場合は、非常に煩わしいものになります。プレーヤーが特定のエリアやレベルから先に進めず、同じサンプルがバックグラウンドで連続して再生されている場合は、プレーヤーの不満を増やさないために、トラックを徐々にフェードアウトすることをおすすめします。もう 1 つの戦略は、ゲームのコンテキストに応じて、さまざまな強度をミックスし、徐々に互いにクロスフェードすることです。

たとえば、プレーヤーが壮大なボスバトルが繰り広げられるゾーンにいる場合、雰囲気が盛り上がるものから前景や強烈な印象まで、さまざまな組み合わせが考えられます。音楽合成ソフトウェアでは、エクスポートで使用するトラックセットを選択することで、楽曲に基づいて(同じ長さの)複数のミックスをエクスポートできます。これにより、内部の整合性が保たれ、トラック間でのクロスフェード時に不自然な遷移を回避できます。

Garageband

次に、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> タグから Web 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 と統合する方法について詳しくは、こちらの短い記事をご覧ください。

効果音

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

ゲームの効果音のもう 1 つの重要な特徴は、同時に複数の効果音を鳴らせることです。複数の俳優が機関銃を撃ち合う銃撃戦の真っ最中にいる状況を想像してみてください。各機関銃は 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();
    }
};

Web Audio による空間化の処理に関する注意事項:

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

上級者向け: 音の円錐

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

位置モデル

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

距離モデルでは、ソースまでの距離に応じてゲインの量を指定しますが、指向性モデルでは、内側と外側の円錐を指定することで、リスナーが内側の円錐内、内側と外側の円錐の間、または外側の円錐の外側にいる場合のゲインの量(通常は負)を決定できます。

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

この例は 2D ですが、このモデルは 3 番目のディメンションに簡単に一般化できます。3D で空間化されたサウンドの例については、こちらのポジショナル サンプルをご覧ください。Web Audio サウンドモデルには、位置情報に加えて、ドップラー シフトの速度もオプションで含まれます。この例では、ドップラー効果について詳しく説明します。

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

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

実際には、音が聞こえる部屋によって、音がどのように聞こえるかは大きく異なります。同じきしむドアでも、地下室では、広々としたホールとまったく音が異なります。制作価値の高いゲームでは、環境ごとに個別のサンプルセットを作成するのは費用がかかりすぎるため、このような効果を模倣する必要があります。また、アセットが増え、ゲームデータの量も増加します。

大まかに言えば、元の音と実際の音の違いをオーディオ用語で表すと、インパルス レスポンスです。これらのインパルス レスポンスを慎重に録音することもできますが、事前に録音されたインパルス レスポンス ファイル(音声として保存)を多数ホストしているサイトもあります。

特定の環境からインパルス レスポンスを作成する方法について詳しくは、Web Audio API 仕様の畳み込み部分の「録音の設定」セクションをご覧ください。

重要なのは、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 を使いすぎないように注意してください。この場合、メーターリングの代替実装では、requestAnimationFrame によって決定されたように、レンダリング時に音声グラフの RealtimeAnalyserNodegetByteFrequencyData をポーリングできます。このアプローチはより効率的ですが、レンダリングは 1 秒あたり最大 60 回しか行われないのに対し、オーディオ信号ははるかに速く変化するため、信号のほとんど(クリップが発生する可能性のある部分を含む)が失われます。

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

クリッピングを防止する

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

砂糖を少し加える

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

ダイナミクス コンプレッションは、一般的に使用することをおすすめします。特に、前述のように、どのような音がいつ再生されるかわからないゲーム環境では、ダイナミクス コンプレッションが有効です。DinahMoe ラボの 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 を使用したゲーム音声開発において最も重要な側面であると考えています。これらの手法を使用すると、ブラウザ内で魅力的な音声エクスペリエンスを構築できます。最後に、ブラウザ固有のヒントをご紹介します。page visibility API を使用してタブがバックグラウンドに移動する場合は、必ず音声を一時停止してください。そうしないと、ユーザーの不満を招く可能性があります。

ウェブ オーディオについて詳しくは、より入門的なスタートガイドの記事をご覧ください。ご質問がある場合は、ウェブ オーディオに関するよくある質問で、回答がすでに記載されていないかご確認ください。 最後に、ご不明な点がございましたら、Stack Overflowweb-audio タグを使用してお問い合わせください。

最後に、実際のゲームで Web Audio API がどのように使用されているか、いくつかご紹介します。