Fallstudie: The Sounds of Racer

Einführung

Racer ist ein Chrome-Experiment für mehrere Spieler und Geräte. Ein Retro-Rennspiel für mehrere Bildschirme. Auf Smartphones oder Tablets mit Android oder iOS Jeder kann teilnehmen. Keine Apps Keine Downloads Nur im mobilen Web.

Plan8 hat zusammen mit unseren Freunden von 14islands die dynamische Musik und den Sound basierend auf einer Originalkomposition von Giorgio Moroder entwickelt. Racer bietet reaktionsfähige Motorgeräusche, Rennsoundeffekte und vor allem einen dynamischen Musikmix, der sich über mehrere Geräte verteilt, wenn Rennfahrer teilnehmen. Es handelt sich um eine Mehrfachlautsprecheranlage, die aus Smartphones besteht.

Wir hatten schon länger damit experimentiert, mehrere Geräte miteinander zu verbinden. Wir hatten Musikexperimente durchgeführt, bei denen der Ton auf verschiedenen Geräten aufgeteilt oder zwischen Geräten hin- und hergesprungen wurde. Diese Ideen wollten wir unbedingt auf Racer anwenden.

Konkret wollten wir testen, ob wir den Musiktrack auf allen Geräten aufbauen können, wenn immer mehr Leute am Spiel teilnehmen – angefangen mit Schlagzeug und Bass, dann Gitarre und Synthesizer usw. Wir haben einige Musikdemos gemacht und uns mit dem Programmieren beschäftigt. Der Effekt mehrerer Sprecher war wirklich beeindruckend. Die Synchronisierung war zu diesem Zeitpunkt noch nicht ganz richtig, aber als wir die verschiedenen Tonschichten über die Geräte verteilt hörten, wussten wir, dass wir auf dem richtigen Weg waren.

Töne erstellen

Google Creative Lab hatte eine kreative Richtung für den Ton und die Musik festgelegt. Wir wollten die Soundeffekte mit analogen Synthesizern erstellen, anstatt die echten Geräusche aufzunehmen oder auf Soundbibliotheken zurückzugreifen. Uns war auch klar, dass der Ausgabelautsprecher in den meisten Fällen ein winziger Smartphone- oder Tablet-Lautsprecher sein würde. Daher mussten die Töne im Frequenzspektrum begrenzt werden, um Verzerrungen zu vermeiden. Das erwies sich als ziemlich schwierig. Als wir die ersten Musikentwürfe von Giorgio erhielten, waren wir erleichtert, da seine Komposition perfekt zu den von uns erstellten Sounds passte.

Motorsound

Die größte Herausforderung bei der Programmierung der Sounds bestand darin, den besten Motorsound zu finden und sein Verhalten zu gestalten. Die Rennstrecke ähnelte einer F1- oder Nascar-Strecke, daher mussten die Autos schnell und dynamisch wirken. Gleichzeitig waren die Autos sehr klein, sodass ein lauter Motorsound nicht wirklich zu den Bildern gepasst hätte. Wir konnten den Sound eines brüllenden Motors ohnehin nicht über den Lautsprecher des Smartphones abspielen, also mussten wir uns etwas anderes einfallen lassen.

Zur Inspiration haben wir uns die Sammlung modularer Synthesizer unseres Freundes Jon Ekstrand angeschlossen und uns an die Arbeit gemacht. Uns hat das gefallen, was wir gehört haben. So klang es mit zwei Oszillatoren, einigen schönen Filtern und einem LFO.

Analoge Geräte wurden bereits mit großem Erfolg mithilfe der Web Audio API nachgebildet. Wir hatten also große Hoffnungen und begannen, einen einfachen Synthesizer in Web Audio zu erstellen. Ein generierter Ton wäre am reaktionsschnellsten, würde aber die Verarbeitungsleistung des Geräts belasten. Wir mussten extrem effizient vorgehen, um alle Ressourcen zu sparen, die wir konnten, damit die visuellen Elemente reibungslos funktionieren. Deshalb haben wir die Technik geändert und stattdessen Audiosamples wiedergegeben.

Modularer Synthesizer als Inspiration für Motorgeräusche

Es gibt verschiedene Techniken, mit denen sich ein Motorsound aus Samples erstellen lässt. Der häufigste Ansatz für Konsolenspiele besteht darin, mehrere Motorgeräusche (je mehr, desto besser) bei unterschiedlichen Umdrehungen pro Minute (mit Last) zu überlagern und dann einen Crossfade und eine Crosspitch zwischen ihnen vorzunehmen. Füge dann eine Ebene mit mehreren Motorgeräuschen hinzu, die bei derselben Drehzahl (ohne Last) hochgedreht werden, und füge einen Crossfade und eine Crosspitch zwischen den beiden hinzu. Wenn Sie beim Schalten ein Crossfading zwischen diesen Ebenen verwenden, klingt das bei richtiger Anwendung sehr realistisch, aber nur, wenn Sie eine große Anzahl von Audiodateien haben. Die Tonhöhenänderung darf nicht zu groß sein, da der Klang sonst sehr synthetisch wird. Da wir lange Ladezeiten vermeiden mussten, war diese Option für uns nicht geeignet. Wir haben es mit fünf oder sechs Audiodateien für jede Ebene versucht, aber der Klang war enttäuschend. Wir mussten einen Weg mit weniger Dateien finden.

Die effektivste Lösung erwies sich so:

  • Eine Audiodatei mit Beschleunigung und Gangschaltung, die mit der visuellen Beschleunigung des Autos synchronisiert ist und in einem programmierten Loop bei der höchsten Tonhöhe/U/min endet. Die Web Audio API ist sehr gut für präzise Loops geeignet, sodass wir das ohne Ruckler oder Knacken tun konnten.
  • Eine Audiodatei mit Verzögerung / Motordrehzahlabsenkung
  • Und schließlich eine Audiodatei, die den Ton für Inaktivität in einer Schleife abspielt.

Sie sieht so aus:

Grafik für Motorsound

Beim ersten Berührungsereignis / der ersten Beschleunigung wird die erste Datei von Anfang an abgespielt. Wenn der Spieler das Gaspedal loslässt, wird die Zeit berechnet, die beim Loslassen in der Audiodatei vergangen ist. Wenn das Gaspedal wieder betätigt wird, springt die Wiedergabe nach der Wiedergabe der zweiten Datei (Rückgang der Drehzahl) an die richtige Stelle in der Beschleunigungsdatei.

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);
}

Probieren Sie es einfach mal aus!

Starten Sie den Motor und drücken Sie die Taste „Gas“.

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

Also haben wir uns mit nur drei kleinen Audiodateien und einem gut klingenden Motor an die nächste Herausforderung gemacht.

Synchronisierung abrufen

Zusammen mit David Lindkvist von 14islands haben wir uns genauer damit beschäftigt, wie wir die Geräte synchron abspielen können. Die grundlegende Theorie ist einfach. Das Gerät fragt den Server nach der Uhrzeit, berücksichtigt die Netzwerklatenz und berechnet dann den lokalen Zeitversatz.

syncOffset = localTime - serverTime - networkLatency

Mit diesem Offset verwendet jedes verbundene Gerät dasselbe Zeitkonzept. Ganz einfach, oder? (Wieder nur in der Theorie.)

Netzwerklatenz berechnen

Angenommen, die Latenz entspricht der Hälfte der Zeit, die zum Anfordern und Empfangen einer Antwort vom Server benötigt wird:

networkLatency = (receivedTime - sentTime) × 0.5

Das Problem bei dieser Annahme ist, dass die Hin- und Rücklaufzeit zum Server nicht immer symmetrisch ist, d.h., die Anfrage kann länger dauern als die Antwort oder umgekehrt. Je höher die Netzwerklatenz ist, desto stärker wirkt sich diese Asymmetrie aus. Das führt dazu, dass Töne verzögert und nicht synchron mit anderen Geräten wiedergegeben werden.

Glücklicherweise ist unser Gehirn so verdrahtet, dass wir es nicht bemerken, wenn Töne leicht verzögert sind. Studien haben gezeigt, dass es 20 bis 30 Millisekunden dauert, bis unser Gehirn Töne als getrennt wahrnimmt. Ab etwa 12 bis 15 ms beginnen Sie jedoch, die Auswirkungen eines verzögerten Signals zu „spüren“, auch wenn Sie es nicht vollständig „wahrnehmen“ können. Wir haben einige etablierte Protokolle zur Zeitsynchronisierung und einfachere Alternativen untersucht und versucht, einige davon in der Praxis zu implementieren. Dank der Infrastruktur mit niedriger Latenz von Google konnten wir am Ende einfach eine Reihe von Anfragen erfassen und das Sample mit der niedrigsten Latenz als Referenz verwenden.

Abweichungen der Uhr beheben

Es hat funktioniert. Wir hatten mehr als fünf Geräte, die einen Puls in perfekter Synchronisation abspielten – aber nur für eine Weile. Nach einigen Minuten der Wiedergabe gerieten die Geräte auseinander, obwohl wir den Ton mithilfe der hochpräzisen Kontextzeit der Web Audio API geplant hatten. Die Verzögerung baute sich langsam auf, nur wenige Millisekunden pro Takt, und war anfangs nicht wahrnehmbar. Nach längerer Wiedergabe waren die einzelnen Musikschichten jedoch völlig asynchron. Hallo, es gibt eine Zeitabweichung.

Die Lösung bestand darin, alle paar Sekunden neu zu synchronisieren, einen neuen Zeitversatz zu berechnen und diesen nahtlos in den Audio-Scheduler einzugeben. Um das Risiko von deutlichen Änderungen bei der Musik aufgrund von Netzwerkverzögerungen zu verringern, haben wir uns entschieden, die Änderung zu glätten, indem wir einen Verlauf der letzten Synchronisationsverschiebungen aufbewahren und einen Durchschnitt berechnen.

Songs planen und Arrangements wechseln

Wenn du ein interaktives Klangerlebnis erstellst, kannst du nicht mehr steuern, wann Teile des Songs abgespielt werden, da du auf Nutzeraktionen angewiesen bist, um den aktuellen Status zu ändern. Wir mussten dafür sorgen, dass wir rechtzeitig zwischen den Arrangements im Song wechseln konnten. Das bedeutete, dass unser Scheduler berechnen musste, wie viel von der gerade wiedergegebenen Taktgruppe noch übrig ist, bevor zum nächsten Arrangement gewechselt wird. Unser Algorithmus sah in etwa so aus:

  • Mit Client(1) wird der Titel gestartet.
  • Client(n) fragt den ersten Client, wann der Titel gestartet wurde.
  • Client(n) berechnet einen Referenzpunkt für den Beginn des Titels anhand des Web Audio-Kontexts unter Berücksichtigung von syncOffset und der Zeit, die seit der Erstellung des Audio-Kontexts vergangen ist.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) berechnet anhand von „playDelta“, wie lange der Titel wiedergegeben wird. So weiß der Song-Scheduler, welcher Takt in der aktuellen Anordnung als Nächstes wiedergegeben werden soll.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Aus Gründen der Übersichtlichkeit haben wir unsere Arrangements auf acht Takte begrenzt und ihnen dasselbe Tempo (Beats pro Minute) gegeben.

Nach vorne schauen

Wenn du setTimeout oder setInterval in JavaScript verwendest, solltest du immer im Voraus planen. Das liegt daran, dass die JavaScript-Uhr nicht sehr genau ist und geplante Rückrufe durch Layout, Rendering, Garbage Collection und XMLHTTPRequests leicht um mehrere Zehntel von Millisekunden verzerrt werden können. In unserem Fall mussten wir auch die Zeit berücksichtigen, die vergeht, bis alle Clients dasselbe Ereignis über das Netzwerk empfangen.

Audio-Sprites

Wenn Sie mehrere Töne in einer Datei kombinieren, können Sie die Anzahl der HTTP-Anfragen sowohl für HTML-Audio als auch für die Web Audio API reduzieren. Außerdem ist es die beste Möglichkeit, mit dem Audioobjekt reaktionsschnell Töne abzuspielen, da vor dem Abspielen kein neues Audioobjekt geladen werden muss. Es gibt bereits einige gute Implementierungen, die wir als Ausgangspunkt verwendet haben. Wir haben unseren Sprite so erweitert, dass er sowohl auf iOS- als auch auf Android-Geräten zuverlässig funktioniert. Außerdem wurden einige ungewöhnliche Fälle behoben, in denen Geräte in den Ruhemodus gewechselt sind.

Unter Android werden Audioelemente auch dann fortgesetzt, wenn Sie das Gerät in den Ruhemodus versetzen. Im Ruhemodus wird die Ausführung von JavaScript eingeschränkt, um den Akku zu schonen. Sie können also nicht auf requestAnimationFrame, setInterval oder setTimeout zurückgreifen, um Callbacks zu starten. Das ist ein Problem, da Audio-Sprites JavaScript benötigen, um ständig zu prüfen, ob die Wiedergabe beendet werden soll. In einigen Fällen wird der Wert currentTime des Audioelements nicht aktualisiert, obwohl das Audio weiterhin wiedergegeben wird.

Sehen Sie sich die AudioSprite-Implementierung an, die wir in Chrome Racer als Fallback für nicht Web Audio verwendet haben.

Audio element

Als wir mit der Arbeit an Racer begannen, unterstützte Chrome für Android noch nicht die Web Audio API. Die Logik, HTML-Audio für einige Geräte und die Web Audio API für andere zu verwenden, kombiniert mit der erweiterten Audioausgabe, die wir erreichen wollten, stellte uns vor einige interessante Herausforderungen. Glücklicherweise ist das jetzt vorbei. Die Web Audio API ist in Android M28 Beta implementiert.

  • Verzögerungen/Zeitungsprobleme Das Audioelement wird nicht immer genau dann wiedergegeben, wenn Sie es anweisen. Da JavaScript ein einzelner Thread ist, kann der Browser ausgelastet sein, was zu Wiedergabeverzögerungen von bis zu zwei Sekunden führt.
  • Aufgrund von Wiedergabeverzögerungen ist ein flüssiger Loop nicht immer möglich. Auf dem Computer kannst du Doppelpufferung verwenden, um weitgehend lückenlose Loops zu erzielen. Auf Mobilgeräten ist das jedoch nicht möglich, weil:
    • Die meisten Mobilgeräte können nicht mehr als ein Audioelement gleichzeitig abspielen.
    • Festes Volumen. Weder unter Android noch unter iOS können Sie die Lautstärke eines Audioobjekts ändern.
  • Kein Vorabladen. Auf Mobilgeräten wird die Quelle des Audioelements erst geladen, wenn die Wiedergabe in einem touchStart-Handler gestartet wird.
  • Suche nach Problemen Das Abrufen von duration oder das Festlegen von currentTime schlägt fehl, es sei denn, Ihr Server unterstützt HTTP-Byte-Bereiche. Achtung: Das ist wichtig, wenn du wie wir einen Audio-Sprite erstellst.
  • Die Basic Auth für MP3 schlägt fehl. Auf einigen Geräten können mit der Basisauthentifizierung geschützte MP3-Dateien nicht geladen werden, unabhängig davon, welchen Browser du verwendest.

Ergebnisse

Wir haben schon viel erreicht, seit die Stummschalttaste die beste Option für den Umgang mit Audio im Web war. Das ist aber erst der Anfang und Web-Audio wird bald richtig durchstarten. Wir haben nur an der Oberfläche dessen gekratzt, was mit der Synchronisierung mehrerer Geräte möglich ist. Die Prozessorleistung von Smartphones und Tablets reichte nicht aus, um Signalverarbeitung und Effekte wie Hall zu nutzen. Mit steigender Geräteleistung werden diese Funktionen aber auch in webbasierten Spielen eingesetzt. Es ist eine spannende Zeit, um die Möglichkeiten von Sound weiter zu erforschen.