Audio-Worklet ist jetzt standardmäßig verfügbar

Hongchan Choi

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.

Diagramm zum globalen Geltungsbereich und zum Geltungsbereich von Audio-Worklets
Abb.1

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.

Diagramm für Audio-Worklet-Knoten und ‑Prozessor
Abbildung 2

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.

Fig.2
Abbildung 2

Ü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.