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);
}
クリック時にオーディオコンテキストを再開するコードは次のようになります。
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;
}
最後に、ノイズを生成するオーディオコールバックを実装できます。
#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つのオプションがあります。
Web Audio「AudioParams」モデルを活用します。各Audio Worklet Processor型は、サンプル精度の精度でオーディオ計算に影響を与えることができるカスタム定義のオーディオパラメーターセットでインスタンス化されます。これらのパラメーターは、オーディオ処理関数に`params`配列で渡されます。
Web Audioコンテキストを作成したメインブラウザー スレッドは、必要に応じていつでもこれらのパラメーターの値を調整できます。MDN関数:setValueAtTimeを参照してください。
データは、GCC/Clangのロックフリーアトミック演算、Emscriptenのアトミック演算、およびWasm Worker APIのスレッド同期プリミティブを使用して、Audio Workletスレッドと共有できます。詳細はWASM_WORKERSを参照してください。
emscripten_audio_worklet_post_function_*()
ファミリのイベントパッシング関数を使用します。これらの関数は、emscripten_wasm_worker_post_function_*()
関数と同様に動作します。これらはpostMessage()
スタイルの通信を可能にし、Audio Workletスレッドとメインブラウザスレッドがお互いにメッセージ(関数呼び出しのディスパッチ)を送信できます。
Web Audio APIとWasm AudioWorkletsに関するその他のコード例については、tests/webaudio/ディレクトリを参照してください。