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 des Webs zu durchbrechen. Zwar ist für Audio im Web kein Plug-in mehr erforderlich, das Audio-Tag bringt jedoch erhebliche Einschränkungen für die Implementierung anspruchsvoller Spiele und interaktiver Anwendungen mit sich.

Die Web Audio API ist eine High-Level-JavaScript-API zum Verarbeiten und Synthetisieren von Audio in Webanwendungen. Ziel dieser API ist es, Funktionen moderner Audio-Engines für Spiele sowie einige der Misch-, Verarbeitungs- und Filteraufgaben moderner Desktop-Anwendungen für die Audioproduktion bereitzustellen. Im Folgenden finden Sie eine kurze Einführung in die Verwendung dieser leistungsstarken API.

Erste Schritte mit dem AudioContext

Ein AudioContext dient zum Verwalten und Abspielen aller Töne. Wenn Sie mit der Web Audio API einen Ton erzeugen möchten, erstellen Sie eine oder mehrere Tonquellen und verbinden Sie sie mit dem Tonausgabeziel, das von der AudioContext-Instanz bereitgestellt wird. Diese Verbindung muss nicht direkt sein und kann über eine beliebige Anzahl von AudioNodes als Verarbeitungsmodule für das Audiosignal erfolgen. Dieses Routing wird in der Web Audio-Spezifikation ausführlicher beschrieben.

Eine einzelne Instanz von AudioContext kann mehrere Audioeingänge und komplexe Audiografiken unterstützen. Daher benötigen wir für jede von uns erstellte Audioanwendung nur eine davon.

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, wie das Erstellen von AudioNodes und das Decodieren von Audiodateidaten, sind Methoden von AudioContext.

Töne werden geladen

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

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

Das folgende Snippet zeigt, wie ein Audio-Sample geladen wird:

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 den responseType der Anfrage auf 'arraybuffer' fest. Weitere Informationen zu ArrayBuffers finden Sie in diesem Artikel zu XHR2.

Sobald die (nicht decodierten) Audiodateidaten empfangen wurden, können sie für eine spätere Decodierung aufbewahrt oder sofort mit der AudioContext-Methode decodeAudioData() decodiert werden. Diese Methode nimmt die ArrayBuffer der in request.response gespeicherten Audiodateidaten und decodiert sie 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.

Wiedergabe von Tönen

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

Mit der Funktion noteOn(time) lässt sich die Audiowiedergabe für Spiele und andere zeitkritische Anwendungen ganz einfach planen. Damit diese Planung ordnungsgemäß funktioniert, müssen die Soundpuffer jedoch vorab geladen werden.

Abstraktion der Web Audio API

Natürlich wäre es besser, ein allgemeineres Ladesystem zu erstellen, das nicht hartcodiert ist, um diesen bestimmten Sound zu laden. Es gibt viele Ansätze für den Umgang mit den vielen kurzen bis mittellangen Tönen, die eine Audioanwendung oder ein Spiel verwenden würde. 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. Wir erstellen zwei AudioBuffers-Objekte und spielen 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);
}

Mit der Zeit umgehen: Sounds mit Rhythmus abspielen

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

Ein einfaches Rock-Schlagzeugmuster

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

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 machen wir nur eine Wiederholung statt der unbegrenzten Schleife, die wir in den Noten sehen. Die Funktion playSound ist eine Methode, die einen Puffer zu einer bestimmten Zeit abspielt:

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 grundlegendsten 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 ändern:

Einfaches Flussdiagramm, das den Audiofluss von der Quelle zum GainNode und dann zum Ziel zeigt.

Diese Verbindung kann so eingerichtet werden:

// 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 der Graph eingerichtet wurde, können Sie die Lautstärke programmatisch ändern, indem Sie den gainNode.gain.value so 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, bei der wir zwei Plattenspieler haben und von einer Tonquelle zur anderen schwenken möchten.

Dazu kann der folgende Audiograph verwendet werden:

Audiograf mit zwei Quellen, die mit separaten Verstärkungsknoten verbunden sind, die dann an dasselbe 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
    };
}

Überblendung mit gleicher Leistung

Bei einem einfachen linearen Crossfade-Ansatz kommt es zu einem Lautstärkeabfall, wenn Sie zwischen den Samples schwenken.

Eine lineare Überblendung, dargestellt als zwei komplementäre gerade Linien, die als Amplitudenpegel über die Zeit abgebildet werden.
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ärkeeinbrüche zwischen Audiobereichen minimiert, was zu einer gleichmäßigeren Überblendung zwischen Bereichen führt, die sich möglicherweise geringfügig im Pegel unterscheiden.

Eine Überblendung mit gleicher Leistung, dargestellt als zwei gekrümmte Linien, die als Amplitudenpegel über die Zeit dargestellt werden.
Gleichmäßige Überblendung

Überblenden von Playlists

Eine weitere gängige Anwendung für Crossfader ist ein Musikplayer. Wenn ein Song wechselt, soll der aktuelle Titel ausgeblendet und der neue eingeblendet werden, um einen abrupten Übergang zu vermeiden. Planen Sie dazu eine Überblendung in der Zukunft. Wir könnten zwar setTimeout für diese Planung verwenden, aber das ist 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 eines AudioGainNode zu planen.

Bei einer Playlist können wir also 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, beide 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 und exponentialRampToValueAtTime.

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

Einfachen Filtereffekt auf einen Sound anwenden

Einfaches Flussdiagramm, das den Audiofluss von der Quelle zum BiquadFilterNode und zum Ziel zeigt.

Mit der Web Audio API können Sie Ton von einem Audioknoten in einen anderen leiten und so eine potenziell komplexe Kette von Prozessoren erstellen, um Ihren Tonformen komplexe Effekte hinzuzufügen.

Eine Möglichkeit hierfür ist, BiquadFilterNodes zwischen der Tonquelle und dem Ziel zu platzieren. Dieser Typ von Audioknoten kann eine Vielzahl von Filtern niedriger Ordnung ausführen, mit denen grafische Equalizer und noch komplexere Effekte erstellt werden können. Meist geht es darum, auszuwählen, welche Teile des Frequenzspektrums eines Tons hervorgehoben und welche gedämpft werden sollen.

Folgende Filtertypen werden unterstützt:

  • Tiefpassfilter
  • Hochpassfilter
  • Band pass filter
  • Low-Shelf-Filter
  • Filter für hohes Regal
  • Peaking-Filter
  • Notch-Filter
  • All pass filter

Alle Filter enthalten Parameter, mit denen sich die Verstärkung, die Frequenz, bei der der Filter angewendet werden soll, und ein Qualitätsfaktor angeben lassen. Der Tiefpassfilter behält den niedrigeren Frequenzbereich bei, verwirft aber hohe Frequenzen. Der Abbruchpunkt wird durch den Frequenzwert bestimmt. Der Q-Faktor ist einheitenlos und bestimmt die Form des Graphen. Die Verstärkung wirkt sich nur auf bestimmte Filter aus, z. B. auf den Low-Shelf- und den Peaking-Filter, nicht aber auf diesen Tiefpassfilter.

Richten wir einen einfachen Tiefpassfilter ein, um nur die Bässe aus einem Audiobeispiel 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 nach demselben Prinzip funktioniert (A4 ist 440 Hz und A5 ist 880 Hz). Weitere Informationen finden Sie im Abschnitt zur Funktion FilterSample.changeFrequency im Quellcode-Link oben.

Beachten Sie, dass Sie mit dem Beispielcode den Filter verbinden und trennen können, wodurch sich das AudioContext-Diagramm dynamisch ändert. Wir können AudioNodes vom Graphen trennen, indem wir node.disconnect(outputNumber) aufrufen. Wenn Sie beispielsweise den Graphen so umleiten möchten, dass er nicht mehr über einen Filter, sondern über eine direkte Verbindung verläuft, können Sie so vorgehen:

// 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 Audio-Samples. Wir haben Audiographen mit Verstärkungsknoten und Filtern erstellt und Sounds und Anpassungen von Audioparametern geplant, um einige gängige Soundeffekte zu ermöglichen. Jetzt können Sie loslegen und tolle Web-Audio-Anwendungen erstellen.

Wenn Sie Inspiration suchen, finden Sie viele großartige Arbeiten, die Entwickler bereits mit der Web Audio API erstellt haben. Zu meinen Favoriten gehören:

  • AudioJedit, ein Tool zum Zusammenfügen von Audio im Browser, das SoundCloud-Permalinks verwendet.
  • ToneCraft, ein Sound-Sequenzer, bei dem Sounds durch das Stapeln von 3D-Blöcken erstellt werden.
  • Plink, ein kollaboratives Musikspiel, das Web Audio und WebSockets verwendet.