Wasm Audio Worklets API

Die AudioWorklet-Erweiterung der Web Audio API-Spezifikation ermöglicht es Websites, benutzerdefinierte AudioWorkletProcessor Web Audio Graph-Knotentypen zu implementieren.

Diese benutzerdefinierten Prozessorknoten verarbeiten Audiodaten in Echtzeit als Teil des Audio-Graph-Verarbeitungsflusses und ermöglichen es Entwicklern, latenzempfindlichen Audioverarbeitungscode in JavaScript zu schreiben.

Die Emscripten Wasm Audio Worklets API ist eine Emscripten-spezifische Integration dieser AudioWorklet-Knoten in WebAssembly. Wasm Audio Worklets ermöglicht es Entwicklern, AudioWorklet-Verarbeitungsknoten in C/C++-Code zu implementieren, der zu WebAssembly kompiliert wird, anstatt JavaScript für die Aufgabe zu verwenden.

Die Entwicklung von AudioWorkletProcessors in WebAssembly bietet den Vorteil einer verbesserten Leistung im Vergleich zu JavaScript, und die Emscripten Wasm Audio Worklets Systemlaufzeit wurde sorgfältig entwickelt, um zu garantieren, dass kein temporärer JavaScript-Level-VM-Garbage generiert wird, wodurch die Möglichkeit von GC-Pausen, die die Audiosynthese-Leistung beeinträchtigen, eliminiert wird.

Die Audio Worklets API basiert auf der Wasm Workers-Funktion. Es ist auch möglich, die Option -pthread zu aktivieren, während Audio Worklets angesprochen werden, aber die Audio Worklets werden immer in einem Wasm Worker und nicht in einem Pthread ausgeführt.

Entwicklungsübersicht

Das Erstellen von Wasm Audio Worklets ähnelt der Entwicklung von Audio Worklets API-basierten Anwendungen in JS (siehe MDN: Using AudioWorklets), mit der Ausnahme, dass Benutzer den JS-Code für die ScriptProcessorNode-Dateien im AudioWorkletGlobalScope nicht manuell implementieren. Dies wird automatisch von der Emscripten Wasm AudioWorklets Laufzeit verwaltet.

Stattdessen müssen Anwendungsentwickler eine geringe Menge an JS <-> Wasm (C/C++) Interop implementieren, um mit dem AudioContext und den AudioNodes von Wasm aus zu interagieren.

Audio Worklets operieren nach einem zweischichtigen „Klassentyp & seine Instanz“-Design: Zuerst definiert man einen oder mehrere Knotentypen (oder Klassen), die AudioWorkletProcessors genannt werden, und dann werden diese Prozessoren ein- oder mehrmals im Audioverarbeitungs-Graph als AudioWorkletNodes instanziiert.

Sobald ein Klassentyp im Web-Audio-Graphen instanziiert und der Graph läuft, wird für jeweils 128 Samples des verarbeiteten Audiostreams, der durch den Knoten fließt, ein C/C++-Funktionszeiger-Callback aufgerufen. Neuere Web Audio API-Spezifikationen erlauben eine Änderung dessen, daher verwenden Sie für zukünftige Kompatibilität samplesPerChannel von AudioSampleFrame, um den Wert zu erhalten.

Dieser Callback wird auf einem dedizierten separaten Audioverarbeitungs-Thread mit Echtzeit-Verarbeitungspriorität ausgeführt. Jeder Web Audio-Kontext wird nur einen einzigen Audioverarbeitungs-Thread verwenden. Das heißt, selbst wenn es mehrere Audio-Knoteninstanzen gibt (möglicherweise von mehreren verschiedenen Audioprozessoren), werden diese alle denselben dedizierten Audio-Thread im AudioContext teilen und nicht in einem eigenen separaten Thread laufen.

Hinweis: Die Verarbeitung des Audio-Worklet-Knotens basiert auf einem Pull-Modus-Callback. Audio Worklets erlauben nicht die Erstellung von Allzweck-Echtzeit-priorisierten Threads. Der Audio-Callback-Code sollte so schnell wie möglich ausgeführt werden und nicht blockieren. Mit anderen Worten, das Starten einer benutzerdefinierten for(;;)-Schleife ist nicht möglich.

Programmierbeispiel

Um praktische Erfahrungen mit der Programmierung von Wasm Audio Worklets zu sammeln, erstellen wir einen einfachen Audio-Knoten, der zufälliges Rauschen über seine Ausgabekanäle ausgibt.

1. Zuerst erstellen wir einen Web Audio-Kontext im C/C++-Code. Dies geschieht über die Funktion emscripten_create_audio_context(). In einer größeren Anwendung, die bestehende Web Audio-Bibliotheken integriert, haben Sie möglicherweise bereits einen AudioContext, der über eine andere Bibliothek erstellt wurde. In diesem Fall würden Sie diesen Kontext stattdessen für WebAssembly sichtbar machen, indem Sie die Funktion emscriptenRegisterAudioObject() aufrufen.

Danach weisen wir die Emscripten-Laufzeit an, einen Wasm Audio Worklet-Thread-Scope in diesem Kontext zu initialisieren. Der Code zur Erledigung dieser Aufgaben sieht wie folgt aus

#include <emscripten/webaudio.h>

uint8_t audioThreadStack[4096];

int main()
{
  EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0);

  emscripten_start_wasm_audio_worklet_thread_async(context, audioThreadStack, sizeof(audioThreadStack),
                                                   &AudioThreadInitialized, 0);
}

2. Wenn der Worklet-Thread-Kontext initialisiert wurde, sind wir bereit, unseren eigenen Rauschgenerator-AudioWorkletProcessor-Knotentyp zu definieren

void AudioThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData)
{
  if (!success) return; // Check browser console in a debug build for detailed errors
  WebAudioWorkletProcessorCreateOptions opts = {
    .name = "noise-generator",
  };
  emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, &AudioWorkletProcessorCreated, 0);
}

3. Nachdem der Prozessor initialisiert wurde, können wir ihn nun als Knoten im Graphen instanziieren und verbinden. Da auf Webseiten die Audiowiedergabe nur als Reaktion auf Benutzereingaben gestartet werden kann, registrieren wir auch einen Event-Handler, der den Audiokontext fortsetzt, wenn der Benutzer auf das auf der Seite vorhandene DOM-Canvas-Element klickt.

void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData)
{
  if (!success) return; // Check browser console in a debug build for detailed errors

  int outputChannelCounts[1] = { 1 };
  EmscriptenAudioWorkletNodeCreateOptions options = {
    .numberOfInputs = 0,
    .numberOfOutputs = 1,
    .outputChannelCounts = outputChannelCounts
  };

  // Create node
  EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext,
                                                            "noise-generator", &options, &GenerateNoise, 0);

  // Connect it to audio context destination
  emscripten_audio_node_connect(wasmAudioWorklet, audioContext, 0, 0);

  // Resume context on mouse click
  emscripten_set_click_callback("canvas", (void*)audioContext, 0, OnCanvasClick);
}
  1. Der Code zum Fortsetzen des Audiokontextes bei Klick sieht so aus

bool OnCanvasClick(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
{
  EMSCRIPTEN_WEBAUDIO_T audioContext = (EMSCRIPTEN_WEBAUDIO_T)userData;
  if (emscripten_audio_context_state(audioContext) != AUDIO_CONTEXT_STATE_RUNNING) {
    emscripten_resume_audio_context_sync(audioContext);
  }
  return false;
}
  1. Schließlich können wir den Audiocallback implementieren, der das Rauschen erzeugen soll

#include <emscripten/em_math.h>

bool GenerateNoise(int numInputs, const AudioSampleFrame *inputs,
                      int numOutputs, AudioSampleFrame *outputs,
                      int numParams, const AudioParamFrame *params,
                      void *userData)
{
  for(int i = 0; i < numOutputs; ++i)
    for(int j = 0; j < outputs[i].samplesPerChannel*outputs[i].numberOfChannels; ++j)
      outputs[i].data[j] = emscripten_random() * 0.2 - 0.1; // Warning: scale down audio volume by factor of 0.2, raw noise can be really loud otherwise

  return true; // Keep the graph output going
}

Und das war's! Kompilieren Sie den Code mit den Linker-Flags -sAUDIO_WORKLET=1 -sWASM_WORKERS=1, um das Ansprechen von AudioWorklets zu ermöglichen.

Synchronisieren des Audio-Threads mit dem Haupt-Thread

Die Wasm Audio Worklets API baut auf der Emscripten Wasm Workers-Funktion auf. Das bedeutet, dass der Wasm Audio Worklet-Thread so modelliert wird, als wäre er ein Wasm Worker-Thread.

Um Informationen zwischen einem Audio Worklet Node und anderen Threads in der Anwendung zu synchronisieren, gibt es drei Optionen

  1. Nutzen Sie das Web Audio „AudioParams“-Modell. Jeder Audio Worklet Processor-Typ wird mit einem benutzerdefinierten Satz von Audioparametern instanziiert, die die Audiokomputation mit Sample-genauer Genauigkeit beeinflussen können. Diese Parameter werden im params-Array an die Audioverarbeitungsfunktion übergeben.

    Der Haupt-Browser-Thread, der den Web Audio-Kontext erstellt hat, kann die Werte dieser Parameter jederzeit nach Belieben anpassen. Siehe MDN Funktion: setValueAtTime.

  2. Daten können mit dem Audio Worklet Thread unter Verwendung von GCC/Clang Lock-Free-Atomaroperationen, Emscripten-Atomaroperationen und den Wasm Worker API Thread-Synchronisationsprimitiven geteilt werden. Weitere Informationen finden Sie unter WASM_WORKERS.

  3. Verwenden Sie die emscripten_audio_worklet_post_function_*()-Familie der Ereignisübergabefunktionen. Diese Funktionen ähneln den emscripten_wasm_worker_post_function_*()-Funktionen. Sie ermöglichen eine postMessage()-ähnliche Kommunikation, bei der der Audio-Worklet-Thread und der Haupt-Browser-Thread Nachrichten (Funktionsaufrufe) aneinander senden können.

Weitere Beispiele

Weitere Codebeispiele zur Web Audio API und Wasm AudioWorklets finden Sie im Verzeichnis tests/webaudio/.