Audio für Spiele mit der Web Audio API entwickeln

Boris Smus
Boris Smus

Einführung

Audio macht Multimedia-Erlebnisse so überzeugend. Wenn Sie sich schon einmal einen Film ohne Ton angesehen haben, ist Ihnen das wahrscheinlich aufgefallen.

Spiele sind da 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 stimmungsvollen Diablo-Soundtrack von Matt Uelmen nicht aus meinem Kopf bekommen. Das gilt auch 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 miteinander 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.

Audio für Spiele 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. Hierbei handelt es sich hoffentlich um ein vorübergehendes Problem, da die Anbieter intensiv an der Verbesserung ihrer jeweiligen Implementierungen arbeiten. Eine gute Testsuite für den Status des <audio>-Tags finden Sie 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:

  • Keine Möglichkeit, Filter auf das Tonsignal anzuwenden
  • Kein Zugriff auf die Roh-PCM-Daten
  • Keine Vorstellung von Position und Richtung von Quellen und Hörern
  • Keine detaillierte Zeitplanung.

Im restlichen Teil 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 dasselbe Sample kontinuierlich im Hintergrund wiedergegeben wird, kann es sich lohnen, den Track schrittweise auszublenden, um weitere 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.

Garagenband

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 eine Quelle für jeden Knoten und einen Verstärkungsknoten für jede Quelle 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);

Weitere Informationen zum Einbinden des <audio>-Tags in die Web Audio API finden Sie in diesem kurzen Artikel.

Soundeffekte

Spiele spielen oft Soundeffekte als Reaktion auf Nutzereingaben oder Änderungen des Spielstatus ab. Ä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 sie in der Warcraft-Reihe beim Klicken auf Einheiten zu hören sind.

Ein weiteres wichtiges Merkmal von Soundeffekten in Spielen ist, dass viele davon gleichzeitig vorkommen können. Stellen Sie sich vor, Sie sind in einer Schießerei mit mehreren Schauspielern, die Maschinengewehre schießen. Jedes Maschinengewehr schießt viele Male pro Sekunde, was dazu führt, dass gleichzeitig Dutzende von Soundeffekten abgespielt werden. Die wahre Stärke der Web Audio API ist die gleichzeitige Wiedergabe von Ton aus mehreren, genau abgestimmten Quellen.

Im folgenden Beispiel wird eine Maschinengewehrrunde aus mehreren einzelnen Aufzählungsbeispielen erstellt. Dazu werden mehrere Tonquellen erzeugt, deren Wiedergabe zeitlich gestaffelt 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 abfeuernden Geschossen
  2. Indem Sie die Wiedergaberate jedes Samples ändern (auch die 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-Sound

Spiele spielen oft in einer Welt mit einigen geometrischen Eigenschaften, entweder in 2D oder in 3D. In diesem Fall kann ein Audiomaterial in Stereoposition die Immersivität erhöhen. 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 ist ein einfaches Beispiel für die Verwendung von AudioPannerNode, um diese Art von Effekt zu erzielen. Die Grundidee des obigen Beispiels besteht darin, auf Mausbewegungen zu reagieren, indem die Position der Audioquelle wie folgt 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, daher 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 hoch entwickelt und basiert größtenteils auf OpenAL. Weitere Informationen finden Sie in den Abschnitten 3 und 4 der oben verlinkten Spezifikation.

Positionsmodell

An den Web Audio API-Kontext ist ein einzelner AudioListener angehängt, der über Position und Ausrichtung im Raum konfiguriert werden kann. Jede Quelle kann durch einen AudioPannerNode geleitet werden, der das Eingabeaudio räumlich ordnet. Der Panner-Knoten hat Position und Ausrichtung 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. In diesem Positionsbeispiel findest du ein Beispiel für räumlich in 3D räumlicher Klang. 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 finden Sie in dieser detaillierten Anleitung zum [Mischen von Positions-Audio 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 knarrende 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 den Unterschied zwischen dem Rohton und seinem tatsächlichen Klang ist also die Impulsreaktion. 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 letzte Countdown

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:

Abschneiden

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

Abschneiden

Es ist wichtig, auf harte Verzerrungen wie die oben gezeigte oder umgekehrt übermäßig gedämpfte Mixe zu hören, die die Zuhörer dazu zwingen, die Lautstärke zu erhöhen. 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 in Zukunft wahrscheinlich einen integrierten MeterNode Web Audio API-Knoten geben.

Übersteuern verhindern

Durch Anpassen der Verstärkung im Master AudioGainNode können Sie Ihren Mix auf einen Pegel reduzieren, der eine Begrenzung verhindert. 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 etwas 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. Durch direktes Zitieren der Spezifikation wird 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 Wiedergabe von Tönen vollständig von Ihnen und den anderen Teilnehmern abhängt. 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 auf Clipping und verhindere es, indem du einen Master-Gain-Knoten einfügst. Anschließend optimieren Sie den gesamten Mix mithilfe eines Dynamikkompressorknotens. Dein Audiodiagramm könnte in etwa so aussehen:

Endergebnis

Fazit

Das sind die meiner Meinung nach wichtigsten Aspekte der Audioentwicklung von Spielen mit der Web Audio API. Mit diesen Techniken können Sie direkt im Browser überzeugende Audioinhalte erstellen. Zum Abschluss noch ein browserspezifischer Tipp: Pausieren Sie den Ton, wenn Ihr Tab mithilfe der Page Visibility API in den Hintergrund wechselt. Andernfalls kann das für 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: