ケーススタディ - The Sounds of Racer

はじめに

Racer は、マルチプレーヤー型、マルチデバイス用の Chrome 試験運用版です。さまざまな画面でプレイされるレトロスタイルのスロットカー ゲーム。スマートフォン、タブレット、Android または iOS。誰でも参加できます。アプリはありません。ダウンロードはありません。モバイルウェブのみ。

Plan814islands の仲間たちとともに、Giorgio Moroder のオリジナル楽曲を基に、ダイナミックな音楽とサウンド体験を生み出しました。Racer は、応答性の高いエンジン サウンドやレース用サウンド エフェクトを備えていますが、さらに重要な点として、ランナーが参加するたびに複数のデバイスに広がるダイナミックな音楽ミックスが提供されます。スマートフォンで構成されるマルチスピーカー インストールです。

複数のデバイスを接続できることは、以前からよく考えられていました。さまざまなデバイスで音が分割されたり、デバイス間で音が飛んだりするような音楽の実験を行っていたため、そのアイデアを Racer にも応用したいと考えました。

具体的には、ドラムとベース、ギターやシンセの追加など、ゲームに参加する人が増えれば、それに合わせてデバイス間で音楽トラックを構築できるかどうかをテストしたいと考えました。音楽のデモを行い、コーディングを学びました。マルチスピーカー効果は本当にやりがいのあるものでした。この時点ではすべての同期がうまくいかなかったのですが、デバイス全体に音のレイヤが広がるのを聞いて、何か良いものがあるはずだとわかりました。

サウンドの作成

Google Creative Lab は、サウンドと音楽のクリエイティブな方向性を概説した。本物のサウンドを録音したりサウンド ライブラリに頼ったりするのではなく、アナログ シンセサイザーを使用して効果音を作りたいと考えました。また、ほとんどの場合、出力スピーカーは小さなスマートフォンやタブレットのスピーカーであることもわかっていたため、スピーカーが歪まないように音の周波数スペクトルを制限する必要があります。これは非常に難しい課題であることが判明しました。Giorgio から最初のドラフト版を受け取ったとき、彼の作曲は私たちが作ったサウンドと完璧に合っていたので、安心感がありました。

エンジンの音

サウンドのプログラミングで最大の課題は、最適なエンジン サウンドを見つけてその動作を表現することでした。レーストラックは F1 や Nascar のサーキットに似ているため、スピード感が増し、爆発的な気分を味わう必要がありました。その一方で、車は非常に小型だったため、エンジンの大きな音ではサウンドと映像を結びつけることはできませんでした。とにかく、大きな音を立てるエンジンがモバイル スピーカーで流れるなんて考えられなかったので、別のことを考えなければなりませんでした。

アイデアを得るため、友人の Jon Ekstrand によるモジュラー シンセのコレクションをいくつか接続して、いろいろ試してみました。良かったです。2 つのオシレーター、便利なフィルターと LFO を使った場合、このような感じです。

アナログ機器は以前、Web Audio API を使って大いに再構築されました。そのため、私たちは大きな期待を持って、ウェブ オーディオでシンプルなシンセの開発を始めました。生成された音は最も反応が早いですが、デバイスの処理能力に負担がかかります。ビジュアルをスムーズに配信するには、可能な限りリソースを節約し、多大な労力を費やす必要がありました。そのため、音声サンプルを再生する手法に切り替えました。

モジュラー シンセでエンジン音をインスピレーション

サンプルからエンジンの音を出すために使用できる手法はいくつかあります。コンソール ゲームで最も一般的なアプローチは、エンジンの複数のサウンドのレイヤ(負荷あり)を異なる RPM で作成し、クロスフェードとクロスピッチを行うことです。次に、同じ RPM で(負荷なしで)回転するだけのエンジンの複数のサウンドのレイヤを追加し、クロスフェードとクロスピッチを行います。ギアをシフトするときにこれらのレイヤ間のクロスフェードを適切に行うと、非常にリアルに聞こえますが、大量のサウンド ファイルがある場合に限られます。クロスピッチの幅は広すぎず、合成音がよくなります。長い読み込み時間を避ける必要があるため、この方法は適していませんでした。レイヤごとに 5、6 個のサウンド ファイルを使ってみましたが、期待どおりのサウンドでした。そこで、ファイルの数を減らす方法を見つける必要がありました。

最も効果的なソリューションは次のとおりです。

  • 加速とギアシフトを含む 1 つのサウンド ファイルは、自動車の視覚的な加速度と同期して、最高ピッチ / RPM でプログラムされたループで終わります。Web Audio API は正確なループ処理に長けているため、不具合やポップ音も発生しません。
  • 減速 / エンジンの回転を下げたサウンド ファイル 1 個。
  • 最後に、静止している / アイドル状態の音をループ再生する 1 つのサウンド ファイルです。

こちらのようです

エンジン音のグラフィック

最初のタップイベント / アクセラレーションでは、最初のファイルを最初から再生し、プレーヤーがスロットルを離した場合、スロットルが再びオンになったときに、2 番目の(レブダウン)ファイルが再生された後、アクセラレーション ファイルの正しい位置にジャンプするように、リリース時のサウンド ファイルの状態から時間を計算します。

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

試してみる

エンジンを始動して、「スロットル」ボタンを押します。

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

そこで、3 つの小さなサウンド ファイルと優れたサウンド エンジンのみを使用して、次の課題に移ることにしました。

同期を取得しています

Google は 14islands の David Lindkvist 氏と協力して、デバイスを完璧に同期させて再生する方法について、より深く検討し始めました。基本的な理論はシンプルです。デバイスはサーバーに時刻を要求し、ネットワーク レイテンシを考慮してローカル クロック オフセットを計算します。

syncOffset = localTime - serverTime - networkLatency

このオフセットにより、接続されている各デバイスは同じ時間の概念を共有します。単に、(理論上はこれと同様です)。

ネットワーク レイテンシの計算

レイテンシは、サーバーからのレスポンスをリクエストして受信するまでにかかる時間の半分であると仮定します。

networkLatency = (receivedTime - sentTime) × 0.5

この前提の問題は、サーバーへのラウンド トリップが常に対称的とは限らないことです。つまり、リクエストがレスポンスよりも時間がかかる場合があり、その逆も同様です。ネットワークのレイテンシが大きいほど、この非対称性の影響が大きくなり、音声が遅延して他のデバイスと同期しなくなる原因となります。

幸い、私たちの脳は、音がわずかに遅れても気づかないようになっています。研究によると、脳が音を別の音として認識するまでに 20 ~ 30 ミリ秒(ms)の遅延があることがわかっています。ただし、12 ~ 15 ミリ秒程度は、たとえ信号を完全に「認識」できなくても、信号の遅延を「感じ」始めます。私たちは、確立されているいくつかの時刻同期プロトコルと、より簡単な代替プロトコルを調査し、そのいくつかを実際に実装してみました。最終的には、Google の低レイテンシ インフラストラクチャのおかげで、大量のリクエストを簡単にサンプリングし、レイテンシが最も低いサンプルを参考として使用することができました。

時計のずれと戦う

うまくいった!5 台以上のデバイスが完璧に同期してパルスを再生していましたが、これは一瞬です。高精度の Web Audio API のコンテキスト時間を使用して音声のスケジュールを設定していても、数分間再生するとデバイスがずれていました。この遅延は一度に数ミリ秒ずつゆっくりと蓄積され、最初は検出できませんでしたが、長時間再生すると音楽レイヤが完全に同期しなくなりました。こんにちは、時計がずれています。

解決策は、数秒ごとに再同期し、新しいクロック オフセットを計算して、これをオーディオ スケジューラにシームレスに供給することでした。ネットワーク ラグによって音楽が大きく変化するリスクを軽減するため、最新の同期オフセットの履歴を保持して平均値を計算することで、変化を平滑化することにしました。

曲のスケジュール設定と入れ替え

インタラクティブなサウンド エクスペリエンスを実現すると、現在の状態はユーザーの操作に応じて変わるため、曲の一部を再生するタイミングを自分で制御できなくなります。曲のアレンジを適切なタイミングで切り替えられるようにする必要がありました。つまり、次のアレンジに切り替える前に、現在再生中の小節がどれだけ残っているかをスケジューラが計算できる必要がありました。 作成したアルゴリズムは次のようになります。

  • Client(1): 曲を開始します。
  • Client(n) は、最初のクライアントに曲がいつ開始されたかを尋ねます。
  • Client(n) は、ウェブ オーディオ コンテキストを使用して曲が開始されたときの基準点を計算します。その際、syncOffset と、オーディオ コンテキストが作成されてからの経過時間が考慮されます。
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) は、playDelta を使用して曲の実行時間を計算します。曲スケジューラはこの情報を使用して、現在の配置のどの小節を次に演奏するかを判断します。
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

念のため、アレンジは常に 8 小節、同じテンポ(1 分あたりの拍数)となるよう制限しました。

先を見据えて

JavaScript で setTimeout または setInterval を使用する場合は、常に前もってスケジュールすることが重要です。これは、JavaScript のクロックの精度があまり高くなく、スケジュール設定されたコールバックが、レイアウト、レンダリング、ガベージ コレクション、XMLHTTPRequest によって数十ミリ秒以上簡単にずれてしまうためです。今回のケースでは、すべてのクライアントがネットワークで同じイベントを受信するのにかかる時間も考慮する必要がありました。

オーディオ スプライト

複数の音声を 1 つのファイルにまとめると、HTML オーディオでも Web Audio API でも HTTP リクエストを減らすことができます。再生前に新しいオーディオ オブジェクトを読み込む必要がないため、オーディオ オブジェクトを使用してレスポンシブにサウンドを再生するのにも最適です。出発点として使用した優れた実装例がすでにいくつかあります。このスプライトを拡張して、iOS と Android の両方で確実に動作し、デバイスがスリープ状態になる奇妙なケースにも対応できるようにしました。

Android では、デバイスをスリープモードにしても、オーディオ要素の再生が継続します。スリープモードでは、バッテリーを節約するために JavaScript の実行が制限され、コールバックの開始を requestAnimationFramesetIntervalsetTimeout に依存することはできません。オーディオ スプライトは JavaScript を使用して再生を停止する必要があるかどうかをチェックし続けるため、これは問題です。さらに悪いことに、音声が再生されているのに、オーディオ要素の currentTime が更新されないことがあります。

ウェブオーディオ以外のフォールバックとして Chrome Racer で使用した AudioSprite の実装をご確認ください。

オーディオ要素

Racer の開発に着手したとき、Chrome for Android は Web Audio API にまだ対応していませんでした。デバイスによっては HTML オーディオ、その他のデバイスには Web Audio API を使用するロジックを、高度なオーディオ出力と組み合わせることで、いくつかの興味深い課題に対処できました。幸いなことに、今はこれがすべての歴史です。Web Audio API は Android M28 ベータ版に実装されています。

  • 遅延/タイミングに関する問題。オーディオ要素は、ユーザーが指定したとおりに再生されるとは限りません。JavaScript はシングル スレッドであるため、ブラウザがビジー状態になり、再生が最大 2 秒遅れることがあります。
  • 再生が遅れると、ループ再生がスムーズになるとは限りません。パソコンではダブル バッファリングを使ってギャップのないループ再生ができますが、モバイル デバイスでは以下の理由からループ再生できません。
    • ほとんどのモバイル デバイスでは、一度に複数の音声要素が再生されることはありません。
    • 音量は固定。Android と iOS のいずれも、音声オブジェクトの音量を変更できません。
  • プリロードなし。モバイル デバイスでは、touchStart ハンドラで再生が開始されない限り、オーディオ要素によるソースの読み込みは開始されません。
  • 問題の探索。サーバーが HTTP バイト範囲をサポートしていない場合、duration の取得または currentTime の設定は失敗します。先ほどのようにオーディオ スプライトを作成する場合は、この点に注意してください。
  • MP3 の基本認証が失敗する。使用しているブラウザにかかわらず、一部のデバイスでは、Basic 認証で保護された MP3 ファイルを読み込めません

まとめ

Google はウェブ向けの音声に対処する方法として、ミュートボタンを押すのに長い道のりを歩んできました。しかし、これはまだ始まりにすぎず、ウェブ音声はこれからも劇的な進化を遂げようとしています。ここでは、複数のデバイスの同期に関して可能なことについてのみ触れました。スマートフォンやタブレットには、信号処理やエフェクト(リバーブなど)を掘り下げる処理能力がありませんでしたが、デバイスのパフォーマンスが向上するにつれて、ウェブベースのゲームでもこれらの機能を利用するようになります。今は、サウンドの可能性を押し広げるエキサイティングな時期です。