Fallstudie: The Sounds of Racer

Einleitung

Racer ist ein Chrome-Experiment für verschiedene Spieler und Geräte. Ein Slotauto im Retrostil, das auf verschiedenen Bildschirmen gespielt wird. Auf Smartphones oder Tablets, Android oder iOS. Jeder kann beitreten. Keine Apps. Ohne Downloads. Nur das mobile Web.

Plan8 entwickelte zusammen mit unseren Freunden auf 14islands das dynamische Musik- und Sounderlebnis basierend auf einer Originalkomposition von Giorgio Moroder. „Racer“ bietet reaktionsschnelle Motorklänge, Rasen-Soundeffekte und vor allem einen dynamischen Musik-Mix, der sich auf verschiedenen Geräten verteilt, wenn die Rennteilnehmer antreten. Es handelt sich dabei um eine Installation mit mehreren Lautsprechern, die aus Smartphones besteht.

Wir probierten schon eine Zeit lang aus, mehrere Geräte miteinander zu verbinden. Wir hatten Musikexperimente durchgeführt, bei denen der Sound auf verschiedenen Geräten geteilt wurde oder zwischen Geräten hin und her ging. Wir wollten diese Ideen also auch auf „Racer“ anwenden.

Genauer gesagt wollten wir testen, ob wir den Musiktitel auf allen Geräten aufbauen können, da immer mehr Leute zum Spiel hinzukommen – zuerst mit Schlagzeug und Bass, dann mit Gitarre und Synthesizern und so weiter. Wir haben Musikdemos durchgeführt und ins Programmieren eingestiegen. Der Multi-Lautsprecher-Effekt hat sich wirklich gelohnt. Zu diesem Zeitpunkt hatten wir zwar noch nicht die gesamte Synchronisierung richtig, aber als wir hörten, wie sich die verschiedenen Klangschichten auf den Geräten ausbreiteten, wussten wir, dass wir hier auf etwas Gutes kamen.

Töne erstellen

Google Creative Lab hatte eine kreative Richtung für den Ton und die Musik vorgelegt. Wir wollten für die Soundeffekte analoge Synthesizer verwenden, statt echte Klänge aufzuzeichnen oder auf Soundbibliotheken zu setzen. Wir wussten auch, dass der Ausgangslautsprecher in den meisten Fällen ein kleines Smartphone- oder Tabletlautsprecher sein würde. Daher musste das Frequenzspektrum begrenzt sein, damit die Lautsprecher nicht verzerrt werden. Das war eine ziemliche Herausforderung. Als wir die ersten Musikentwürfe von Giorgio erhielten, war das eine Erleichterung, da seine Komposition perfekt zu unseren Sounds passte.

Motorgeräusche

Die größte Herausforderung bei der Programmierung der Sounds bestand darin, den besten Engine-Sound zu finden und sein Verhalten zu formen. Die Rennstrecke ähnelte einer Formel-1- oder Nascar-Strecke, daher mussten die Autos schnell und explosiv wirken. Gleichzeitig waren die Autos sehr klein, sodass ein großes Motorengeräusch den Sound nicht wirklich mit der visuellen Gestaltung verbinden konnte. Es war nicht möglich, im mobilen Lautsprecher ein lautes Geräusch zu hören, also mussten wir etwas anderes herausfinden.

Um uns inspirieren zu lassen, haben wir uns die modularen Synthesizer-Sammlung unseres Freundes Jon Ekstrand angeschafft und viel herumgespielt. Uns hat das Feedback gefallen. So klang es mit zwei Oszillatoren, ein paar schönen Filtern und dem LFO.

Analoge Geräte wurden zuvor mit dem Web Audio API mit großem Erfolg umgestaltet. Daher hatten wir große Hoffnungen und begannen mit der Entwicklung eines einfachen Synthesizers in Web Audio. Ein erzeugter Ton würde am reaktionsschnellsten sein, würde aber die Prozessorleistung des Geräts belasten. Wir mussten sehr schlank sein, um alle verfügbaren Ressourcen für einen reibungslosen Ablauf der Visualisierungen zu ersparen. Also haben wir die Technik geändert und stattdessen Audioproben abgespielt.

Modularer Synthesizer zur Inspiration für Motorklang

Es gibt verschiedene Techniken, mit denen ein Engine-Sound aus Samples generiert werden kann. Der gängigste Ansatz für Konsolenspiele besteht darin, mehrere Sounds der Engine bei unterschiedlichen Umdrehungsgeschwindigkeiten (mit Last) zu erzeugen (je mehr, desto besser) und überblenden und kreuzen. Dann füge eine Schicht mit mehreren Klängen des Motors hinzu, der (ohne Last) mit der gleichen Umdrehungsgeschwindigkeit dreht und über- und untergeht. Wenn Sie beim Wechseln zwischen den Schichten überblenden, klingt – wenn es richtig gemacht – sehr realistisch, allerdings nur, wenn Sie eine große Menge an Audiodateien haben. Die Cross-Pitching-Technik darf nicht zu breit sein, sonst klingt sie sehr synthetisch. Da wir lange Ladezeiten vermeiden mussten, war diese Option für uns nicht gut. Wir haben es mit fünf oder sechs Sounddateien für jede Schicht versucht, aber der Sound war enttäuschend. Wir mussten einen Weg mit weniger Dateien finden.

Dies war die effektivste Lösung:

  • Eine Sounddatei mit Beschleunigung und Gangschaltung, die mit der visuellen Beschleunigung des Autos synchronisiert ist, das in einer programmierten Schleife mit der höchsten Tonhöhe/U/min endet. Die Web Audio API ist sehr gut darin, Schleifen zu erstellen, sodass wir dies ohne Störungen oder Knacker tun könnten.
  • Eine Sounddatei mit Verzögerung / Motor, die herunterdreht.
  • Und schließlich eine Sounddatei, die den stillen / inaktiven Ton in einer Schleife wiedergibt.

Sieht so aus

Grafik des Motors

Für das erste Berührungsereignis / die erste Beschleunigung würden wir die erste Datei von Anfang an abspielen. Wenn der Player die Drosselung loslässt, würden wir die Zeit berechnen, ab der wir uns zum Zeitpunkt der Veröffentlichung in der Sounddatei befanden, sodass die Drosselung bei der nächsten Wiedergabe der Drosselung an die richtige Stelle in der Beschleunigungsdatei springen würde, nachdem die zweite Datei (zum Herunterdrehen des Reglers) an die richtige Stelle springt.

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 Drosseltaste.

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

Mit nur drei kleinen Audiodateien und einer gut klingenden Engine entschieden wir uns für die nächste Herausforderung.

Synchronisierung wird abgerufen

Gemeinsam mit David Lindkvist von 14islands haben wir uns angeschaut, wie die Geräte perfekt aufeinander abgestimmt sind. Die Grundtheorie ist einfach. Das Gerät fragt den Server nach der Zeit, berücksichtigt die Netzwerklatenz und berechnet dann den lokalen Zeitversatz.

syncOffset = localTime - serverTime - networkLatency

Mit diesem Offset nutzt jedes verbundene Gerät das gleiche Zeitkonzept. Einfach, oder? (Theoretisch wieder einmal.)

Netzwerklatenz berechnen

Wir nehmen an, dass die Latenz halb so lange dauert, bis eine Antwort vom Server angefordert und empfangen wird:

networkLatency = (receivedTime - sentTime) × 0.5

Das Problem bei dieser Annahme ist, dass der Umlauf 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 größer ist die Auswirkung dieser Asymmetrie, denn Töne werden verzögert und nicht synchron mit anderen Geräten wiedergegeben.

Glücklicherweise ist unser Gehirn darauf veranschlagt, nicht wahrzunehmen, wenn Töne leicht verzögert sind. Studien haben gezeigt, dass es eine Verzögerung von 20 bis 30 Millisekunden (ms) dauert, bis unser Gehirn Töne als separate Wahrnehmungen wahrnimmt. Nach etwa 12 bis 15 ms beginnen Sie jedoch, die Effekte eines verzögerten Signals zu „spüren“, auch wenn Sie es nicht vollständig „wahrnehmen“ können. Wir haben einige etablierte Zeitsynchronisierungsprotokolle und einfachere Alternativen untersucht und versucht, einige davon in der Praxis zu implementieren. Dank der geringen Latenzzeit von Google konnten wir am Ende einfach eine Reihe von Anfragen abtasten und das Beispiel mit der niedrigsten Latenz als Referenz verwenden.

Kampfuhrenverschiebung

Es hat funktioniert! Wir hatten über 5 Geräte, die den Puls perfekt synchron liefen – aber nur eine Zeit lang. Nach ein paar Minuten der Wiedergabe schienen die Geräte auseinander, obwohl der Ton mit der hochpräzisen Kontextzeit der Web Audio API geplant wurde. Die Verzögerung akkumulierte sich langsam, jeweils nur um ein paar Millisekunden, und ist erst einmal nicht erkennbar. Nach einer längeren Wiedergabedauer sind die Musikebenen aber völlig nicht mehr synchron. Hallo, Uhrverschiebung.

Die Lösung bestand darin, alle paar Sekunden eine neue Synchronisierung durchzuführen, eine neue Zeitverschiebung zu berechnen und diese nahtlos in den Audio-Planer einzuspeisen. Um das Risiko bemerkenswerter Änderungen bei der Musik aufgrund von Netzwerkverzögerungen zu verringern, haben wir beschlossen, die Änderung auszugleichen, indem wir einen Verlauf der letzten Synchronisierungsverschiebungen speichern und einen Durchschnitt berechnen.

Einen Song planen und das Arrangement wechseln

Bei einem interaktiven Audioerlebnis hast du nicht mehr die Kontrolle darüber, wann Teile des Songs abgespielt werden, da der aktuelle Zustand nur durch Nutzeraktionen geändert werden kann. Wir mussten sicherstellen, dass wir rechtzeitig zwischen den Arrangements im Song hin- und herwechseln können, was bedeutete, dass unser Planer berechnen musste, wie viel vom aktuell wiedergegebenen Takt übrig bleibt, bevor er zum nächsten Arrangement wechselte. Unser Algorithmus sah dann in etwa so aus:

  • Client(1) startet den Song.
  • Client(n) fragt den ersten Kunden, wann der Song gestartet wurde.
  • Client(n) berechnet einen Referenzpunkt für den Beginn des Titels anhand seines Web Audio-Kontexts. Dabei werden syncOffset und die seit der Erstellung des Audiokontexts vergangene Zeit berücksichtigt.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) berechnet mithilfe von playDelta, wie lange der Titel läuft. Der Song-Planer verwendet diese Informationen, um zu bestimmen, welcher Takt im aktuellen Arrangement als Nächstes gespielt werden soll.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Aus Gründen der Übersichtlichkeit beschränkten wir unsere Arrangements auf eine Länge von immer acht Takten und dasselbe Tempo (Schläge pro Minute).

Nach vorne schauen

Wenn Sie setTimeout oder setInterval in JavaScript verwenden, ist es immer wichtig, einen Termin zu planen. Das liegt daran, dass die JavaScript-Uhr nicht sehr genau ist und geplante Callbacks leicht um mehrere zehn Millisekunden oder mehr durch Layout, Rendering, automatische Speicherbereinigung und XMLHTTPRequests verzerrt werden können. In unserem Fall mussten wir auch die Zeit berücksichtigen, die alle Kunden benötigen, um dasselbe Ereignis über das Netzwerk zu empfangen.

Audio-Sprites

Das Kombinieren von Tönen in einer Datei ist eine großartige Möglichkeit, HTTP-Anfragen sowohl für HTML Audio als auch für das Web Audio API zu reduzieren. Es ist auch die beste Möglichkeit, Töne responsiv über das Audio-Objekt abzuspielen, da es vor der Wiedergabe kein neues Audioobjekt laden muss. Es gibt bereits einige gute Implementierungen, die wir als Ausgangspunkt verwendet haben. Wir haben das Sprite ausgeweitet, sodass es sowohl unter iOS als auch auf Android zuverlässig funktioniert. Außerdem kommen einige ungewöhnliche Fälle vor, in denen Geräte in den Ruhemodus versetzt werden.

Unter Android werden Audioelemente weiter abgespielt, auch wenn Sie das Gerät in den Ruhemodus versetzen. Im Schlafmodus ist die JavaScript-Ausführung eingeschränkt, um den Akku zu schonen, und du kannst dich nicht auf requestAnimationFrame, setInterval oder setTimeout verlassen, um Callbacks auszulösen. Dies ist ein Problem, da Audio-Sprites auf JavaScript angewiesen sind, um zu prüfen, ob die Wiedergabe gestoppt werden sollte. In einigen Fällen wird der currentTime des Audioelements nicht aktualisiert, obwohl die Audioinhalte noch abgespielt werden.

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

Audio element

Als wir mit der Arbeit an Racer begannen, unterstützte Chrome für Android die Web Audio API noch nicht. Die Logik bei der Verwendung von HTML-Audio bei einigen Geräten, bei anderen das Web Audio API in Kombination mit der fortschrittlichen Audioausgabe, die wir erreichen wollten, machten einige interessante Herausforderungen. Zum Glück ist das jetzt alles Geschichte. Die Web Audio API ist in der Android M28-Betaversion implementiert.

  • Verzögerungen/Zeitüberschreitungen Das Audioelement wird nicht immer genau in dem Moment wiedergegeben, in dem Sie es auffordern. Da es sich bei JavaScript um Single-Threaded handelt, ist der Browser möglicherweise überlastet, was zu Verzögerungen bei der Wiedergabe von bis zu zwei Sekunden führen kann.
  • Aufgrund von Wiedergabeverzögerungen ist eine reibungslose Schleife nicht immer möglich. Auf Computern können Sie doppelte Zwischenspeichern verwenden, um etwas lückenlose Schleifen zu erreichen. Auf Mobilgeräten ist dies aus folgenden Gründen nicht möglich:
    • Auf den meisten Mobilgeräten wird immer nur ein Audioelement gleichzeitig abgespielt.
    • Korrigierte Lautstärke. Weder unter Android noch auf iOS ist es möglich, die Lautstärke eines Audioobjekts zu ändern.
  • Kein Vorabladen. Auf Mobilgeräten beginnt das Audioelement erst dann mit dem Laden der Quelle, wenn die Wiedergabe in einem touchStart-Handler initiiert 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-Range. Seien Sie vorsichtig, wenn Sie wie wir ein Audio-Sprite erstellen.
  • Basic Auth bei MP3 schlägt fehl. Einige Geräte können keine durch Basic Auth geschützten MP3-Dateien laden, unabhängig davon, welchen Browser Sie verwenden.

Ergebnisse

Wir haben viel getan, seitdem Sie die Stummschaltung als beste Option für den Umgang mit Audioinhalten im Web verwenden möchten, aber dies ist erst der Anfang. Wir haben nur oberflächlich die Möglichkeiten bei der Synchronisierung mehrerer Geräte behandelt. Bei den Smartphones und Tablets fehlten die Rechenleistung, um die Signalverarbeitung und Effekte wie Nachhall zu untersuchen. Mit zunehmender Geräteleistung werden wir diese Funktionen aber auch für webbasierte Spiele nutzen. Es sind aufregende Zeiten, in denen wir die Möglichkeiten des Sounds weiter vorantreiben.