Audio für Spiele mit der Web Audio API entwickeln

Boris Smus
Boris Smus

Einführung

Audio ist ein wichtiger Faktor, der Multimediainhalte so ansprechend macht. Wenn Sie sich schon einmal einen Film ohne Ton angesehen haben, ist Ihnen das wahrscheinlich aufgefallen.

Spiele sind keine Ausnahme. Meine liebsten Erinnerungen an Videospiele sind die Musik und die Soundeffekte. Auch heute, fast zwei Jahrzehnte nach dem Spielen meiner Lieblingsspiele, kann ich die Kompositionen von Koji Kondo für Zelda und den atmosphärischen Diablo-Soundtrack von Matt Uelmen nicht aus meinem Kopf bekommen. Das Gleiche gilt für Soundeffekte wie die sofort erkennbaren Klickgeräusche von Einheiten in Warcraft und Samples aus Nintendo-Klassikern.

Der Audiobereich in Spielen stellt einige interessante Herausforderungen. Um überzeugende Spielmusik zu erstellen, müssen sich Designer an den potenziell unvorhersehbaren Spielstatus anpassen, in dem sich ein Spieler befindet. In der Praxis können Teile des Spiels eine unbekannte Dauer haben, Geräusche können mit der Umgebung interagieren und auf komplexe Weise vermischt werden, z. B. Raumeffekte und relative Tonpositionierung. Schließlich kann es eine große Anzahl von gleichzeitig abgespielten Sounds geben, die alle gut zusammenklingen und ohne Leistungseinbußen gerendert werden müssen.

Spielaudio im Web

Bei einfachen Spielen reicht das <audio>-Tag möglicherweise aus. Viele Browser bieten jedoch eine schlechte Implementierung, was zu Audiostörungen und hoher Latenz führt. Hoffentlich ist das nur ein vorübergehendes Problem, da die Anbieter intensiv daran arbeiten, ihre jeweiligen Implementierungen zu verbessern. Einen Überblick über den Status des <audio>-Tags bietet die Testsuite unter areweplayingyet.org.

Wenn Sie sich jedoch die <audio>-Tag-Spezifikation genauer ansehen, wird klar, dass viele Dinge damit einfach nicht möglich sind. Das ist nicht überraschend, da es für die Medienwiedergabe entwickelt wurde. Zu den Einschränkungen gehören:

  • Es können keine Filter auf das Tonsignal angewendet werden.
  • Kein Zugriff auf die Roh-PCM-Daten
  • Kein Konzept für Position und Richtung von Quellen und Zuhörern
  • Keine detaillierte Zeitplanung.

Im Rest des Artikels werde ich einige dieser Themen im Kontext von Game-Audio besprechen, das mit der Web Audio API erstellt wurde. Eine kurze Einführung in diese API finden Sie im Einstiegsleitfaden.

Hintergrundmusik

In Spielen wird oft Hintergrundmusik in einer Schleife abgespielt.

Es kann sehr nervig werden, wenn der Loop kurz und vorhersehbar ist. Wenn ein Spieler in einem Bereich oder Level feststeckt und im Hintergrund immer dasselbe Sample abgespielt wird, kann es sinnvoll sein, den Titel nach und nach auszublenden, um Frustration zu vermeiden. Eine weitere Strategie besteht darin, Mischungen verschiedener Intensität zu verwenden, die je nach Kontext des Spiels allmählich ineinander übergehen.

Wenn sich Ihr Spieler beispielsweise in einer Zone mit einem epischen Bosskampf befindet, können Sie mehrere Mixe mit unterschiedlicher emotionaler Bandbreite haben, von atmosphärisch über andeutungsweise bis hin zu intensiv. Mit Musiksynthesesoftware können Sie oft mehrere Mixe (derselben Länge) auf der Grundlage eines Titels exportieren, indem Sie die Tracks auswählen, die im Export verwendet werden sollen. So hast du eine gewisse interne Konsistenz und vermeidest abrupte Übergänge beim Überblenden von einem Titel zum nächsten.

Garageband

Anschließend können Sie mit der Web Audio API alle diese Samples über XHR mithilfe einer Klasse wie der BufferLoader-Klasse importieren. Weitere Informationen finden Sie im Einführungsartikel zur Web Audio API. Das Laden von Audioinhalten dauert. Daher sollten Assets, die im Spiel verwendet werden, beim Laden der Seite, zu Beginn des Levels oder schrittweise während des Spiels geladen werden.

Als Nächstes erstellen Sie für jeden Knoten eine Quelle und für jede Quelle einen Verstärkungsknoten und verbinden den Graphen.

Anschließend können Sie alle diese Quellen gleichzeitig in einer Schleife abspielen. Da sie alle dieselbe Länge haben, sorgt die Web Audio API dafür, dass sie synchron bleiben. Je näher oder weiter der Charakter vom letzten Bosskampf entfernt ist, desto mehr kann der Spielgewinn für jeden der jeweiligen Knoten in der Kette variieren. Dazu wird ein Algorithmus für den Gewinnbetrag verwendet, der in etwa so aussieht:

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

Bei diesem Ansatz werden zwei Quellen gleichzeitig abgespielt und wir verwenden einen Crossfade mit gleichen Leistungskurven (wie in der Einführung beschrieben).

Viele Spieleentwickler verwenden das <audio>-Tag für ihre Hintergrundmusik, da es sich gut für Streaminginhalte eignet. Du kannst jetzt Inhalte aus dem <audio>-Tag in einen Web Audio-Kontext einbinden.

Diese Methode kann nützlich sein, da das <audio>-Tag mit Streaminginhalten funktioniert. So kannst du die Hintergrundmusik sofort abspielen, anstatt warten zu müssen, bis sie vollständig heruntergeladen wurde. Wenn Sie den Stream in die Web Audio API einbinden, können Sie ihn bearbeiten oder analysieren. Im folgenden Beispiel wird ein Tiefpassfilter auf die Musik angewendet, die über das <audio>-Tag wiedergegeben wird:

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

Eine ausführlichere Erläuterung zur Einbindung des <audio>-Tags in die Web Audio API findest du in diesem kurzen Artikel.

Soundeffekte

In Spielen werden häufig Soundeffekte als Reaktion auf Nutzereingaben oder Änderungen des Spielstatus wiedergegeben. Ähnlich wie Hintergrundmusik können Soundeffekte jedoch sehr schnell nervig werden. Um dies zu vermeiden, ist es oft sinnvoll, einen Pool mit ähnlichen, aber unterschiedlichen Klängen zu haben. Das kann von leichten Variationen von Fußschritten bis hin zu drastischen Variationen reichen, wie in der Warcraft-Reihe beim Klicken auf Einheiten.

Ein weiteres wichtiges Merkmal von Soundeffekten in Spielen ist, dass es viele davon gleichzeitig geben kann. Stellen Sie sich vor, Sie befinden sich mitten in einer Schießerei mit mehreren Schauspielern, die mit Maschinengewehren schießen. Jedes Maschinengewehr schießt viele Male pro Sekunde, was dazu führt, dass gleichzeitig Dutzende von Soundeffekten abgespielt werden. Die gleichzeitige Wiedergabe von Audio aus mehreren, genau getimten Quellen ist ein Anwendungsfall, in dem die Web Audio API ihre Stärken voll ausspielen kann.

Im folgenden Beispiel wird eine Maschinengewehrsalve aus mehreren einzelnen Gewehrpatronen-Samples erstellt. Dazu werden mehrere Audioquellen erstellt, deren Wiedergabe zeitlich versetzt ist.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Wenn alle Maschinengewehre in Ihrem Spiel genau so klangen, wäre das ziemlich langweilig. Natürlich variieren sie je nach Ton, je nach Entfernung vom Ziel und je nach relativer Position (mehr dazu später). Aber selbst das reicht möglicherweise nicht aus. Glücklicherweise bietet die Web Audio API zwei Möglichkeiten, das Beispiel oben ganz einfach anzupassen:

  1. mit einer subtilen Zeitverschiebung zwischen den abgefeuerten Kugeln
  2. Durch Ändern der Wiedergaberate jedes Samples (auch Tonhöhe ändern), um die Zufälligkeit der realen Welt besser zu simulieren.

Ein realistischeres Beispiel für diese Techniken in der Praxis findest du in der Demo für den Billardtisch, in der zufällige Stichproben verwendet und die Wiedergaberate variiert wird, um einen interessanteren Klang bei der Ballkollision zu erzeugen.

3D-Positions-Audio

Spiele spielen oft in einer Welt mit einigen geometrischen Eigenschaften, entweder in 2D oder in 3D. In diesem Fall kann Stereoton das immersive Erlebnis erheblich steigern. Glücklicherweise bietet die Web Audio API integrierte hardwaregestützte Funktionen für Positionsaudio, die sich ganz einfach verwenden lassen. Achten Sie darauf, dass Sie Stereolautsprecher (vorzugsweise Kopfhörer) haben, damit das folgende Beispiel Sinn macht.

Im obigen Beispiel befindet sich in der Mitte des Canvas ein Zuhörer (Personensymbol) und die Maus wirkt sich auf die Position der Quelle (Lautsprechersymbol) aus. Das obige Beispiel zeigt, wie du mit dem AudioPannerNode einen solchen Effekt erzielst. Bei diesem Beispiel geht es darum, auf Mausbewegungen zu reagieren, indem die Position der Audioquelle so festgelegt wird:

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

Wissenswertes zur Spatialisierung in Web Audio:

  • Der Listener befindet sich standardmäßig am Ursprung (0, 0, 0).
  • Web Audio-Positions-APIs sind ohne Maßeinheit. Deshalb habe ich einen Multiplikator eingeführt, um den Klang der Demo zu verbessern.
  • Web Audio verwendet kartesische Koordinaten mit y-nach-oben (im Gegensatz zu den meisten Computergrafiksystemen). Deshalb tausche ich die Y-Achse im Snippet oben aus.

Erweitert: Schallkegel

Das Positionsmodell ist sehr leistungsstark und ziemlich fortschrittlich. Es basiert hauptsächlich auf OpenAL. Weitere Informationen finden Sie in den Abschnitten 3 und 4 der oben verlinkten Spezifikation.

Positionsmodell

Dem Web Audio API-Kontext ist ein einzelner AudioListener zugeordnet, der im Raum durch Position und Ausrichtung konfiguriert werden kann. Jede Quelle kann durch einen AudioPannerNode geleitet werden, der das Eingabeaudio räumlich ordnet. Der Schwenkknoten hat eine Position und Orientierung sowie ein Entfernungs- und Richtungsmodell.

Beim Entfernungsmodell wird die Verstärkung in Abhängigkeit von der Nähe zur Quelle angegeben. Das Richtungsmodell kann durch Angabe eines inneren und eines äußeren Kegels konfiguriert werden, die die (in der Regel negative) Verstärkung bestimmen, wenn sich der Hörer im inneren Kegel, zwischen dem inneren und dem äußeren Kegel oder außerhalb des äußeren Kegels befindet.

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

Obwohl mein Beispiel in 2D ist, lässt sich dieses Modell leicht auf die dritte Dimension verallgemeinern. Ein Beispiel für einen räumlichen 3D-Ton findest du in diesem Positionsbeispiel. Neben der Position enthält das Web Audio-Tonmodell optional auch die Geschwindigkeit für Dopplerverschiebungen. Dieses Beispiel zeigt den Dopplereffekt genauer.

Weitere Informationen zu diesem Thema findest du in diesem ausführlichen Tutorial zum [Mischen von Positionsaudio und WebGL][webgl].

Raumeffekte und -filter

In Wirklichkeit hängt die Wahrnehmung von Geräuschen stark vom Raum ab, in dem sie zu hören sind. Die gleiche quietschende Tür klingt in einem Keller ganz anders als in einem großen offenen Saal. Bei Spielen mit hohem Produktionswert sollten diese Effekte nachgebildet werden, da das Erstellen eines separaten Sets von Samples für jede Umgebung unerschwinglich teuer ist und zu noch mehr Assets und einer größeren Menge an Spieldaten führen würde.

Der Audiobegriff für die Differenz zwischen dem Rohton und dem tatsächlichen Klang ist die Impulsantwort. Diese Impulsantworten können mühsam aufgenommen werden. Es gibt jedoch Websites, auf denen viele dieser vorab aufgezeichneten Impulsantwortdateien (als Audio gespeichert) gehostet werden.

Weitere Informationen dazu, wie Impulsantworten in einer bestimmten Umgebung erstellt werden können, finden Sie im Abschnitt „Aufnahmeeinrichtung“ im Teil Konvolution der Web Audio API-Spezifikation.

Noch wichtiger für unsere Zwecke ist, dass die Web Audio API eine einfache Möglichkeit bietet, diese Impulsantworten mit dem ConvolverNode auf unsere Töne anzuwenden.

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

Sehen Sie sich auch diese Demo von Raumeffekten auf der Seite mit den Web Audio API-Spezifikationen sowie dieses Beispiel an, mit dem Sie die trockene (roh) und feuchte (über einen Convolver verarbeitete) Mischung eines großartigen Jazzstandards steuern können.

Der Countdown läuft

Sie haben also ein Spiel erstellt, die Positionsaudio konfiguriert und jetzt haben Sie eine große Anzahl von AudioNodes in Ihrem Diagramm, die alle gleichzeitig wiedergegeben werden. Super, aber es gibt noch etwas zu beachten:

Da mehrere Töne ohne Normalisierung einfach übereinander gelegt werden, kann es vorkommen, dass Sie die Kapazität Ihres Lautsprechers überschreiten. Ähnlich wie bei Bildern, die über den Canvas hinausragen, können auch Töne abgeschnitten werden, wenn die Wellenform den maximalen Grenzwert überschreitet. Dies führt zu einer deutlichen Verzerrung. Die Wellenform sieht in etwa so aus:

Clipping

Hier ist ein praktisches Beispiel für das Zuschneiden. Die Wellenform sieht schlecht aus:

Clipping

Es ist wichtig, sich solche harten Verzerrungen wie oben anzusehen oder umgekehrt zu übertrieben gedämpften Mixen, die die Zuhörer dazu zwingen, die Lautstärke hochzudrehen. Wenn Sie sich in dieser Situation befinden, müssen Sie das unbedingt ändern.

Ausschnitte erkennen

Aus technischer Sicht tritt Clipping auf, wenn der Wert des Signals in einem Kanal den gültigen Bereich überschreitet, also -1 bis 1. Sobald dies erkannt wird, ist es hilfreich, visuelles Feedback zu geben, dass dies der Fall ist. Damit das zuverlässig funktioniert, fügen Sie Ihrem Diagramm einen JavaScriptAudioNode hinzu. Das Audiodiagramm würde so eingerichtet:

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

Und das Zuschneiden könnte im folgenden processAudio-Handler erkannt werden:

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

Verwenden Sie das JavaScriptAudioNode aus Leistungsgründen im Allgemeinen nicht zu oft. In diesem Fall könnte eine alternative Implementierung der Analyse bei der Wiedergabezeit einen RealtimeAnalyserNode im Audio-Graphen auf getByteFrequencyData abfragen, wie von requestAnimationFrame bestimmt. Dieser Ansatz ist effizienter, aber es wird der Großteil des Signals nicht erfasst (auch nicht an Stellen, an denen es möglicherweise zu Clipping kommt), da das Rendering maximal 60 Mal pro Sekunde erfolgt, während sich das Audiosignal viel schneller ändert.

Da die Clip-Erkennung so wichtig ist, wird es wahrscheinlich in Zukunft einen integrierten MeterNode Web Audio API-Knoten geben.

Übersteuern verhindern

Durch Anpassung der Verstärkung am Master-AudioGainNode kannst du deinen Mix so leise machen, dass Clipping verhindert wird. Da die in Ihrem Spiel wiedergegebenen Töne jedoch von einer Vielzahl von Faktoren abhängen können, ist es in der Praxis oft schwierig, den Master-Gain-Wert zu bestimmen, der Clipping für alle Status verhindert. Im Allgemeinen sollten Sie die Gewinne so anpassen, dass Sie den Worst-Case-Fall abdecken. Das ist aber mehr eine Kunst als eine Wissenschaft.

Fügen Sie ein wenig Zucker hinzu.

Kompressoren werden häufig in der Musik- und Spieleproduktion verwendet, um das Signal zu glätten und Spitzen im Gesamtsignal zu kontrollieren. Diese Funktion ist in Web Audio über das Symbol DynamicsCompressorNode verfügbar. Es kann in deinen Audio-Graphen eingefügt werden, um einen lauteren, reicheren und volleren Klang zu erzielen und auch bei Clipping zu helfen. Gemäß der Spezifikation ist dieser Knoten

Die dynamische Komprimierung ist im Allgemeinen eine gute Idee, insbesondere in einer Spielumgebung, in der Sie, wie bereits erwähnt, nicht genau wissen, welche Töne wann abgespielt werden. Plink von DinahMoe Labs ist ein gutes Beispiel dafür, da die wiedergegebenen Töne vollständig von Ihnen und anderen Teilnehmern abhängen. Ein Kompressor ist in den meisten Fällen nützlich, mit Ausnahme einiger seltener Fälle, in denen es um sorgfältig gemasterte Tracks geht, die bereits so eingestellt wurden, dass sie „genau richtig“ klingen.

Dazu musst du einfach einen DynamicsCompressorNode in deinen Audiographen einfügen, in der Regel als letzten Knoten vor dem Ziel:

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Weitere Informationen zur dynamischen Komprimierung finden Sie in diesem Wikipedia-Artikel.

Zusammenfassend: Achte genau darauf, ob es zu Clipping kommt, und verhindere dies, indem du einen Master-Gain-Knoten einfügst. Anschließend habe ich den gesamten Mix mit einem Dynamik-Kompressor-Knoten gestrafft. Dein Audiodiagramm könnte in etwa so aussehen:

Endergebnis

Fazit

Das sind meiner Meinung nach die wichtigsten Aspekte der Audioentwicklung für Spiele mit der Web Audio API. Mit diesen Techniken können Sie direkt im Browser überzeugende Audioinhalte erstellen. Bevor ich ende, möchte ich Ihnen noch einen browserspezifischen Tipp geben: Pausieren Sie den Ton, wenn Ihr Tab mithilfe der Page Visibility API in den Hintergrund wechselt. Andernfalls kann das für Ihre Nutzer frustrierend sein.

Weitere Informationen zu Web Audio finden Sie im Einstiegsartikel. Wenn Sie eine Frage haben, sehen Sie in den häufig gestellten Fragen zu Web Audio nach, ob sie dort bereits beantwortet wird. Falls Sie weitere Fragen haben, können Sie sie unter Stack Overflow mit dem Tag web-audio stellen.

Zum Abschluss möchte ich Ihnen noch einige tolle Anwendungsfälle der Web Audio API in aktuellen Spielen vorstellen: