Erste Schritte mit Web Audio API

Boris Smus
Boris Smus

Vor dem HTML5 <audio> Element war Flash oder ein anderes Plug-in erforderlich um Audioinhalte im Web wiederzugeben. Zwar ist für Audioinhalte im Web kein Plug-in mehr erforderlich, das Audio-Tag bringt jedoch erhebliche Einschränkungen für die Implementierung komplexer Spiele und interaktiver Anwendungen mit sich.

Die Web Audio API ist eine JavaScript API auf hoher Ebene zum Verarbeiten und Synthetisieren von Audio in Webanwendungen. Ziel dieser API ist es, Funktionen moderner Game-Audio-Engines und einige der Misch-, Verarbeitungs- und Filteraufgaben zu integrieren, die in modernen Desktop-Anwendungen für die Audioproduktion zu finden sind. Im Folgenden finden Sie eine Einführung in die Verwendung dieser leistungsstarken API.

Erste Schritte mit AudioContext

Ein AudioContext dient zum Verwalten und Abspielen aller Sounds. Um mit der Web Audio API einen Sound zu erzeugen, erstellen Sie eine oder mehrere Soundquellen und verbinden Sie sie mit dem Soundziel, das von der AudioContext-Instanz bereitgestellt wird. Diese Verbindung muss nicht direkt sein, sondern kann über eine beliebige Anzahl von zwischengeschalteten AudioNodes erfolgen, die als Verarbeitungs module für das Audiosignal dienen. Diese Weiterleitung wird in der Web Audio-Spezifikation ausführlicher beschrieben.

Eine einzelne Instanz von AudioContext kann mehrere Audioeingaben und komplexe Audiografiken unterstützen. Daher benötigen wir nur eine für jede Audioanwendung, die wir erstellen.

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 webkit Präfix, wie bei webkitAudioContext.

Viele der interessanten Funktionen der Web Audio API, z. B. das Erstellen von AudioNodes und das Decodieren von Audiodateien, sind Methoden von AudioContext.

Sounds laden

Die Web Audio API verwendet einen AudioBuffer für kurze bis mittellange Sounds. Der grundlegende Ansatz besteht darin, XMLHttpRequest zum Abrufen von Sounddateien zu verwenden.

Die API unterstützt das Laden von Audiodateien in verschiedenen Formaten wie WAV, MP3, AAC und OGG sowie anderen. Die Browserunterstützung für verschiedene Audioformate variiert.

Das folgende Snippet zeigt das Laden eines Soundbeispiels:

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 Audiodateien sind binär (nicht Text). Daher legen wir die responseType der Anfrage auf 'arraybuffer' fest. Weitere Informationen zu ArrayBuffers finden Sie in diesem Artikel zu XHR2.

Sobald die (nicht decodierten) Audiodateien empfangen wurden, können sie für die spätere Decodierung aufbewahrt oder sofort mit der AudioContext-Methode decodeAudioData() decodiert werden. Diese Methode verwendet den ArrayBuffer der Audiodateien, der in request.response gespeichert ist, und decodiert ihn asynchron (ohne den Haupt-JavaScript-Ausführungsthread zu blockieren).

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

Sounds abspielen

Sobald ein oder mehrere AudioBuffers geladen sind, können wir Sounds abspielen. Angenommen, wir haben gerade einen AudioBuffer mit dem Geräusch eines bellenden Hundes geladen und das Laden 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 jemand eine Taste drückt oder mit der Maus auf etwas klickt.

Mit der Funktion noteOn(time) können Sie die genaue Soundwiedergabe für Spiele und andere zeitkritische Anwendungen planen. Damit diese Planung ordnungsgemäß funktioniert, müssen Ihre Soundpuffer jedoch vorab geladen werden.

Web Audio API abstrahieren

Natürlich wäre es besser, ein allgemeineres Ladesystem zu erstellen, das nicht fest auf das Laden dieses bestimmten Sounds codiert ist. Es gibt viele Ansätze für den Umgang mit den vielen kurzen bis mittellangen Sounds, die in einer Audioanwendung oder einem Spiel verwendet werden. Hier ist eine Möglichkeit mit einem BufferLoader (nicht Teil des Webstandards).

Im Folgenden finden Sie ein Beispiel für die Verwendung der Klasse BufferLoader. Erstellen wir zwei AudioBuffers und spielen wir sie gleichzeitig ab, sobald sie geladen sind.

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

Umgang mit der Zeit: Sounds mit Rhythmus abspielen

Mit der Web Audio API können Entwickler die Wiedergabe genau planen. Um dies zu veranschaulichen, richten wir einen einfachen Rhythmus-Track ein. Das wohl bekannteste Drumkit-Muster ist das folgende:

Ein einfaches Rock-Drum-Muster

Dabei wird eine Hi-Hat bei jeder Achtelnote gespielt und Kick und Snare werden abwechselnd bei jeder Viertelnote im 4/4-Takt gespielt.

Angenommen, wir haben die Puffer kick, snare und hihat geladen. Der Code dafür ist 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 Noten verwendet. Die Funktion playSound ist eine Methode, mit der ein Puffer zu einem bestimmten Zeitpunkt wiedergegeben wird:

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

Lautstärke eines Sounds ändern

Eine der grundlegendsten Operationen, die Sie mit einem Sound ausführen möchten, ist das Ändern der Lautstärke. Mit der Web Audio API können wir unsere Quelle an das Ziel über einen AudioGainNode weiterleiten, um die Lautstärke zu ändern:

Einfaches Flussdiagramm, das zeigt, wie Audio von der Quelle über den GainNode zum Ziel fließt.

Diese Verbindungseinrichtung kann so erfolgen:

// 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 das Diagramm eingerichtet wurde, können Sie die Lautstärke programmatisch ändern, indem Sie gainNode.gain.value wie folgt bearbeiten:

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

Überblenden zwischen zwei Sounds

Angenommen, wir haben ein etwas komplexeres Szenario, in dem wir mehrere Sounds abspielen, aber zwischen ihnen überblenden möchten. Dies ist ein häufiger Fall in einer DJ-ähnlichen Anwendung, in der wir zwei Plattenspieler haben und von einer Soundquelle zur anderen wechseln möchten.

Dies kann mit dem folgenden Audiografen erfolgen:

Audio-Diagramm mit zwei Quellen, die mit separaten Verstärkungsknoten verbunden sind, die dann zum selben Ziel weitergeleitet werden.

Dazu erstellen wir einfach zwei AudioGainNodes und verbinden jede Quelle über die Knoten, etwa mit dieser 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
    };
}

Überblenden mit gleicher Leistung

Ein naiver linearer Überblendungsansatz weist einen Lautstärkeabfall auf, wenn Sie zwischen den Samples wechseln.

Ein linearer Crossfade, der als zwei sich ergänzende gerade Linien dargestellt wird, die als Amplitudenpegel im Zeitverlauf abgebildet werden.
Eine lineare Überblendung

Um dieses Problem zu beheben, verwenden wir eine Kurve mit gleicher Leistung, bei der die entsprechenden Verstärkungskurven nicht linear sind und sich bei einer höheren Amplitude schneiden. Dadurch werden Lautstärkeabfälle zwischen Audiobereichen minimiert, was zu einer gleichmäßigeren Überblendung zwischen Bereichen führt, die sich im Pegel leicht unterscheiden können.

Ein Crossfade mit gleicher Leistung, dargestellt als zwei gekrümmte Linien, die als Amplitudenpegel über die Zeit abgebildet werden.
Eine Überblendung mit gleicher Leistung

Überblenden von Playlists

Eine weitere häufige Anwendung für Überblenden ist eine Musikplayer-Anwendung. Wenn ein Song wechselt, möchten wir den aktuellen Titel ausblenden und den neuen einblenden, um einen abrupten Übergang zu vermeiden. Planen Sie dazu eine Überblendung in der Zukunft. Wir könnten setTimeout verwenden, um dies zu planen, aber das ist nicht genau. Mit der Web Audio API können wir die AudioParam-Schnittstelle verwenden, um zukünftige Werte für Parameter wie den Verstärkungswert eines AudioGainNode zu planen.

So können wir bei einer Playlist zwischen Titeln wechseln, indem wir eine Verringerung der Verstärkung für den aktuell wiedergegebenen Titel und eine Erhöhung der Verstärkung für den nächsten Titel planen, und zwar kurz bevor der aktuelle Titel zu Ende ist:

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 praktische Reihe von RampToValue Methoden, um den Wert eines Parameters schrittweise zu ändern, z. B. linearRampToValueAtTime und exponentialRampToValueAtTime.

Die Funktion für die Übergangszeit kann aus integrierten linearen und exponentiellen Funktionen ausgewählt werden (wie oben). Sie können aber auch eine eigene Wertkurve über ein Array von Werten mit der Funktion setValueCurveAtTime angeben.

Einen einfachen Filtereffekt auf einen Sound anwenden

Einfaches Flussdiagramm, das zeigt, wie Audio von der Quelle über den BiquadFilterNode zum Ziel fließt.

Mit der Web Audio API können Sie Sound von einem Audio-Node zu einem anderen weiterleiten und so eine potenziell komplexe Kette von Prozessoren erstellen, um Ihren Soundformen komplexe Effekte hinzuzufügen.

Eine Möglichkeit dazu besteht darin, BiquadFilterNodes zwischen Sound quelle und -ziel zu platzieren. Dieser Audioknotentyp kann eine Vielzahl von Filtern niedriger Ordnung ausführen, mit denen grafische Equalizer und noch komplexere Effekte erstellt werden können. Dabei geht es hauptsächlich darum, welche Teile des Frequenzspektrums eines Sounds hervorgehoben und welche unterdrückt werden sollen.

Unterstützte Filtertypen:

  • Tiefpassfilter
  • Hochpassfilter
  • Band pass filter
  • Tiefenfilter
  • Höhenfilter
  • Peak-Filter
  • Kerbfilter
  • All pass filter

Alle Filter enthalten Parameter, mit denen eine bestimmte Verstärkung, die Frequenz, bei der der Filter angewendet werden soll, und ein Qualitätsfaktor angegeben werden können. Der Tiefpassfilter behält den niedrigeren Frequenzbereich bei, verwirft aber hohe Frequenzen. Der Trennpunkt wird durch den Frequenzwert bestimmt, und der Q-Faktor ist einheitenlos und bestimmt die Form des Diagramms. Die Verstärkung wirkt sich nur auf bestimmte Filter aus, z. B. Tiefen- und Peak-Filter, nicht aber auf diesen Tiefpassfilter.

Richten wir einen einfachen Tiefpassfilter ein, um nur die Bässe aus einem Soundbeispiel 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 Frequenzregler so angepasst werden, dass sie auf einer logarithmischen Skala funktionieren, da das menschliche Gehör selbst nach demselben Prinzip funktioniert (d. h. A4 ist 440 Hz und A5 ist 880 Hz). Weitere Informationen finden Sie in der Funktion FilterSample.changeFrequency im Quellcode-Link oben.

Beachten Sie schließlich, dass Sie mit dem Beispielcode den Filter verbinden und trennen und so das AudioContext-Diagramm dynamisch ändern können. Wir können AudioNodes aus dem Diagramm trennen, indem wir node.disconnect(outputNumber) aufrufen. Um beispielsweise die Weiterleitung des Diagramms von einem Filter zu einer direkten Verbindung zu ändern, können wir Folgendes tun:

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

Weitere Informationen

Wir haben die Grundlagen der API behandelt, einschließlich des Ladens und Abspielens von Audiobeispielen. Wir haben Audiografiken mit Verstärkungsknoten und Filtern erstellt und Sounds und Audioparameter-Anpassungen geplant, um einige gängige Soundeffekte zu ermöglichen. Jetzt können Sie loslegen und einige tolle Web-Audioanwendungen erstellen.

Wenn Sie Inspiration suchen, haben viele Entwickler bereits großartige Arbeit mit der Web Audio API geleistet. Einige meiner Favoriten sind:

  • AudioJedit, ein Tool zum Schneiden von Sounds im Browser, das SoundCloud-Permalinks verwendet.
  • ToneCraft, ein Soundsequenzer, bei dem Sounds durch Stapeln von 3D-Blöcken erstellt werden.
  • Plink, ein kollaboratives Spiel zur Musikproduktion mit Web Audio und Web Sockets.