Erste Schritte mit Web Audio API

Boris Smus
Boris Smus

Bevor das HTML5-<audio>-Element verwendet wurde, war Flash oder ein anderes Plug-in erforderlich, um Websites ohne Unterbrechungen zu stören. Für den Ton im Web ist kein Plug-in mehr erforderlich. Das Audio-Tag bringt jedoch erhebliche Einschränkungen bei der Implementierung anspruchsvoller Spiele und interaktiver Anwendungen mit sich.

Die Web Audio API ist eine allgemeine JavaScript API für die Verarbeitung und Synthetisierung von Audio in Webanwendungen. Ziel dieser API ist es, die Funktionen moderner Spiele-Audio-Engines sowie einige der Mix-, Verarbeitungs- und Filteraufgaben in modernen Desktop-Audioproduktionsanwendungen einzubeziehen. Im Folgenden finden Sie eine Einführung in die Verwendung dieser leistungsstarken API.

Erste Schritte mit AudioContext

Mit AudioContext können alle Töne verwaltet und abgespielt werden. Um mit der Web Audio API einen Ton zu erzeugen, erstellen Sie eine oder mehrere Audioquellen und verbinden Sie sie mit dem von der AudioContext-Instanz bereitgestellten Tonziel. Diese Verbindung muss nicht direkt sein und kann eine beliebige Anzahl von AudioNodes durchlaufen, die als Verarbeitungsmodul für das Audiosignal dienen. Dieses Routing wird in der Web Audio-Spezifikation ausführlicher beschrieben.

Eine einzelne Instanz von AudioContext kann mehrere Toneingaben und komplexe Audiodiagramme unterstützen. Daher benötigen wir für jede erstellte Audioanwendung nur eine davon.

Mit dem folgenden Snippet wird ein AudioContext erstellt:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

Verwenden Sie für ältere WebKit-basierte Browser das Präfix webkit, wie bei webkitAudioContext.

Viele der interessanten Web Audio API-Funktionen wie das Erstellen von AudioNodes und das Decodieren von Audiodateidaten sind Methoden von AudioContext.

Töne werden geladen

Die Web Audio API verwendet für kurze bis mittellange Töne einen AudioBuffer. Der grundlegende Ansatz besteht darin, Audiodateien mit XMLHttpRequest abzurufen.

Die API unterstützt das Laden von Audiodateidaten in mehreren Formaten, z. B. WAV, MP3, AAC, OGG und andere. Die Browserunterstützung für verschiedene Audioformate variiert.

Mit dem folgenden Snippet wird das Laden eines Tonbeispiels veranschaulicht:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

Die Daten der Audiodatei sind binär (kein Text), daher legen wir responseType der Anfrage auf 'arraybuffer' fest. Weitere Informationen zu ArrayBuffers finden Sie in diesem Artikel zu XHR2.

Nachdem die (nicht decodierten) Audiodateidaten empfangen wurden, können sie zur späteren Decodierung aufbewahrt oder sofort mit der AudioContext-Methode decodeAudioData() decodiert werden. Bei dieser Methode wird die ArrayBuffer der in request.response gespeicherten Audiodateidaten asynchron decodiert. Der Hauptthread für die JavaScript-Ausführung wird dabei nicht blockiert.

Wenn decodeAudioData() abgeschlossen ist, wird eine Callback-Funktion aufgerufen, die die decodierten PCM-Audiodaten als AudioBuffer bereitstellt.

Ton abspielen

Ein einfaches Audiodiagramm
Ein einfaches Audiodiagramm

Sobald ein oder mehrere AudioBuffers geladen sind, können Töne abgespielt werden. Angenommen, Sie haben gerade ein AudioBuffer mit dem Bellen eines Hundes geladen und der Ladevorgang ist abgeschlossen. Dann können wir diesen Puffer mit dem folgenden Code abspielen.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Diese playSound()-Funktion kann jedes Mal aufgerufen werden, wenn ein Nutzer eine Taste drückt oder mit der Maus auf etwas klickt.

Mit der Funktion noteOn(time) ist es einfach, eine präzise Audiowiedergabe für Spiele und andere zeitkritische Anwendungen zu planen. Damit diese Planung ordnungsgemäß funktioniert, sollten Sie jedoch darauf achten, dass die Tonpuffer vorab geladen werden.

Web Audio API abstrahieren

Natürlich ist es besser, ein allgemeineres Ladesystem zu erstellen, das nicht hartcodiert ist, um diese Töne zu laden. Es gibt viele Ansätze für den Umgang mit den vielen kurzen bis mittellangen Tönen, die eine Audioanwendung oder ein Audiospiel verwenden würde. Hier ist ein Weg mit einem BufferLoader (nicht Teil des Webstandards).

Im Folgenden finden Sie ein Beispiel dafür, wie Sie die Klasse BufferLoader verwenden können. Erstellen wir zwei AudioBuffers. Sobald sie geladen sind, werden sie gleichzeitig abgespielt.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Mit der Zeit umgehen: Klänge mit Rhythmus abspielen

Mit der Web Audio API können Entwickler die Wiedergabe genau planen. Um dies zu demonstrieren, erstellen wir einen einfachen Rhythmus-Track. Das wahrscheinlich bekannteste Schlagzeugmuster ist wie folgt:

Einfaches Rock-Trommelmuster
Ein einfaches Trommelmuster mit Rock

bei dem jede Achtelnote ein Hihat und Kick und Snare abwechselnd im Viertel 4/4 gespielt werden.

Wenn die Zwischenspeicher kick, snare und hihat geladen sind, ist der Code dafür einfach:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

Hier wird nur eine Wiederholung anstelle der unbegrenzten Schleife in den Notenblättern. Die Funktion playSound ist eine Methode, die einen Puffer zu einer bestimmten Zeit wiedergibt:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Lautstärke eines Tons ändern

Eine der einfachsten Aktionen, die Sie an einem Ton vornehmen können, ist das Ändern der Lautstärke. Mit der Web Audio API können wir die Quelle über einen AudioGainNode an ihr Ziel weiterleiten, um die Lautstärke zu ändern:

Audiodiagramm mit Verstärkungsknoten
Audiodiagramm mit Verstärkungsknoten

Diese Verbindungseinrichtung können Sie so erreichen:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

Nachdem die Grafik eingerichtet wurde, können Sie die Lautstärke programmatisch ändern. Dazu bearbeiten Sie gainNode.gain.value so:

// Reduce the volume.
gainNode.gain.value = 0.5;

Überblenden zwischen zwei Tönen

Angenommen, wir haben ein etwas komplexeres Szenario, in dem wir mehrere Töne abspielen, aber zwischen ihnen überblenden möchten. Dies ist ein üblicher Fall bei einer DJ-Anwendung, bei der wir zwei Plattenspieler haben und in der Lage sein möchten, von einer Tonquelle zur anderen zu schwenken.

Verwenden Sie dazu das folgende Audiodiagramm:

Audiodiagramm mit zwei Quellen, die über Verstärkungsknoten verbunden sind
Audiodiagramm mit zwei Quellen, die über Verstärkungsknoten verbunden sind

Um dies einzurichten, erstellen wir einfach zwei AudioGainNodes und verbinden jede Quelle über die Knoten, etwa mit der folgenden Funktion:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Gleichmäßige Überblendung der Potenz

Bei einem naiven linearen Überblendungsansatz kommt es beim Schwenken zwischen den Proben zu einem Lautstärkeabfall.

Eine lineare Überblendung
Lineare Überblendung

Um dieses Problem zu umgehen, verwenden wir eine gleichmäßige Powerkurve, bei der die entsprechenden Verstärkungskurven nicht linear sind und sich mit einer höheren Amplitude schneiden. Dadurch werden Lautstärkeeinbrüche zwischen Audioregionen minimiert, was zu einer gleichmäßigeren Überblendung zwischen Regionen führt, deren Pegel geringfügig unterschiedliche Pegel haben.

Eine gleichmäßige Überblendung der Leistung.
Gleiche Potenzüberblendung

Überblenden in Playlists

Eine weitere gängige Crossfader-Anwendung ist ein Musikplayer. Wenn sich ein Song ändert, möchten wir den aktuellen Track ausblenden und den neuen ein- und ausblenden, um einen abrupten Übergang zu vermeiden. Planen Sie dazu eine Überblendung in die Zukunft. Für diese Planung könnten wir zwar setTimeout verwenden, dies ist jedoch nicht präzise. Mit der Web Audio API können wir die AudioParam-Schnittstelle verwenden, um zukünftige Werte für Parameter wie den Verstärkungswert einer AudioGainNode zu planen.

Bei einer Playlist können wir also zwischen den Tracks wechseln, indem wir eine Verstärkung für den aktuell wiedergegebenen Titel und eine Verstärkungszunahme für den nächsten Titel planen, und zwar kurz vor dem Ende des aktuellen Titels:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

Die Web Audio API bietet eine Reihe praktischer RampToValue-Methoden, mit denen sich der Wert eines Parameters schrittweise ändern lässt, z. B. linearRampToValueAtTime oder exponentialRampToValueAtTime.

Die Funktion zum Zeitpunkt des Übergangs kann aus integrierten linearen und exponentiellen Einsen (wie oben) ausgewählt werden. Sie können aber auch eine eigene Wertkurve über ein Wertearray mit der Funktion setValueCurveAtTime angeben.

Einen einfachen Filtereffekt auf einen Ton anwenden

Audiodiagramm mit einem BiquadFilterNode
Audiodiagramm mit einem BiquadFilterNode

Mit der Web Audio API kannst du Ton von einem Audioknoten auf einen anderen übertragen und so eine potenziell komplexe Prozessorkette schaffen, um deinen Soundformen komplexe Effekte hinzuzufügen.

Eine Möglichkeit besteht darin, BiquadFilterNodes zwischen der Audioquelle und dem Ziel zu platzieren. Diese Art von Audioknoten kann eine Vielzahl von Filtern niedrigerer Reihenfolge ausführen, mit denen grafische Equalizer und noch komplexere Effekte erstellt werden können. In erster Linie geht es dabei um die Auswahl der Teile des Frequenzspektrums eines Tons, die betont und welche abgeschwächt werden sollen.

Folgende Filtertypen werden unterstützt:

  • Tiefpassfilter
  • Hochpassfilter
  • Band pass filter
  • Low-Shelf-Filter
  • High-Shelf-Filter
  • Filter „Höchstleistung“
  • Notch-Filter
  • All pass filter

Alle Filter enthalten Parameter zum Festlegen eines Verstärkungsgrads, die Häufigkeit, mit der der Filter angewendet werden soll, und einen Qualitätsfaktor. Der Tiefpassfilter behält den unteren Frequenzbereich bei, verwirft jedoch hohe Frequenzen. Der Haltepunkt wird durch den Häufigkeitswert bestimmt. Der Q-Faktor ist keine Einheit und bestimmt die Form der Grafik. Die Verstärkung wirkt sich nur auf bestimmte Filter wie den Low-Shelf-Filter und den Spitzenfilter aus, nicht auf diesen Lowpassfilter.

Lassen Sie uns einen einfachen Tiefpassfilter einrichten, um nur die Basen aus einem Tonbeispiel zu extrahieren:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

Im Allgemeinen müssen Frequenzkontrollen optimiert werden, um auf einer logarithmischen Skala zu funktionieren, da das menschliche Hören selbst nach dem gleichen Prinzip funktioniert (d. h. A4 ist 440 Hz und A5 ist 880 Hz). Weitere Informationen finden Sie in der Funktion FilterSample.changeFrequency im Quellcodelink oben.

Außerdem können Sie mit dem Beispielcode den Filter verbinden und trennen und so das AudioContext-Diagramm dynamisch ändern. Wir können AudioNodes durch Aufrufen von node.disconnect(outputNumber) vom Diagramm trennen. Um die Grafik beispielsweise von einem Filter in eine direkte Verbindung umzuleiten, können Sie Folgendes tun:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Weitere Zuhören

Wir haben die Grundlagen der API besprochen, einschließlich des Ladens und Abspielens von Audiobeispielen. Wir haben Audiodiagramme mit Verstärkungsknoten und Filtern sowie geplanten Optimierungen von Tönen und Audioparametern erstellt, um einige gängige Soundeffekte zu ermöglichen. Jetzt sind Sie bereit, ein paar tolle Web-Audio-Anwendungen zu erstellen.

Viele Entwickler haben mit der Web Audio API bereits großartige Arbeit erarbeitet. Zu meinen Favoriten gehören:

  • AudioJedit, ein im Browser integriertes Tool zum Aufteilen von Geräuschen, das SoundCloud-Permalinks verwendet.
  • ToneCraft, ein Sound-Sequencer, bei dem Klänge durch das Stapeln von 3D-Blöcken erzeugt werden.
  • Plink, ein Spiel zum gemeinsamen Musikmachen mit Web Audio und WebSockets