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

はじめに

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

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

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

ウェブでのゲーム音声

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

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

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

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

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

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

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

たとえば、プレイヤーが壮大なボス戦のゾーンにいる場合は、雰囲気から伏線、激しいものまで、感情の幅に応じて複数のミックスを用意できます。音楽合成ソフトウェアでは、エクスポートで使用するトラックセットを選択することで、楽曲に基づいて(同じ長さの)複数のミックスをエクスポートできます。これにより、内部の整合性が保たれ、トラック間でのクロスフェード時に不自然な遷移を回避できます。

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 コンテキストには 1 つの 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 の仕様ページにあるルーム エフェクトのデモと、素晴らしいジャズ標準曲のドライ(未加工)とウェット(コンボリュータで処理)のミキシングを制御できるこの例もご覧ください。

最後のカウントダウン

ゲームを作成して位置オーディオを構成し、グラフに多数の 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 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 を使用したゲーム サウンド開発で最も重要な要素です。これらの手法を使用すると、ブラウザ内で魅力的な音声エクスペリエンスを構築できます。最後に、ブラウザ固有のヒントをご紹介します。page visibility API を使用してタブがバックグラウンドに移動する場合は、必ず音声を一時停止してください。そうしないと、ユーザーの不満を招く可能性があります。

Web Audio の詳細については、スタートガイドの記事をご覧ください。質問がある場合は、Web Audio に関するよくある質問で回答がないかご確認ください。他にご不明な点がある場合は、web-audio タグを使用して Stack Overflow でお問い合わせください。

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