Wasm 音频工作线程 API

音频工作线程扩展 Web 音频 API 规范 使网站能够实现自定义的音频工作线程处理器 Web 音频图节点类型。

这些自定义的处理器节点实时处理音频数据,作为音频图处理流程的一部分,使开发人员能够使用 JavaScript 编写低延迟敏感的音频处理代码。

Emscripten Wasm 音频工作线程 API 是这些音频工作线程节点在 WebAssembly 中的 Emscripten 特定集成。Wasm 音频工作线程使开发人员能够使用 C/C++ 代码实现音频工作线程处理节点,这些代码编译为 WebAssembly,而不是使用 JavaScript 来完成任务。

在 WebAssembly 中开发音频工作线程处理器具有与 JavaScript 相比性能更高的优势,并且 Emscripten Wasm 音频工作线程系统运行时经过精心开发,以确保不会生成任何临时的 JavaScript 级 VM 垃圾,从而消除 GC 暂停影响音频合成性能的可能性。

音频工作线程 API 基于 Wasm 工作线程功能。也可以在定位音频工作线程时启用 -pthread 选项,但音频工作线程将始终在 Wasm 工作线程中运行,而不是在 Pthread 中。

开发概述

创作 Wasm 音频工作线程类似于在 JS 中开发基于音频工作线程 API 的应用程序(参见 MDN: 使用音频工作线程),不同之处在于用户不会手动实现音频工作线程全局范围内的脚本处理器节点文件的 JS 代码。这由 Emscripten Wasm 音频工作线程运行时自动管理。

相反,应用程序开发人员需要实现少量的 JS <-> Wasm (C/C++) 交互,以与 Wasm 中的音频上下文和音频节点进行交互。

音频工作线程在两层“类类型及其实例”设计上运行:首先定义一个或多个称为音频工作线程处理器的节点类型(或类),然后在音频处理图中将这些处理器实例化一次或多次作为音频工作线程节点。

一旦类类型在 Web 音频图上实例化并且图正在运行,就会为流经节点的每个 128 个处理音频流样本调用 C/C++ 函数指针回调。更新的 Web 音频 API 规范允许更改此值,因此为了将来兼容性,请使用 AudioSampleFramesamplesPerChannel 获取该值。

此回调将在具有实时处理优先级的专用独立音频处理线程上执行。每个 Web 音频上下文将仅使用一个音频处理线程。也就是说,即使存在多个音频节点实例(可能来自多个不同的音频处理器),它们也将在音频上下文的同一专用音频线程上共享,并且不会在各自的单独线程中运行。

注意:音频工作线程节点处理基于拉取模式回调。音频工作线程不允许创建通用的实时优先级线程。音频回调代码应尽快执行且不阻塞。换句话说,不可能旋转自定义的 for(;;) 循环。

编程示例

为了获得使用 Wasm 音频工作线程进行编程的实践经验,让我们创建一个简单的音频节点,通过其输出通道输出随机噪声。

1. 首先,我们将在 C/C++ 代码中创建一个 Web 音频上下文。这是通过 emscripten_create_audio_context() 函数实现的。在一个集成现有 Web 音频库的较大应用程序中,您可能已经通过其他库创建了 AudioContext,在这种情况下,您将通过调用函数 emscriptenRegisterAudioObject() 将该上下文注册为对 WebAssembly 可见。

然后,我们将指示 Emscripten 运行时在此上下文中初始化一个 Wasm 音频工作线程线程范围。实现这些任务的代码如下所示

#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. 当工作线程上下文初始化后,我们就可以定义我们自己的噪声生成器音频工作线程处理器节点类型了

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 编译代码以启用定位音频工作线程。

将音频线程与主线程同步

Wasm 音频工作线程 API 构建在 Emscripten Wasm 工作线程功能之上。这意味着 Wasm 音频工作线程被建模为 Wasm 工作线程。

为了在音频工作线程节点和应用程序中的其他线程之间同步信息,有三个选项

  1. 利用 Web 音频“音频参数”模型。每个音频工作线程处理器类型都使用一组自定义定义的音频参数实例化,这些参数可以以样本精确度影响音频计算。这些参数在音频处理函数中的 params 数组中传递。

    创建 Web 音频上下文的浏览器主线程可以在需要时调整这些参数的值。请参见 MDN 函数:setValueAtTime

  2. 可以使用 GCC/Clang 无锁原子操作、Emscripten 原子操作和 Wasm 工作线程 API 线程同步原语与音频工作线程共享数据。请参见 WASM_WORKERS 获取更多信息。

  3. 利用 emscripten_audio_worklet_post_function_*() 函数族。这些函数的工作原理类似于 emscripten_wasm_worker_post_function_*() 函数。它们启用 postMessage() 样式的通信,音频工作线程和浏览器主线程可以彼此发送消息(函数调用调度)。

更多示例

请参见目录 tests/webaudio/,以获取有关 Web 音频 API 和 Wasm 音频工作线程的更多代码示例。