Audio-Worklet eingeben

Hongchan Choi

Chrome 64 bietet eine mit Spannung erwartete neue Funktion in der Web Audio API: AudioWorklet. In diesem Artikel werden Konzept und Verwendung für diejenigen vorgestellt, die einen benutzerdefinierten Audioprozessor mit JavaScript-Code entwickeln möchten. Sehen Sie sich die Live-Demos auf GitHub an. Auch der nächste Artikel der Reihe, Audio Worklet Design Pattern, könnte interessante Lesematerialien zum Erstellen einer fortschrittlichen Audio-App sein.

Hintergrund: ScriptProcessorNode

Die Audioverarbeitung wird in der Web Audio API in einem anderen Thread als dem Haupt-UI-Thread ausgeführt, um für einen reibungslosen Ablauf zu sorgen. Um die benutzerdefinierte Audioverarbeitung in JavaScript zu ermöglichen, schlug die Web Audio API einen ScriptProcessorNode vor, der mithilfe von Event-Handlern das Nutzerskript im Hauptthread der Benutzeroberfläche aufruft.

Bei diesem Design gibt es zwei Probleme: Die Ereignisverarbeitung ist standardmäßig asynchron und die Codeausführung erfolgt im Hauptthread. Der erstere verursacht die Latenz, und der zweite den Hauptthread, der häufig mit verschiedenen UI- und DOM-bezogenen Aufgaben überfüllt ist, verursacht eine Verzögerung in der UI oder es kommt zu Audiostörungen. Aufgrund dieses grundlegenden Designfehlers wurde ScriptProcessorNode aus der Spezifikation verworfen und durch AudioWorklet ersetzt.

Konzepte

Audio Worklet speichert den vom Nutzer bereitgestellten JavaScript-Code ganz einfach im Audioverarbeitungsthread, es muss also nicht zum Hauptthread gewechselt werden, um Audio zu verarbeiten. Der vom Nutzer bereitgestellte Skriptcode wird also zusammen mit anderen integrierten AudioNodes im Audio-Rendering-Thread (AudioWorkletGlobalScope) ausgeführt, sodass keine zusätzliche Latenz und kein synchrones Rendering möglich sind.

Diagramm zum globalen Hauptbereich und zum Audio Worklet-Bereich
Abb.1

Registrierung und Instanziierung

Das Audio Worklet besteht aus zwei Teilen: AudioWorkletProcessor und AudioWorkletNode. Dies ist aufwendiger als die Verwendung von ScriptProcessorNode, aber es ist erforderlich, um Entwicklern die Low-Level-Funktion zur benutzerdefinierten Audioverarbeitung zur Verfügung zu stellen. AudioWorkletProcessor steht für den tatsächlichen in JavaScript-Code geschriebenen Audioprozessor, der sich im AudioWorkletGlobalScope befindet. AudioWorkletNode ist das Gegenstück von AudioWorkletProcessor und kümmert sich um die Verbindung zu und von anderen AudioNodes im Hauptthread. Er wird im globalen Hauptbereich bereitgestellt und funktioniert wie ein normaler AudioNode.

Im Folgenden finden Sie ein Paar 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);
});

Zum Erstellen eines AudioWorkletNode sind mindestens zwei Elemente erforderlich: ein AudioContext-Objekt und den Prozessornamen als String. Eine Prozessordefinition kann durch den Aufruf addModule() des neuen Audio Worklet-Objekts geladen und registriert werden. Worklet APIs, die Audio Worklet enthalten, sind nur in einem sicheren Kontext verfügbar. Daher muss eine Seite, auf der sie verwendet werden, über HTTPS bereitgestellt werden, obwohl http://localhost als sicher für lokale Tests gilt.

Sie können AudioWorkletNode auch ableiten, um einen benutzerdefinierten Knoten zu definieren, der von dem auf dem Worklet ausgeführten Prozessor unterstützt wird.

// This is "processor.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 Methode registerProcessor() im AudioWorkletGlobalScope verwendet einen String für den Namen des zu registrierenden Prozessors und die Klassendefinition. Nach Abschluss der Auswertung des Skriptcodes auf globaler Ebene wird das Promise von AudioWorklet.addModule() aufgelöst, wodurch die Nutzer informiert werden, dass die Klassendefinition im globalen Hauptbereich verwendet werden kann.

Benutzerdefinierter Audioparameter

Einer der nützlichen Vorteile von AudioNodes ist die planbare Parameterautomatisierung mit AudioParams. AudioWorkletNodes können damit freigegebene Parameter abrufen, die automatisch mit der Audiorate gesteuert werden können.

Knoten- und Prozessordiagramm für Audio-Worklet
Abb.2

Benutzerdefinierte Audioparameter können in einer AudioWorkletProcessor-Klassendefinition deklariert werden, indem eine Reihe von AudioParamDescriptors eingerichtet wird. Die zugrunde liegende WebAudio-Engine erfasst diese Informationen beim Erstellen eines AudioWorkletNode, erstellt dann entsprechende AudioParam-Objekte und verknüpft sie 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 Callback-Methode process() im AudioWorkletProcessor und muss vom Nutzer in der Klassendefinition implementiert werden. Die WebAudio-Engine ruft diese Funktion isochronisch auf, um Eingaben und Parameter einzuführen und Ausgaben 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 Methode process() verwendet werden, um die Lebensdauer von AudioWorkletNode zu steuern, sodass Entwickler den Speicherbedarf verwalten können. Durch die Rückgabe von false aus der process()-Methode 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/Prozessorpaar am Ende durch das System automatisch bereinigt.

Bidirektionale Kommunikation mit MessagePort

Manchmal möchten benutzerdefinierte AudioWorkletNodes Steuerelemente einblenden, die nicht AudioParam zugeordnet sind. Beispielsweise kann ein stringbasiertes type-Attribut verwendet werden, um einen benutzerdefinierten Filter zu steuern. Zu diesem Zweck und darüber hinaus sind AudioWorkletNode und AudioWorkletProcessor mit einem MessagePort für eine bidirektionale Kommunikation ausgestattet. Über diesen Kanal können alle Arten von benutzerdefinierten Daten ausgetauscht werden.

Fig.2
Abb.2

Auf MessagePort kann sowohl auf dem Knoten als auch auf dem Prozessor über das Attribut .port zugegriffen werden. Die Methode port.postMessage() 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!');
});
/* "processor.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);

Beachten Sie auch, dass MessagePort die Funktion „Transferable“ unterstützt, mit der Sie Datenspeicher oder ein WASM-Modul über die Thread-Grenze übertragen können. Das eröffnet zahllose Möglichkeiten, das Audio Worklet-System zu nutzen.

Schritt-für-Schritt-Anleitung: GainNode erstellen

Hier ist ein vollständiges Beispiel für GainNode, das auf AudioWorkletNode und AudioWorkletProcessor aufbaut.

Index.html

<!doctype html>
<html>
<script>
  const context = new AudioContext();

  // Loads module script via 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>

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 werden die Grundlagen des Audio Worklet-Systems behandelt. Live-Demos sind im GitHub-Repository des Chrome WebAudio-Teams verfügbar.

Funktionsübergang: Experimentell zur stabilen Version

Audio-Worklet ist für Chrome 66 und höher standardmäßig aktiviert. In Chrome 64 und 65 stand die Funktion hinter dem experimentellen Flag.