Wasm Audio Worklets API

AudioWorklet 擴充功能至 Web Audio API 規範,使網站能夠實作自訂的 AudioWorkletProcessor Web Audio 圖形節點類型。

這些自訂處理器節點會即時處理音訊資料,作為音訊圖形處理流程的一部分,並讓開發人員能夠以 JavaScript 編寫低延遲敏感的音訊處理程式碼。

Emscripten Wasm Audio Worklets API 是這些 AudioWorklet 節點與 WebAssembly 的 Emscripten 特定整合。Wasm Audio Worklets 使開發人員能夠以 C/C++ 程式碼實作 AudioWorklet 處理節點,並編譯成 WebAssembly,而不是使用 JavaScript 執行此任務。

與 JavaScript 相比,以 WebAssembly 開發 AudioWorkletProcessors 可提供更高的效能,而 Emscripten Wasm Audio Worklets 系統執行時間已仔細開發,以確保不會產生暫時的 JavaScript 層級 VM 垃圾,從而消除 GC 暫停影響音訊合成效能的可能性。

Audio Worklets API 是基於 Wasm Workers 功能。目標設定 Audio Worklets 時也可以啟用 -pthread 選項,但音訊 worklets 一律會在 Wasm Worker 中執行,而非在 Pthread 中執行。

開發概述

撰寫 Wasm Audio Worklets 與在 JS 中開發基於 Audio Worklets API 的應用程式類似 (請參閱 MDN:使用 AudioWorklets),唯一的例外是使用者不會手動實作 AudioWorkletGlobalScope 中 ScriptProcessorNode 檔案的 JS 程式碼。這由 Emscripten Wasm AudioWorklets 執行時間自動管理。

相反地,應用程式開發人員需要實作少量的 JS <-> Wasm (C/C++) 交互操作,以便從 Wasm 與 AudioContext 和 AudioNodes 互動。

Audio Worklets 會在雙層「類別類型及其執行個體」設計上運作:第一個定義一或多個稱為 AudioWorkletProcessors 的節點類型 (或類別),然後將這些處理器在音訊處理圖形中執行個體化一或多次,做為 AudioWorkletNodes。

一旦類別類型在 Web Audio 圖形上執行個體化,且圖形正在執行時,就會針對流經節點的每個 128 個取樣的處理音訊串流叫用 C/C++ 函式指標回呼。較新的 Web Audio API 規格允許變更此項,因此為了未來相容性,請使用 AudioSampleFramesamplesPerChannel 來取得值。

此回呼會在具有即時處理優先順序的專用個別音訊處理執行緒上執行。每個 Web Audio 環境設定只會使用單一音訊處理執行緒。也就是說,即使有多個音訊節點執行個體 (可能來自多個不同的音訊處理器),這些執行個體也會全部在 AudioContext 上共用相同的專用音訊執行緒,且不會各自在自己的個別執行緒中執行。

注意:音訊 worklet 節點處理是基於提取模式回呼。Audio Worklets 不允許建立通用即時優先執行緒。音訊回呼程式碼應儘快執行,且不得封鎖。換句話說,無法執行自訂的 for(;;) 迴圈。

程式設計範例

為了取得使用 Wasm Audio Worklets 程式設計的實務經驗,讓我們建立一個簡單的音訊節點,透過其輸出通道輸出隨機雜訊。

1. 首先,我們將在 C/C++ 程式碼中建立 Web Audio 環境設定。這可透過 emscripten_create_audio_context() 函式達成。在整合現有 Web Audio 程式庫的較大型應用程式中,您可能已經透過其他程式庫建立 AudioContext,在這種情況下,您將改為呼叫 emscriptenRegisterAudioObject() 函式,將該環境設定註冊為對 WebAssembly 可見。

然後,我們將指示 Emscripten 執行時間在此環境設定上初始化 Wasm Audio Worklet 執行緒範圍。用於達成這些工作的程式碼如下所示

#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. 初始化 worklet 執行緒環境設定後,我們就可以定義自己的雜訊產生器 AudioWorkletProcessor 節點類型

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. 初始化處理器後,我們現在可以執行個體化並將其連線為圖形上的節點。由於網頁上的音訊播放只能在回應使用者輸入時啟動,我們也會註冊一個事件處理常式,當使用者按一下網頁上存在的 DOM Canvas 元素時,該常式會繼續執行音訊環境設定。

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. 按一下時繼續執行音訊環境設定的程式碼如下所示

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. 最後,我們可以實作將產生雜訊的音訊回呼

#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
}

就這樣!使用連結器旗標 -sAUDIO_WORKLET=1 -sWASM_WORKERS=1 編譯程式碼,以啟用以 AudioWorklets 為目標。

將音訊執行緒與主要執行緒同步

Wasm Audio Worklets API 是建立於 Emscripten Wasm Workers 功能之上。這表示 Wasm Audio Worklet 執行緒會模擬為如同它是 Wasm Worker 執行緒一樣。

若要同步 Audio Worklet 節點與應用程式中其他執行緒之間的資訊,有三個選項

  1. 利用 Web Audio「AudioParams」模型。每個 Audio Worklet Processor 類型都會使用一組自訂定義的音訊參數執行個體化,這些參數可以精確地以取樣影響音訊計算。這些參數會傳遞至音訊處理函式中的 params 陣列。

    建立 Web Audio 環境設定的主要瀏覽器執行緒可以隨時調整這些參數的值。請參閱 MDN 函式:setValueAtTime

  2. 可使用 GCC/Clang 無鎖定原子作業、Emscripten 原子作業和 Wasm Worker API 執行緒同步基本元素,與 Audio Worklet 執行緒共用資料。如需詳細資訊,請參閱 WASM_WORKERS

  3. 利用 emscripten_audio_worklet_post_function_*() 系列的事件傳遞函式。這些函式的運作方式與 emscripten_wasm_worker_post_function_*() 函式類似。它們可啟用 postMessage() 樣式的通訊,其中音訊 worklet 執行緒和主要瀏覽器執行緒可以互相傳送訊息 (函式呼叫分派)。

更多範例

如需更多關於 Web Audio API 和 Wasm AudioWorklets 的程式碼範例,請參閱目錄 tests/webaudio/。