Chrome 64 enthält eine lang erwartete neue Funktion in der Web Audio API: AudioWorklet. Hier erfährst du, wie du einen benutzerdefinierten Audioprozessor mit JavaScript-Code erstellst. Sehen Sie sich die Live-Demos an. Der nächste Artikel in der Reihe, Audio Worklet Design Pattern, könnte für die Entwicklung einer erweiterten Audio-App interessant sein.
Hintergrund: ScriptProcessorNode
Die Audioverarbeitung in der Web Audio API wird in einem separaten Thread vom Haupt-UI-Thread ausgeführt, sodass sie reibungslos funktioniert. Um benutzerdefinierte Audioverarbeitung in JavaScript zu ermöglichen, wurde in der Web Audio API ein ScriptProcessorNode vorgeschlagen, der Ereignishandler verwendet, um das Nutzerscript im Haupt-UI-Thread aufzurufen.
Bei diesem Design gibt es zwei Probleme: Die Ereignisbehandlung ist standardmäßig asynchron und die Codeausführung erfolgt im Hauptthread. Ersteres führt zu Latenz und letzteres belastet den Hauptthread, der häufig mit verschiedenen UI- und DOM-bezogenen Aufgaben überlastet ist, was zu Rucklern bei der Benutzeroberfläche oder zu Audiostörungen führt. Aufgrund dieses grundlegenden Designfehlers wird ScriptProcessorNode
in der Spezifikation nicht mehr unterstützt und durch AudioWorklet ersetzt.
Konzepte
In Audio-Worklets wird der vom Nutzer bereitgestellte JavaScript-Code im Audioverarbeitungs-Thread gehalten. Das bedeutet, dass es nicht zum Haupt-Thread wechseln muss, um Audio zu verarbeiten. Das bedeutet, dass der vom Nutzer bereitgestellte Scriptcode zusammen mit anderen integrierten AudioNodes
im Audio-Rendering-Thread (AudioWorkletGlobalScope
) ausgeführt wird. Dadurch wird eine zusätzliche Latenz vermieden und ein synchrones Rendering ermöglicht.
Registrierung und Instanziierung
Die Verwendung von Audio-Worklets besteht aus zwei Teilen: AudioWorkletProcessor
und AudioWorkletNode
. Das ist aufwendiger als die Verwendung von ScriptProcessorNode, aber es ist erforderlich, um Entwicklern die Low-Level-Funktionen für die benutzerdefinierte Audioverarbeitung zur Verfügung zu stellen. AudioWorkletProcessor
steht für den tatsächlichen Audioprozessor, der in JavaScript-Code geschrieben ist und sich im AudioWorkletGlobalScope
befindet.
AudioWorkletNode
ist das Gegenstück zu AudioWorkletProcessor
und übernimmt die Verbindung zu und von anderen AudioNodes
im Hauptthread. Sie ist im globalen Hauptbereich verfügbar und funktioniert wie eine normale AudioNode
.
Hier sind zwei Code-Snippets, die die Registrierung und Instanziierung veranschaulichen.
// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
constructor(context) {
super(context, 'my-worklet-processor');
}
}
let context = new AudioContext();
context.audioWorklet.addModule('processors.js').then(() => {
let node = new MyWorkletNode(context);
});
Wenn Sie eine AudioWorkletNode
erstellen möchten, müssen Sie ein AudioContext-Objekt und den Namen des Prozessors als String hinzufügen. Eine Prozessordefinition kann über den addModule()
-Aufruf des neuen Audio-Worklet-Objekts geladen und registriert werden.
Worklet APIs, einschließlich Audio Worklet, sind nur in einem sicheren Kontext verfügbar. Daher muss eine Seite, auf der sie verwendet werden, über HTTPS bereitgestellt werden. http://localhost
gilt jedoch als sicher für lokale Tests.
Sie können AudioWorkletNode
unterordnen, um einen benutzerdefinierten Knoten zu definieren, der vom Prozessor unterstützt wird, der im Worklet ausgeführt wird.
// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor() {
super();
}
process(inputs, outputs, parameters) {
// audio processing code here.
}
}
registerProcessor('my-worklet-processor', MyWorkletProcessor);
Die registerProcessor()
-Methode in der AudioWorkletGlobalScope
nimmt einen String für den Namen des zu registrierenden Prozessors und die Klassendefinition an.
Nach Abschluss der Scriptcode-Bewertung im globalen Gültigkeitsbereich wird das Versprechen von AudioWorklet.addModule()
erfüllt und die Nutzer werden darüber informiert, dass die Klassendefinition im globalen Hauptbereich verwendet werden kann.
Benutzerdefinierte Audioparameter
Eine der nützlichen Funktionen von AudioNodes ist die planbare Parameterautomatisierung mit AudioParam
. AudioWorklet-Knoten können diese verwenden, um freigegebene Parameter abzurufen, die automatisch mit der Audiorate gesteuert werden können.
Benutzerdefinierte Audioparameter können in einer AudioWorkletProcessor
-Klassendefinition deklariert werden, indem eine Reihe von AudioParamDescriptor
eingerichtet wird. Die zugrunde liegende WebAudio-Engine liest diese Informationen beim Erstellen eines AudioWorkletNode und erstellt und verknüpft dann entsprechend AudioParam
-Objekte mit dem Knoten.
/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {
// Static getter to define AudioParam objects in this custom processor.
static get parameterDescriptors() {
return [{
name: 'myParam',
defaultValue: 0.707
}];
}
constructor() { super(); }
process(inputs, outputs, parameters) {
// |myParamValues| is a Float32Array of either 1 or 128 audio samples
// calculated by WebAudio engine from regular AudioParam operations.
// (automation methods, setter) Without any AudioParam change, this array
// would be a single value of 0.707.
const myParamValues = parameters.myParam;
if (myParamValues.length === 1) {
// |myParam| has been a constant value for the current render quantum,
// which can be accessed by |myParamValues[0]|.
} else {
// |myParam| has been changed and |myParamValues| has 128 values.
}
}
}
Methode AudioWorkletProcessor.process()
Die eigentliche Audioverarbeitung erfolgt in der process()
-Callback-Methode in der AudioWorkletProcessor
. Sie muss von einem Nutzer in der Klassendefinition implementiert werden. Die WebAudio-Engine ruft diese Funktion isochron auf, um Eingabewerte und Parameter zu übergeben und Ausgabewerte abzurufen.
/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
// The processor may have multiple inputs and outputs. Get the first input and
// output.
const input = inputs[0];
const output = outputs[0];
// Each input or output may have multiple channels. Get the first channel.
const inputChannel0 = input[0];
const outputChannel0 = output[0];
// Get the parameter value array.
const myParamValues = parameters.myParam;
// if |myParam| has been a constant value during this render quantum, the
// length of the array would be 1.
if (myParamValues.length === 1) {
// Simple gain (multiplication) processing over a render quantum
// (128 samples). This processor only supports the mono channel.
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[0];
}
} else {
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[i];
}
}
// To keep this processor alive.
return true;
}
Außerdem kann der Rückgabewert der process()
-Methode verwendet werden, um die Lebensdauer von AudioWorkletNode
zu steuern, damit Entwickler den Arbeitsspeicherbedarf verwalten können. Wenn false
von der process()
-Methode zurückgegeben wird, wird der Prozessor als inaktiv markiert und die WebAudio
-Engine ruft die Methode nicht mehr auf. Damit der Prozessor aktiv bleibt, muss die Methode true
zurückgeben.
Andernfalls wird das Knoten- und Prozessorpaar vom System irgendwann durch den Garbage Collector gelöscht.
Bidirektionale Kommunikation mit MessagePort
Manchmal sollen für eine benutzerdefinierte AudioWorkletNode
Steuerelemente angezeigt werden, die nicht auf AudioParam
zugeordnet sind, z. B. ein stringsbasiertes type
-Attribut, das zum Steuern eines benutzerdefinierten Filters verwendet wird. Zu diesem und anderen Zwecken sind AudioWorkletNode
und AudioWorkletProcessor
mit einer MessagePort
für die bidirektionale Kommunikation ausgestattet. Über diesen Kanal können alle Arten von benutzerdefinierten Daten ausgetauscht werden.
Über das Attribut .port
kann sowohl auf den Knoten als auch auf den Prozessor zugegriffen werden. Die port.postMessage()
-Methode des Knotens sendet eine Nachricht an den port.onmessage
-Handler des zugehörigen Prozessors und umgekehrt.
/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
let node = new AudioWorkletNode(context, 'port-processor');
node.port.onmessage = (event) => {
// Handling data from the processor.
console.log(event.data);
};
node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
// Handling data from the node.
console.log(event.data);
};
this.port.postMessage('Hi!');
}
process(inputs, outputs, parameters) {
// Do nothing, producing silent output.
return true;
}
}
registerProcessor('port-processor', PortProcessor);
MessagePort
unterstützt „übertragbar“, mit dem Sie Datenspeicher oder ein WASM-Modul über die Threadgrenze hinweg übertragen können. Das eröffnet unzählige Möglichkeiten, wie das Audio-Worklet-System verwendet werden kann.
Schritt-für-Schritt-Anleitung: GainNode erstellen
Hier ein vollständiges Beispiel für einen GainNode, der auf AudioWorkletNode
und AudioWorkletProcessor
basiert.
Die Datei index.html
:
<!doctype html>
<html>
<script>
const context = new AudioContext();
// Loads module script with AudioWorklet.
context.audioWorklet.addModule('gain-processor.js').then(() => {
let oscillator = new OscillatorNode(context);
// After the resolution of module loading, an AudioWorkletNode can be
// constructed.
let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');
// AudioWorkletNode can be interoperable with other native AudioNodes.
oscillator.connect(gainWorkletNode).connect(context.destination);
oscillator.start();
});
</script>
</html>
Die Datei gain-processor.js
:
class GainProcessor extends AudioWorkletProcessor {
// Custom AudioParams can be defined with this static getter.
static get parameterDescriptors() {
return [{ name: 'gain', defaultValue: 1 }];
}
constructor() {
// The super constructor call is required.
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const gain = parameters.gain;
for (let channel = 0; channel < input.length; ++channel) {
const inputChannel = input[channel];
const outputChannel = output[channel];
if (gain.length === 1) {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[0];
} else {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[i];
}
}
return true;
}
}
registerProcessor('gain-processor', GainProcessor);
Hier erfahren Sie die Grundlagen des Audio-Worklet-Systems. Live-Demos sind im GitHub-Repository des Chrome WebAudio-Teams verfügbar.
Funktionelle Umstellung: Von der experimentellen zur stabilen Version
Audio-Worklets sind in Chrome 66 oder höher standardmäßig aktiviert. In Chrome 64 und 65 war die Funktion hinter dem experimentellen Flag versteckt.