Wasm Audio Worklets API

Web Audio API仕様のAudioWorklet拡張により、ウェブサイトはカスタムAudioWorkletProcessor Web Audioグラフノードタイプを実装できます。

これらのカスタムプロセッサノードは、オーディオグラフ処理フローの一部としてリアルタイムでオーディオデータを処理し、開発者はJavaScriptではなく、低遅延に敏感なオーディオ処理コードをJavaScriptで記述できます。

Emscripten Wasm Audio Worklets APIは、これらのAudioWorkletノードをWebAssemblyに統合するEmscripten固有の統合です。Wasm Audio Workletsにより、開発者は、このタスクにJavaScriptを使用するのではなく、WebAssemblyにコンパイルされるC/C++コードでAudioWorklet処理ノードを実装できます。

WebAssemblyでAudioWorkletProcessorを開発することには、JavaScriptと比較してパフォーマンスが向上するという利点があり、Emscripten Wasm Audio Workletsシステムランタイムは、一時的なJavaScriptレベルのVMガベージが生成されないことを保証するために慎重に開発されており、GC一時停止によるオーディオ合成パフォーマンスへの影響の可能性を排除しています。

Audio Worklets APIはWasm Workers機能に基づいています。Audio Workletsをターゲットにする際に`-pthread`オプションを有効にすることもできますが、オーディオワークレットは常にWasm Workerで実行され、Pthreadでは実行されません。

開発概要

Wasm Audio Workletsの作成は、JSでAudio Worklets APIベースのアプリケーションを開発するのと似ています(MDN:AudioWorkletsの使用を参照)。ただし、ユーザーはAudioWorkletGlobalScopeでScriptProcessorNodeファイルのJSコードを手動で実装する必要はありません。これはEmscripten Wasm AudioWorkletsランタイムによって自動的に管理されます。

代わりに、アプリケーション開発者は、WasmからAudioContextとAudioNodesと対話するために、少量のJS <-> Wasm(C/C++)インターオペレーションを実装する必要があります。

Audio Workletsは、2層の「クラス型とそのインスタンス」設計で動作します。最初に、AudioWorkletProcessorsと呼ばれる1つ以上のノード型(またはクラス)を定義し、次に、これらのプロセッサはオーディオ処理グラフでAudioWorkletNodesとして1回以上インスタンス化されます。

クラス型がWeb Audioグラフでインスタンス化され、グラフが実行されると、ノードを通過する処理済みオーディオストリームの128サンプルごとに、C/C++関数ポインタコールバックが呼び出されます。新しいWeb Audio API仕様ではこれを変更できるため、将来の互換性のために、`AudioSampleFrame`の`samplesPerChannel`を使用して値を取得してください。

このコールバックは、リアルタイム処理優先度を持つ専用の個別のオーディオ処理スレッドで実行されます。各Web Audioコンテキストは、単一のオーディオ処理スレッドのみを使用します。つまり、複数のオーディオノードインスタンス(おそらく複数の異なるオーディオプロセッサからのもの)があっても、これらはすべてAudioContextで同じ専用のオーディオスレッドを共有し、それぞれ個別のスレッドで実行されることはありません。

注:オーディオワークレットノードの処理はプルモードのコールバックベースです。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. ワークレットスレッドコンテキストが初期化されると、独自のノイズジェネレーター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ノードとアプリケーションの他のスレッド間の情報を同期するには、3つのオプションがあります。

  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()スタイルの通信を可能にし、Audio Workletスレッドとメインブラウザスレッドがお互いにメッセージ(関数呼び出しのディスパッチ)を送信できます。

その他の例

Web Audio APIとWasm AudioWorkletsに関するその他のコード例については、tests/webaudio/ディレクトリを参照してください。