Skip to content

Commit 5402fc9

Browse files
authored
Audio Worklets (#16449)
* Add first implementation of Wasm Audio Worklets, based on Wasm Workers. Fix Chrome not continuing the AudioWorklet processing if a number 1 is returned from the callback - must return 'true' specifically. Add new tone generator sample. Adjust comment. Use MessagePort.onmessage instead of add/removeEventListener(), since onmessage .start()s the MessagePort automatically. Fix name noise-generator to tone-generator Improve assertions. * Add src/audio_worklet.js to eslint ignore * Optimize code size * Add emscripten_current_thread_is_audio_worklet(), remove ENVIRONMENT_IS_AUDIO_WORKLET. * Fix to work with Closure * Simplify MINIMAL_RUNTIME shell module preamble generation. * Fix Closure and simple AudioWorklet creation * Fix Module import for AudioWorklets, and move towards globalThis * Disable -sAUDIO_WORKLET + -sTEXTDECODER=2 combo * Fix shell case * Mark AUDIO_WORKLET and SINGLE_FILE not mutually compatible * Default runtime support * Revert unnecessary code * Fix merge * AudioWorklets functioning in default runtime. * Remove #if WASM_WORKERS checks since Wasm Workers is unconditionally depended on. * Add more interactive tests * Test Audio Worklets with Closure
1 parent 44b2c2a commit 5402fc9

33 files changed

+1525
-53
lines changed

.eslintrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ignorePatterns:
2222
- "src/emrun_postjs.js"
2323
- "src/worker.js"
2424
- "src/wasm_worker.js"
25+
- "src/audio_worklet.js"
2526
- "src/wasm2js.js"
2627
- "src/webGLClient.js"
2728
- "src/webGLWorker.js"

ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ See docs/process.md for more on how version tagging works.
4040
- --pre-js and --post-js files are now fed through the JS preprocesor, just
4141
like JS library files and the core runtime JS files. This means they can
4242
now contain #if/#else/#endif blocks and {{{ }}} macro blocks. (#18525)
43+
- Added support for Wasm-based AudioWorklets for realtime audio processing
44+
(#16449)
4345
- `-sEXPORT_ALL` can now be used to export symbols on the `Module` object
4446
when used with `-sMINIMA_RUNTIME` and `-sMODULARIZE` together. (#17911)
4547
- The llvm version that emscripten uses was updated to 17.0.0 trunk.

emcc.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,7 +2360,7 @@ def phase_linker_setup(options, state, newargs):
23602360
if settings.WASM_WORKERS:
23612361
# TODO: After #15982 is resolved, these dependencies can be declared in library_wasm_worker.js
23622362
# instead of having to record them here.
2363-
wasm_worker_imports = ['_emscripten_wasm_worker_initialize']
2363+
wasm_worker_imports = ['_emscripten_wasm_worker_initialize', '___set_thread_state']
23642364
settings.EXPORTED_FUNCTIONS += wasm_worker_imports
23652365
building.user_requested_exports.update(wasm_worker_imports)
23662366
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_wasm_worker_initializeRuntime']
@@ -2369,6 +2369,19 @@ def phase_linker_setup(options, state, newargs):
23692369
settings.WASM_WORKER_FILE = unsuffixed(os.path.basename(target)) + '.ww.js'
23702370
settings.JS_LIBRARIES.append((0, shared.path_from_root('src', 'library_wasm_worker.js')))
23712371

2372+
settings.SUPPORTS_GLOBALTHIS = feature_matrix.caniuse(feature_matrix.Feature.GLOBALTHIS)
2373+
2374+
if settings.AUDIO_WORKLET:
2375+
if not settings.SUPPORTS_GLOBALTHIS:
2376+
exit_with_error('Must target recent enough browser versions that will support globalThis in order to target Wasm Audio Worklets!')
2377+
if settings.AUDIO_WORKLET == 1:
2378+
settings.AUDIO_WORKLET_FILE = unsuffixed(os.path.basename(target)) + '.aw.js'
2379+
settings.JS_LIBRARIES.append((0, shared.path_from_root('src', 'library_webaudio.js')))
2380+
if not settings.MINIMAL_RUNTIME:
2381+
# MINIMAL_RUNTIME exports these manually, since this export mechanism is placed
2382+
# in global scope that is not suitable for MINIMAL_RUNTIME loader.
2383+
settings.EXPORTED_RUNTIME_METHODS += ['stackSave', 'stackAlloc', 'stackRestore']
2384+
23722385
if settings.FORCE_FILESYSTEM and not settings.MINIMAL_RUNTIME:
23732386
# when the filesystem is forced, we export by default methods that filesystem usage
23742387
# may need, including filesystem usage from standalone file packager output (i.e.
@@ -3143,6 +3156,17 @@ def phase_final_emitting(options, state, target, wasm_target, memfile):
31433156
minified_worker = building.acorn_optimizer(worker_output, ['minifyWhitespace'], return_output=True)
31443157
write_file(worker_output, minified_worker)
31453158

3159+
# Deploy the Audio Worklet module bootstrap file (*.aw.js)
3160+
if settings.AUDIO_WORKLET == 1:
3161+
worklet_output = os.path.join(target_dir, settings.AUDIO_WORKLET_FILE)
3162+
with open(worklet_output, 'w') as f:
3163+
f.write(shared.read_and_preprocess(shared.path_from_root('src', 'audio_worklet.js'), expand_macros=True))
3164+
3165+
# Minify the audio_worklet.js file in optimized builds
3166+
if (settings.OPT_LEVEL >= 1 or settings.SHRINK_LEVEL >= 1) and not settings.DEBUG_LEVEL:
3167+
minified_worker = building.acorn_optimizer(worklet_output, ['minifyWhitespace'], return_output=True)
3168+
open(worklet_output, 'w').write(minified_worker)
3169+
31463170
# track files that will need native eols
31473171
generated_text_files_with_native_eols = []
31483172

@@ -3800,11 +3824,16 @@ def modularize():
38003824
38013825
return %(return_value)s
38023826
}
3827+
%(capture_module_function_for_audio_worklet)s
38033828
''' % {
38043829
'maybe_async': async_emit,
38053830
'EXPORT_NAME': settings.EXPORT_NAME,
38063831
'src': src,
3807-
'return_value': return_value
3832+
'return_value': return_value,
3833+
# Given the async nature of how the Module function and Module object come into existence in AudioWorkletGlobalScope,
3834+
# store the Module function under a different variable name so that AudioWorkletGlobalScope will be able to reference
3835+
# it without aliasing/conflicting with the Module variable name.
3836+
'capture_module_function_for_audio_worklet': 'globalThis.AudioWorkletModule = Module;' if settings.AUDIO_WORKLET and settings.MODULARIZE else ''
38083837
}
38093838

38103839
if settings.MINIMAL_RUNTIME and not settings.USE_PTHREADS:
@@ -3864,14 +3893,15 @@ def module_export_name_substitution():
38643893
logger.debug(f'Private module export name substitution with {settings.EXPORT_NAME}')
38653894
src = read_file(final_js)
38663895
final_js += '.module_export_name_substitution.js'
3867-
if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL:
3896+
if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL and not settings.AUDIO_WORKLET:
38683897
# On the web, with MINIMAL_RUNTIME, the Module object is always provided
38693898
# via the shell html in order to provide the .asm.js/.wasm content.
38703899
replacement = settings.EXPORT_NAME
38713900
else:
38723901
replacement = "typeof %(EXPORT_NAME)s !== 'undefined' ? %(EXPORT_NAME)s : {}" % {"EXPORT_NAME": settings.EXPORT_NAME}
3873-
src = re.sub(r'{\s*[\'"]?__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__[\'"]?:\s*1\s*}', replacement, src)
3874-
write_file(final_js, src)
3902+
new_src = re.sub(r'{\s*[\'"]?__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__[\'"]?:\s*1\s*}', replacement, src)
3903+
assert new_src != src, 'Unable to find Closure syntax __EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__ in source!'
3904+
write_file(final_js, new_src)
38753905
shared.get_temp_files().note(final_js)
38763906
save_intermediate('module_export_name_substitution')
38773907

site/source/docs/api_reference/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ high level it consists of:
2525
- :ref:`wasm_workers`:
2626
Enables writing multithreaded programs using a web-like API.
2727

28+
- :ref:`wasm_audio_worklets`:
29+
Allows programs to implement audio processing nodes that run in a dedicated real-time audio processing thread context.
30+
2831
- :ref:`Module`:
2932
Global JavaScript object that can be used to control code execution and access
3033
exported methods.
@@ -64,4 +67,5 @@ high level it consists of:
6467
fiber.h
6568
proxying.h
6669
wasm_workers
70+
wasm_audio_worklets
6771
advanced-apis
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
.. _wasm_audio_worklets:
2+
3+
=======================
4+
Wasm Audio Worklets API
5+
=======================
6+
7+
The AudioWorklet extension to the `Web Audio API specification
8+
<https://webaudio.github.io/web-audio-api/#AudioWorklet>`_ enables web sites
9+
to implement custom AudioWorkletProcessor Web Audio graph node types.
10+
11+
These custom processor nodes process audio data in real-time as part of the
12+
audio graph processing flow, and enable developers to write low latency
13+
sensitive audio processing code in JavaScript.
14+
15+
The Emscripten Wasm Audio Worklets API is an Emscripten-specific integration
16+
of these AudioWorklet nodes to WebAssembly. Wasm Audio Worklets enables
17+
developers to implement AudioWorklet processing nodes in C/C++ code that
18+
compile down to WebAssembly, rather than using JavaScript for the task.
19+
20+
Developing AudioWorkletProcessors in WebAssembly provides the benefit of
21+
improved performance compared to JavaScript, and the Emscripten
22+
Wasm Audio Worklets system runtime has been carefully developed to guarantee
23+
that no temporary JavaScript level VM garbage will be generated, eliminating
24+
the possibility of GC pauses from impacting audio synthesis performance.
25+
26+
Audio Worklets API is based on the Wasm Workers feature. It is possible to
27+
also enable the `-pthread` option while targeting Audio Worklets, but the
28+
audio worklets will always run in a Wasm Worker, and not in a Pthread.
29+
30+
Development Overview
31+
====================
32+
33+
Authoring Wasm Audio Worklets is similar to developing Audio Worklets
34+
API based applications in JS (see `MDN: Using AudioWorklets <https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_AudioWorklet>`_), with the exception that users will not manually implement
35+
the JS code for the ScriptProcessorNode files in the AudioWorkletGlobalScope.
36+
This is managed automatically by the Emscripten Wasm AudioWorklets runtime.
37+
38+
Instead, application developers will need to implement a small amount of JS <-> Wasm
39+
(C/C++) interop to interact with the AudioContext and AudioNodes from Wasm.
40+
41+
Audio Worklets operate on a two layer "class type & its instance" design:
42+
first one defines one or more node types (or classes) called AudioWorkletProcessors,
43+
and then, these processors are instantiated one or more times in the audio
44+
processing graph as AudioWorkletNodes.
45+
46+
Once a class type is instantiated on the Web Audio graph and the graph is
47+
running, a C/C++ function pointer callback will be invoked for each 128
48+
samples of the processed audio stream that flows through the node.
49+
50+
This callback will be executed on a dedicated separate audio processing
51+
thread with real-time processing priority. Each Web Audio context will
52+
utilize only a single audio processing thread. That is, even if there are
53+
multiple audio node instances (maybe from multiple different audio processors),
54+
these will all share the same dedicated audio thread on the AudioContext,
55+
and will not run in a separate thread of their own each.
56+
57+
Note: the audio worklet node processing is pull-mode callback based. Audio
58+
Worklets do not allow the creation of general purpose real-time prioritized
59+
threads. The audio callback code should execute as quickly as possible and
60+
be non-blocking. In other words, spinning a custom `for(;;)` loop is not
61+
possible.
62+
63+
Programming Example
64+
===================
65+
66+
To get hands-on experience with programming Wasm Audio Worklets, let's create a
67+
simple audio node that outputs random noise through its output channels.
68+
69+
1. First, we will create a Web Audio context in C/C++ code. This is achieved
70+
via the ``emscripten_create_audio_context()`` function. In a larger application
71+
that integrates existing Web Audio libraries, you may already have an
72+
``AudioContext`` created via some other library, in which case you would instead
73+
register that context to be visible to WebAssembly by calling the function
74+
``emscriptenRegisterAudioObject()``.
75+
76+
Then, we will instruct the Emscripten runtime to initialize a Wasm Audio Worklet
77+
thread scope on this context. The code to achieve these tasks looks like:
78+
79+
.. code-block:: cpp
80+
81+
#include <emscripten/webaudio.h>
82+
83+
uint8_t audioThreadStack[4096];
84+
85+
int main()
86+
{
87+
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0);
88+
89+
emscripten_start_wasm_audio_worklet_thread_async(context, audioThreadStack, sizeof(audioThreadStack),
90+
&AudioThreadInitialized, 0);
91+
}
92+
93+
2. When the worklet thread context has been initialized, we are ready to define our
94+
own noise generator AudioWorkletProcessor node type:
95+
96+
.. code-block:: cpp
97+
98+
void AudioThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData)
99+
{
100+
if (!success) return; // Check browser console in a debug build for detailed errors
101+
WebAudioWorkletProcessorCreateOptions opts = {
102+
.name = "noise-generator",
103+
};
104+
emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, &AudioWorkletProcessorCreated, 0);
105+
}
106+
107+
3. After the processor has initialized, we can now instantiate and connect it as a node on the graph. Since on
108+
web pages audio playback can only be initiated as a response to user input, we will also register an event handler
109+
which resumes the audio context when the user clicks on the DOM Canvas element that exists on the page.
110+
111+
.. code-block:: cpp
112+
113+
void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData)
114+
{
115+
if (!success) return; // Check browser console in a debug build for detailed errors
116+
117+
int outputChannelCounts[1] = { 1 };
118+
EmscriptenAudioWorkletNodeCreateOptions options = {
119+
.numberOfInputs = 0,
120+
.numberOfOutputs = 1,
121+
.outputChannelCounts = outputChannelCounts
122+
};
123+
124+
// Create node
125+
EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext,
126+
"noise-generator", &options, &GenerateNoise, 0);
127+
128+
// Connect it to audio context destination
129+
EM_ASM({emscriptenGetAudioObject($0).connect(emscriptenGetAudioObject($1).destination)},
130+
wasmAudioWorklet, audioContext);
131+
132+
// Resume context on mouse click
133+
emscripten_set_click_callback("canvas", (void*)audioContext, 0, OnCanvasClick);
134+
}
135+
136+
4. The code to resume the audio context on click looks like this:
137+
138+
.. code-block:: cpp
139+
140+
EM_BOOL OnCanvasClick(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
141+
{
142+
EMSCRIPTEN_WEBAUDIO_T audioContext = (EMSCRIPTEN_WEBAUDIO_T)userData;
143+
if (emscripten_audio_context_state(audioContext) != AUDIO_CONTEXT_STATE_RUNNING) {
144+
emscripten_resume_audio_context_sync(audioContext);
145+
}
146+
return EM_FALSE;
147+
}
148+
149+
5. Finally we can implement the audio callback that is to generate the noise:
150+
151+
.. code-block:: cpp
152+
153+
#include <emscripten/em_math.h>
154+
155+
EM_BOOL GenerateNoise(int numInputs, const AudioSampleFrame *inputs,
156+
int numOutputs, AudioSampleFrame *outputs,
157+
int numParams, const AudioParamFrame *params,
158+
void *userData)
159+
{
160+
for(int i = 0; i < numOutputs; ++i)
161+
for(int j = 0; j < 128*outputs[i].numberOfChannels; ++j)
162+
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
163+
164+
return EM_TRUE; // Keep the graph output going
165+
}
166+
167+
And that's it! Compile the code with the linker flags ``-sAUDIO_WORKLET=1 -sWASM_WORKERS=1`` to enable targeting AudioWorklets.
168+
169+
Synchronizing audio thread with the main thread
170+
===============================================
171+
172+
Wasm Audio Worklets API builds on top of the Emscripten Wasm Workers feature. This means that the Wasm Audio Worklet thread is modeled as if it was a Wasm Worker thread.
173+
174+
To synchronize information between an Audio Worklet Node and other threads in the application, there are two options:
175+
176+
1. Leverage the Web Audio "AudioParams" model. Each Audio Worklet Processor type is instantiated with a custom defined set of audio parameters that can affect the audio computation at sample precise accuracy. These parameters are passed in the ``params`` array into the audio processing function.
177+
178+
The main browser thread that created the Web Audio context can adjust the values of these parameters whenever desired. See `MDN function: setValueAtTime <https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setValueAtTime>`_ .
179+
180+
2. Data can be shared with the Audio Worklet thread using GCC/Clang lock-free atomics operations, Emscripten atomics operations and the Wasm Worker API thread synchronization primitives. See :ref:`wasm_workers` for more information.
181+
182+
3. Utilize the ``emscripten_audio_worklet_post_function_*()`` family of event passing functions. These functions operate similar to how the function family emscripten_wasm_worker_post_function_*()`` does. Posting functions enables a ``postMessage()`` style of communication, where the audio worklet thread and the main browser thread can send messages (function call dispatches) to each others.
183+
184+
185+
More Examples
186+
=============
187+
188+
See the directory tests/webaudio/ for more code examples on Web Audio API and Wasm AudioWorklets.

0 commit comments

Comments
 (0)