Erste Schritte mit Web Audio API

Boris Smus
Boris Smus

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

Die Web Audio API ist eine hochrangige JavaScript API zur Verarbeitung und Synthese von Audio in Webanwendungen. Ziel dieser API ist es, Funktionen moderner Audio-Engines für Spiele und einige der Funktionen zum Mischen, Bearbeiten und Filtern von Audioinhalten in modernen Desktop-Anwendungen zur Audioproduktion zu integrieren. Im Folgenden finden Sie eine kurze Einführung in die Verwendung dieser leistungsstarken API.

Mit einem AudioContext können alle Töne verwaltet und abgespielt werden. Wenn Sie mit der Web Audio API einen Ton erzeugen möchten, erstellen Sie eine oder mehrere Tonquellen und verbinden Sie sie mit dem Tonziel, das von der AudioContext-Instanz bereitgestellt wird. Diese Verbindung muss nicht direkt sein und kann beliebig viele Zwischen-AudioNodes durchlaufen, die als Verarbeitungsmodule für das Audiosignal dienen. Dieses Routing wird in der Web Audio-Spezifikation ausführlicher beschrieben.

Eine einzelne Instanz von AudioContext kann mehrere Audioeingaben und komplexe Audio-Grafiken unterstützen. Wir benötigen also nur eine davon für jede Audioanwendung, die wir erstellen.

Im 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 Funktionen der Web Audio API, z. B. das Erstellen von Audioknoten und das Decodieren von Audiodateidaten, sind Methoden von AudioContext.

Ladetöne

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

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

Im folgenden Snippet wird das Laden eines Audiosamples 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 Audiodateidaten sind binär (kein Text). Daher setzen wir die responseType der Anfrage auf 'arraybuffer'. Weitere Informationen zu ArrayBuffers finden Sie in diesem Artikel über XHR2.

Nachdem die (nicht decodierten) Audiodateidaten empfangen wurden, können sie zur späteren Decodierung gespeichert oder sofort mit der AudioContext-Methode decodeAudioData() decodiert werden. Bei dieser Methode wird die in request.response gespeicherte ArrayBuffer der Audiodateidaten asynchron decodiert, 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.

Wiedergabe von Tönen

Ein einfaches Audiodiagramm
Ein einfaches Audiodiagramm

Sobald mindestens eine AudioBuffers geladen ist, können wir Töne abspielen. Angenommen, wir haben gerade eine AudioBuffer mit dem Gebell eines Hundes geladen und das Laden ist abgeschlossen. Diesen Puffer können wir dann 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 etwas anklickt.

Mit der Funktion noteOn(time) ist es einfach, eine präzise Audiowiedergabe für Spiele und andere zeitkritische Anwendungen zu planen. Damit diese Planung jedoch ordnungsgemäß funktioniert, müssen Ihre Audio-Puffer vorab geladen werden.

Web Audio API abstrahieren

Natürlich wäre es besser, ein allgemeineres Ladesystem zu erstellen, das nicht hartcodiert ist, um diesen bestimmten Ton zu laden. Es gibt viele Ansätze, um mit den vielen kurzen bis mittellangen Tönen umzugehen, die in einer Audioanwendung oder einem Spiel verwendet werden. Hier ist eine Möglichkeit mit einem BufferLoader (kein Teil des Webstandards).

Im Folgenden finden Sie ein Beispiel für die Verwendung der Klasse BufferLoader. Erstellen wir zwei AudioBuffers und spielen sie nach dem Laden gleichzeitig ab.

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

Der richtige Umgang mit der Zeit: Klänge mit Rhythmus spielen

Mit der Web Audio API können Entwickler die Wiedergabe genau planen. Zur Veranschaulichung erstellen wir einen einfachen Rhythmus-Track. Das wohl bekannteste Drumkit-Muster ist das folgende:

Ein einfaches Rock-Drum-Pattern
Ein einfaches Rock-Drum-Pattern

Dabei wird auf jeder achten Note ein Hihat gespielt, bei dem Kick und Snare alle Viertel im Viertel 4/4 abwechselnd gespielt werden.

Angenommen, wir haben die Puffer kick, snare und hihat geladen, ist der Code dazu ganz 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 einmal wiederholt, anstatt der unbegrenzten Schleife, die in der Notenschrift zu sehen ist. Die Funktion playSound ist eine Methode, mit der ein Puffer zu einer bestimmten Zeit wiedergegeben wird:

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 mit einem Ton ausführen können, ist die Änderung 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 verändern:

Audiodiagramm mit einem Verstärkungsknoten
Audiograph mit einem Verstärkungsknoten

So richten Sie die Verbindung ein:

// 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. Bearbeiten Sie dazu gainNode.gain.value so:

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

Cross-Fading zwischen zwei Tönen

Angenommen, wir haben ein etwas komplexeres Szenario, bei dem mehrere Töne abgespielt werden, aber ein Cross-Fade zwischen ihnen verwendet werden soll. Dies ist häufig bei einer DJ-ähnlichen Anwendung der Fall, in der wir zwei Plattenspieler haben und in der Lage sein möchten, von einer Tonquelle zur anderen zu schwenken.

Dazu können Sie das folgende Audiodiagramm verwenden:

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

Dazu erstellen wir einfach zwei AudioGainNodes und verbinden jede Quelle über die Knoten mithilfe einer Funktion wie dieser:

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

Crossfading mit gleicher Leistung

Bei einem naiven linearen Crossfade kommt es zu einem Lautstärkeabfall, wenn du zwischen den Samples schwebst.

Linearer Crossfade
Lineare Überblendung

Um dieses Problem zu lösen, verwenden wir eine Gleichheitskurve, 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 einem gleichmäßigeren Crossfade zwischen Regionen führt, die sich in der Lautstärke leicht unterscheiden können.

Ein Crossfade mit gleicher Lautstärke.
Ein Crossfade mit gleicher Lautstärke

Crossfading in Playlists

Eine weitere gängige Anwendung für einen Crossfader ist in einer Musikplayer-App. Wenn ein Song wechselt, soll der aktuelle Titel ausgeblendet und der neue eingeschwenkt werden, um einen abrupten Übergang zu vermeiden. Dazu musst du einen Crossfade in der Zukunft planen. Wir könnten setTimeout für diese Planung verwenden, dies ist jedoch nicht präzise. Mit der Web Audio API können Sie die Schnittstelle AudioParam verwenden, um zukünftige Werte für Parameter wie den Verstärkungswert eines AudioGainNode zu planen.

So können wir bei einer Playlist zwischen den Titeln wechseln, indem wir kurz vor dem Ende des aktuellen Titels eine Lautstärkesenkung für den gerade abgespielten Titel und eine Lautstärkeerhöhung für den nächsten Titel planen:

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 praktische RampToValue-Methoden, mit denen sich der Wert eines Parameters wie linearRampToValueAtTime und exponentialRampToValueAtTime schrittweise ändern lässt.

Sie können die Funktion für die Übergangszeit aus den integrierten linearen und exponentiellen Funktionen auswählen (wie oben). Sie können aber auch mithilfe der Funktion setValueCurveAtTime eine eigene Wertkurve über ein Wertearray angeben.

Einen einfachen Filtereffekt auf einen Ton anwenden

Ein Audio-Diagramm mit einem BiquadFilterNode
Audiograph mit einem BiquadFilterNode

Mit der Web Audio API können Sie Klänge von einem Audioknoten in einen anderen übertragen. Dadurch entsteht eine potenziell komplexe Prozessorkette, mit der Ihren Klangformen komplexe Effekte hinzugefügt werden.

Eine Möglichkeit dazu besteht darin, BiquadFilterNodes zwischen die Audioquelle und das Ziel zu platzieren. Dieser Audioknoten kann eine Vielzahl von Filtern niedriger Ordnung ausführen, die zum Erstellen von grafischen Equalizern und sogar komplexeren Effekten verwendet werden können. Dabei geht es hauptsächlich darum, auszuwählen, welche Teile des Frequenzspektrums eines Tons hervorgehoben und welche unterdrückt werden sollen.

Folgende Filtertypen werden unterstützt:

  • Tiefpassfilter
  • Hochpassfilter
  • Band pass filter
  • Low-Shelf-Filter
  • Hochpassfilter
  • Peaking-Filter
  • Kerbfilter
  • Filter „Alle Karten/Tickets“

Alle Filter enthalten Parameter, mit denen die Verstärkung, die Häufigkeit der Filteranwendung und ein Qualitätsfaktor festgelegt werden können. Der Tiefpassfilter behält den unteren Frequenzbereich bei, verwirft aber hohe Frequenzen. Der Verbindungspunkt wird durch den Frequenzwert bestimmt. Der Q-Faktor ist ohne Einheit und bestimmt die Form der Grafik. Die Verstärkung wirkt sich nur auf bestimmte Filter aus, z. B. auf die Tiefpass- und Peaking-Filter, aber nicht auf diesen Tiefpassfilter.

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

Im Beispielcode können Sie den Filter verbinden und trennen, um die AudioContext-Grafik dynamisch zu ändern. Wir können Audioknoten durch Aufrufen von node.disconnect(outputNumber) von der Grafik trennen. Wenn wir beispielsweise die Route des Graphen von einem Filter zu einer direkten Verbindung umleiten möchten, 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 Audiosamples. Wir haben Audio-Grafiken mit Verstärkungsknoten und Filtern erstellt und Töne und Audioparameter angepasst, um einige gängige Audioeffekte zu ermöglichen. Jetzt können Sie loslegen und coole Web-Audioanwendungen erstellen.

Wenn Sie nach Inspiration suchen, haben viele Entwickler bereits beeindruckende Projekte mit der Web Audio API erstellt. Zu meinen Favoriten gehören:

  • AudioJedit, ein Tool zum Schneiden von Audio im Browser, das SoundCloud-Permalinks verwendet.
  • ToneCraft, ein Sound-Sequencer, mit dem Klänge durch Stapeln von 3D-Blöcken erstellt werden.
  • Plink, ein kollaboratives Musikspiel mit Web Audio und Web Sockets.