Audio für Spiele mit der Web Audio API entwickeln

Boris Smus
Boris Smus

Einleitung

Audio spielt eine große Rolle, warum Multimedia-Erlebnisse so fesselnd werden. Wenn du schon mal einen Film ohne Ton abgespielt hast, ist dir das sicher schon aufgefallen.

Spiele sind da keine Ausnahme. Ich erinnere mich am schönsten an die Musik und die Soundeffekte. In vielen Fällen, fast zwei Jahrzehnte, nachdem ich meine Lieblingssongs gespielt habe, kann ich die Zelda-Kompositionen von Koji Kondo und den atmosphärischen Diablo-Soundtrack von Matt Uelmen immer noch nicht aus dem Kopf bekommen. Dasselbe gilt für Soundeffekte, z. B. die sofort erkennbaren Klickantworten auf Einheiten von Warcraft und Samples aus den Klassikern von Nintendo.

Der Ton eines Spiels ist mit einigen interessanten Herausforderungen verbunden. Um überzeugende Spielmusik zu erstellen, müssen sich Designer an einen möglicherweise unvorhersehbaren Spielstatus anpassen, in dem sich der Spieler befindet. In der Praxis können Teile des Spiels über eine unbekannte Dauer laufen, Töne können mit der Umgebung interagieren und sich auf komplexe Weise vermischen, wie z. B. Raumeffekte und relative Klangpositionierung. Schließlich kann eine große Anzahl von Klängen gleichzeitig abgespielt werden, die alle gut klingen und ohne Leistungseinbußen gerendert werden müssen.

Audio für Spiele im Web

Für einfache Spiele kann die Verwendung des Tags <audio> ausreichend sein. Viele Browser bieten jedoch eine schlechte Implementierung, was zu Audiofehlern und einer hohen Latenz führen kann. Dies ist hoffentlich ein vorübergehendes Problem, da die Anbieter hart an der Verbesserung ihrer jeweiligen Implementierungen arbeiten. Unter areweplayingyet.org gibt es eine praktische Testsuite, um einen Einblick in den Status des <audio>-Tags zu erhalten.

Bei genauerer Betrachtung der <audio>-Tag-Spezifikation wird jedoch deutlich, dass es viele Dinge gibt, die damit nicht möglich sind. Daher ist es nicht verwunderlich, dass es für die Medienwiedergabe entwickelt wurde. Zu den Einschränkungen gehören:

  • Keine Möglichkeit, Filter auf das Tonsignal anzuwenden
  • Keine Möglichkeit, auf die PCM-Rohdaten zuzugreifen
  • Keine Vorstellung von Position und Richtung der Quellen und Zuhörer
  • Kein genaues Timing.

Im Rest des Artikels gehe ich auf einige dieser Themen im Kontext der mit der Web Audio API geschriebenen Audioinhalte von Spielen ein. Eine kurze Einführung in diese API finden Sie im Startleitfaden.

Hintergrundmusik

Bei Spielen wird die Hintergrundmusik oft in einer Endlosschleife abgespielt.

Es kann sehr lästig werden, wenn die Schleife kurz und vorhersehbar ist. Wenn ein Spieler in einem Bereich oder Level feststeckt und dasselbe Sample kontinuierlich im Hintergrund abgespielt wird, kann es sich lohnen, den Track nach und nach auszublenden, um weitere Frustrationen zu vermeiden. Eine andere Strategie besteht darin, Mischungen unterschiedlicher Intensität zu verwenden, die sich je nach Kontext des Spiels schrittweise ineinander übergehen.

Wenn sich Ihr Spieler beispielsweise in einer Zone mit einem epischen Bosskampf befindet, haben Sie möglicherweise mehrere Mixe, die im emotionalen Bereich von atmosphärisch über schattenhaft bis heftig sind. Mit Musiksynthesesoftware kannst du oft mehrere Mixe (gleicher Länge) basierend auf einem Stück exportieren. Dazu musst du nur den Satz von Tracks auswählen, der für den Export verwendet werden soll. Auf diese Weise sorgen Sie für eine gewisse interne Konsistenz und vermeiden irritierende Übergänge beim Überblenden von einem Track zum anderen.

Garagenband

Anschließend kannst du alle diese Beispiele mithilfe der Web Audio API mithilfe der BufferLoader-Klasse über XHR importieren. Dies wird im einführenden Artikel zur Web Audio API ausführlich behandelt. Das Laden von Tönen nimmt Zeit in Anspruch. Daher sollten die im Spiel verwendeten Assets beim Seitenaufbau, zu Beginn des Levels oder möglicherweise auch 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 wiedergeben. Da sie alle die gleiche Länge haben, garantiert die Web Audio API, dass sie ausgerichtet bleiben. Wenn sich der Charakter dem endgültigen Bosskampf nähert oder weiter entfernt, kann das Spiel die Verstärkungswerte für jeden der entsprechenden Knoten in der Kette variieren. Dazu wird ein Algorithmus für die Gewinnmenge wie der folgende verwendet:

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

Beim obigen Ansatz werden zwei Quellen gleichzeitig wiedergegeben. Die Überblendung zwischen ihnen erfolgt mithilfe gleichmäßiger Potenzkurven (wie in der Einführung beschrieben).

Viele Spieleentwickler verwenden heute das <audio>-Tag für ihre Hintergrundmusik, da es sich gut zum Streamen von Inhalten eignet. Jetzt kannst du 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 auf den gesamten Download warten zu müssen. Wenn du den Stream in die Web Audio API einbindest, kannst du ihn bearbeiten oder analysieren. Im folgenden Beispiel wird ein Tiefpassfilter auf die Musik angewendet, die über das Tag <audio> 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 Diskussion zum Einbinden des <audio>-Tags in die Web Audio API finden Sie in diesem kurzen Artikel.

Soundeffekte

Spiele geben oft Soundeffekte als Reaktion auf Nutzereingaben oder Änderungen des Spielstatus wieder. Wie bei Hintergrundmusik können Soundeffekte aber sehr schnell lästig werden. Um dies zu vermeiden, ist es oft nützlich, einen Pool ähnlicher, aber unterschiedlicher Töne zu haben. Dies kann von leichten Varianten von Fußgängen bis hin zu drastischen Abweichungen variieren, wie sie in der Warcraft-Serie als Reaktion auf das Klicken auf Einheiten dargestellt werden.

Ein weiteres wichtiges Merkmal von Soundeffekten in Spielen ist, dass es viele davon gleichzeitig geben kann. Stellen Sie sich vor, Sie stehen mitten in einer Schießerei, in der mehrere Schauspieler Maschinengewehre abfeuern. Jedes Maschinengewehr wird mehrmals pro Sekunde abgefeuert, wodurch Dutzende von Soundeffekten gleichzeitig abgespielt werden. Die Web Audio API glänzt in der Musikwiedergabe aus mehreren, genau zeitgesteuerten Quellen gleichzeitig.

Im folgenden Beispiel wird eine Maschinengewehrrunde aus mehreren einzelnen Aufzählungszeichen erstellt, indem mehrere Tonquellen erstellt werden, 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 klingen, wäre das ziemlich langweilig. Natürlich variieren sie je nach Entfernung vom Ziel und der relativen Position (mehr dazu später), aber selbst das reicht unter Umständen nicht aus. Die Web Audio API bietet eine einfache Möglichkeit, das Beispiel oben auf zwei Arten zu optimieren:

  1. Durch einen dezenten zeitlichen Abstand zwischen den Geschossen
  2. Durch Ändern der „Wiedergaberate“-Rate jeder Stichprobe (auch mit Veränderung der Tonhöhe), um die Zufälligkeit einer realen Welt besser zu simulieren

Ein Praxisbeispiel für diese Techniken in Aktion findest du in der Billard-Table-Demo. Hier werden zufällige Stichproben verwendet und die Wiedergaberate variiert, um ein interessanteres Ballkollisionsgeräusch zu erzeugen.

3D-Positionsgeräusche

Spiele spielen oft in einer Welt mit einigen geometrischen Eigenschaften, entweder in 2D oder in 3D. In diesem Fall kann der Stereo-Positionston die Eindrücke des Erlebnisses erheblich erhöhen. Glücklicherweise verfügt die Web Audio API über integrierte hardwarebeschleunigte Positionsaudiofunktionen, die sehr einfach zu verwenden sind. Übrigens solltest du prüfen, ob du über Stereolautsprecher (am besten Kopfhörer) verfügst, damit das folgende Beispiel Sinn ergibt.

Im obigen Beispiel befindet sich in der Mitte des Canvas ein Listener (Personensymbol) und die Maus beeinflusst die Position der Quelle (Sprechersymbol). Oben sehen Sie ein einfaches Beispiel für die Verwendung von AudioPannerNode, um einen solchen Effekt zu erzielen. Die Grundidee des obigen Beispiels besteht darin, durch Festlegen der Position der Audioquelle auf Mausbewegungen zu reagieren:

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 Verräumung bei Web Audio:

  • Der Listener befindet sich standardmäßig im Ursprung (0, 0, 0).
  • Positionsbezogene Web Audio APIs haben keine Einheiten, deshalb habe ich einen Multiplikator eingeführt, um die Demo besser zu klingen.
  • Web Audio verwendet die kartesischen Koordinaten (y-is-up) (das Gegenteil der meisten Computergrafiksysteme). Deshalb tausche ich die Y-Achse im Snippet oben

Fortgeschritten: Tonkegel

Das Positionsmodell ist sehr leistungsstark und sehr fortschrittlich 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 über einen AudioPannerNode übergeben werden, der die Eingabeaudios räumlich verarbeitet. Der Schwenkknoten hat Position und Ausrichtung sowie ein Entfernungs- und Richtungsmodell.

Das Entfernungsmodell gibt die Verstärkung abhängig von der Nähe zur Quelle an. Das Richtungsmodell kann durch Angabe eines inneren und äußeren Kegels konfiguriert werden. Diese bestimmen die (in der Regel negative) Verstärkung, wenn sich der Listener innerhalb des inneren und äußeren Kegels 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 in 3D räumlich dargestellten Klang findest du in diesem Positionsbeispiel. Zusätzlich zur Position enthält das Web Audio-Tonmodell optional auch die Geschwindigkeit für Dopplerverschiebungen. Dieses Beispiel zeigt den Doppler-Effekt genauer.

Weitere Informationen zu diesem Thema finden Sie in dieser detaillierten Anleitung 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. Eine knackende Tür klingt im Keller ganz anders als in einem großen, offenen Saal. Spiele mit einem hohen Produktionswert sollten diese Effekte imitieren, da das Erstellen eines separaten Satzes von Beispielen für jede Umgebung unerschwinglich ist und zu noch mehr Assets und einer größeren Menge an Spieldaten führen würde.

Einfach gesagt ist der Audiobegriff für den Unterschied zwischen dem Rohklang und der Art, wie er in der Realität klingt, die Impulsreaktion. Diese Impulsreaktionen können sorgfältig aufgezeichnet werden, und tatsächlich gibt es Websites, die viele dieser vorab aufgezeichneten (als Audio gespeicherten) Dateien für Impulsantworten hosten.

Weitere Informationen zum Erstellen von Impulsreaktionen aus einer bestimmten Umgebung finden Sie im Abschnitt zur Aufnahmeeinrichtung im Abschnitt Faltung der Web Audio API-Spezifikation.

Für unsere Zwecke bietet die Web Audio API eine einfache Möglichkeit, diese Impulsreaktionen mithilfe des ConvolverNode auf 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 der Raumeffekte auf der Seite mit den Web Audio API-Spezifikationen sowie dieses Beispiel an, mit dem Sie die Trocken- (Roh-) und Nass-Mischung (durch Convolver verarbeitet) eines großartigen Jazz-Standards steuern können.

Der letzte Countdown

Sie haben also ein Spiel erstellt und Positionsaudio konfiguriert. In der Grafik befindet sich jetzt eine große Anzahl von Audioknoten, die alle gleichzeitig wiedergegeben werden. Super, aber du solltest noch einen weiteren Punkt beachten:

Da sich mehrere Töne ohne Normalisierung einfach übereinander stapeln, kann es vorkommen, dass Sie den Schwellenwert Ihrer Lautsprecherkapazität überschreiten. Wie bei Bildern, die über ihre Canvas-Grenzen hinausgehen, können auch Töne abgeschnitten werden, wenn die Wellenform den maximalen Schwellenwert überschreitet, was zu einer deutlichen Verzerrung führt. Die Wellenform sieht in etwa so aus:

Abschneidung

Hier ist ein echtes Beispiel für Clipping in Aktion. Die Wellenform sieht schlecht aus:

Abschneidung

Es ist wichtig, bei starken Verzerrungen wie der oben gezeigten zu hören und umgekehrt auf übermäßig gedämpfte Mixe, die Ihre Zuhörer dazu zwingen, die Lautstärke zu erhöhen. Wenn du in dieser Situation bist, musst du sie wirklich beheben.

Clipping erkennen

Aus technischer Sicht erfolgt eine Begrenzung, wenn der Wert des Signals in einem beliebigen Kanal den gültigen Bereich zwischen -1 und 1 überschreitet. Sobald dies erkannt wurde, ist es hilfreich, visuelles Feedback zu geben. Um dies zuverlässig zu erreichen, fügen Sie einen JavaScriptAudioNode in Ihre Grafik ein. Das Audiodiagramm würde so aussehen:

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

Im folgenden processAudio-Handler wurde Abschneidung erkannt:

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

Achten Sie im Allgemeinen darauf, den JavaScriptAudioNode nicht zu verwenden, um die Leistung zu steigern. In diesem Fall könnte eine alternative Messmethode einen RealtimeAnalyserNode im Audiodiagramm für getByteFrequencyData zur Renderingzeit abfragen, wie durch requestAnimationFrame bestimmt. Dieser Ansatz ist effizienter, es fehlt jedoch der größte Teil des Signals (einschließlich Stellen, an denen es möglicherweise Clips erstellt), da das Rendering höchstens 60-mal pro Sekunde stattfindet, während sich das Audiosignal viel schneller ändert.

Da die Cliperkennung so wichtig ist, ist es wahrscheinlich, dass wir in Zukunft einen integrierten MeterNode Web Audio API-Knoten sehen werden.

Clipping verhindern

Indem Sie die Verstärkung des Master-AudioGainNode anpassen, können Sie Ihren Mix auf ein Maß reduzieren, das Überschneidungen verhindert. Da die Töne in Ihrem Spiel jedoch von einer Vielzahl von Faktoren abhängen können, kann es in der Praxis schwierig sein, den Master-Verstärkungswert zu bestimmen, der eine Begrenzung für alle Zustände verhindert. Generell sollten Sie die Gewinne optimieren, um den schlimmsten Fall voraussichtlich zu erkennen, aber dies ist mehr eine Kunst als eine Wissenschaft.

Etwas Zucker hinzufügen

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 der Web Audio-Welt über DynamicsCompressorNode verfügbar, das in dein Audiodiagramm eingefügt werden kann, um einen lauteren, satteren und volleren Klang zu erzeugen und Clips zu optimieren. Durch direktes Zitieren der Spezifikation wird dieser Knoten

Die Verwendung der dynamischen 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 abgespielten Töne vollständig von Ihnen und den anderen Teilnehmern abhängen. Ein Kompressor ist in den meisten Fällen nützlich, außer in einigen seltenen Fällen, in denen es mühsam gemasterte Tracks gibt, die bereits so gestimmt sind, dass sie genau richtig klingen.

Zur Implementierung muss einfach ein DynamicsCompressorNode in den Audiodiagramm eingefügt werden, in der Regel als letzter 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 lässt sich sagen, dass Sie genau auf Clipping achten und verhindern, dass es zu Problemen kommt, indem Sie einen Master-Verstärkungsknoten einsetzen. Anschließend wird der gesamte Mix mithilfe eines dynamischen Kompressorknotens straffer. Ihr Audiodiagramm könnte in etwa so aussehen:

Endergebnis

Fazit

Das waren die wichtigsten Aspekte der Audioentwicklung für Spiele mit der Web Audio API. Mit diesen Techniken können Sie direkt in Ihrem Browser beeindruckende Audioerlebnisse schaffen. Bevor ich mich verabschiede, möchte ich Ihnen noch einen Tipp geben: Halten Sie den Ton an, wenn Ihr Tab mit der Page visibility API in den Hintergrund verschoben wird. Andernfalls könnte die Nutzung für die Nutzer frustrierend sein.

Weitere Informationen zu Web Audio finden Sie im Artikel zu den ersten Schritten. Wenn Sie Fragen haben, finden Sie in den FAQs zu Web Audio vielleicht eine Antwort. Wenn Sie weitere Fragen haben, können Sie diese auf Stack Overflow mit dem Tag web-audio stellen.

Bevor ich mich verabschiede, möchte ich Ihnen noch ein paar tolle Funktionen für die Verwendung der Web Audio API in echten Spielen vorstellen: