From 69cd5bb1a13fbf5f99dd04dbdc9b49a5afe77c96 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 12 Dec 2025 11:15:06 -0800 Subject: [PATCH] Add support for running output directly in Audio Worklet There are some use cases where its useful to be able to run emscripten generated code in an audio worklet without building with `-sAUDIO_WORKLET`. One major one is for deploying in a context where shared memory not available (i.e. no cross origin isolation) Fixes: #25943, #25943 --- ChangeLog.md | 7 ++++ .../api_reference/wasm_audio_worklets.rst | 8 ++++ .../tools_reference/settings_reference.rst | 10 +++++ src/preamble.js | 6 +++ src/settings.js | 10 +++++ src/settings_internal.js | 1 + src/shell.js | 15 +++++--- test/test_browser.py | 38 +++++++++++++++++++ tools/link.py | 10 +++-- 9 files changed, 97 insertions(+), 8 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f3a4fc16e1488..ee291b213e3d7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,13 @@ See docs/process.md for more on how version tagging works. 4.0.23 (in development) ----------------------- +- It is now possible to load emscripten-generated code directly into an Audio + Worklet context without using the `-sAUDIO_WORKLET` setting (which depends on + shared memory and `-sWASM_WORKERS`). To do this, build with + `-sENVIRONMENT=worklet`. In this environment, because audio worklets don't + have a fetch API, you will need to either use `-sSINGLE_FILE` (to embed the + Wasm file), or use a custom `instantiateWasm` callback to supply the + Wasm module yourself. (#25942) 4.0.22 - 12/18/25 ----------------- diff --git a/site/source/docs/api_reference/wasm_audio_worklets.rst b/site/source/docs/api_reference/wasm_audio_worklets.rst index 7f92d13bdf3d5..6aa51378fba65 100644 --- a/site/source/docs/api_reference/wasm_audio_worklets.rst +++ b/site/source/docs/api_reference/wasm_audio_worklets.rst @@ -27,6 +27,14 @@ Audio Worklets API is based on the Wasm Workers feature. It is possible to also enable the `-pthread` option while targeting Audio Worklets, but the audio worklets will always run in a Wasm Worker, and not in a Pthread. +.. note:: + If you want to load an emscripten-generated program into an AudioContext that + you have created yourself, without depending on shared memory or + :ref:`WASM_WORKERS` you can add ``worklet`` to :ref:`ENVIRONMENT`. In this + mode, because Audio Worklets do not have any kind of fetch API, you will need + either use `-sSINGLE_FILE` (to embed the Wasm file), or use a custom + `instantiateWasm` to supply the Wasm module yourself (e.g. via `postMessage`). + Development Overview ==================== diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 77871dc3d80af..5c9f3468d483e 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -991,6 +991,7 @@ are: - 'webview' - just like web, but in a webview like Cordova; considered to be same as "web" in almost every place - 'worker' - a web worker environment. +- 'worklet' - Audio Worklet environment. - 'node' - Node.js. - 'shell' - a JS shell like d8, js, or jsc. @@ -998,6 +999,10 @@ This setting can be a comma-separated list of these environments, e.g., "web,worker". If this is the empty string, then all environments are supported. +Certain settings will automatically add to this list. For examble, building +with pthreads will automatically add `worker` and building with +``AUDIO_WORKLET`` will automatically add `worklet`. + Note that the set of environments recognized here is not identical to the ones we identify at runtime using ``ENVIRONMENT_IS_*``. Specifically: @@ -2502,6 +2507,11 @@ AUDIO_WORKLET If true, enables targeting Wasm Web Audio AudioWorklets. Check out the full documentation in site/source/docs/api_reference/wasm_audio_worklets.rst +Note: The setting will implicitly add ``worklet`` to the :ref:`ENVIRONMENT`, +(i.e. the resulting code and run in a worklet environment) but additionaly +depends on ``WASM_WORKERS`` and Wasm SharedArrayBuffer to run new Audio +Worklets. + Default value: 0 .. _audio_worklet_support_audio_params: diff --git a/src/preamble.js b/src/preamble.js index 25bb5f0ebbd1e..ec5dad2a59b10 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -443,6 +443,12 @@ function findWasmBinary() { } #endif +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET && !AUDIO_WORKLET // AUDIO_WORKLET handled above + if (ENVIRONMENT_IS_AUDIO_WORKLET) { + return '{{{ WASM_BINARY_FILE }}}'; + } +#endif + if (Module['locateFile']) { return locateFile('{{{ WASM_BINARY_FILE }}}'); } diff --git a/src/settings.js b/src/settings.js index 1ba3b7856b8da..3b62f6b0c6650 100644 --- a/src/settings.js +++ b/src/settings.js @@ -646,6 +646,7 @@ var LEGACY_VM_SUPPORT = false; // - 'webview' - just like web, but in a webview like Cordova; considered to be // same as "web" in almost every place // - 'worker' - a web worker environment. +// - 'worklet' - Audio Worklet environment. // - 'node' - Node.js. // - 'shell' - a JS shell like d8, js, or jsc. // @@ -653,6 +654,10 @@ var LEGACY_VM_SUPPORT = false; // "web,worker". If this is the empty string, then all environments are // supported. // +// Certain settings will automatically add to this list. For examble, building +// with pthreads will automatically add `worker` and building with +// ``AUDIO_WORKLET`` will automatically add `worklet`. +// // Note that the set of environments recognized here is not identical to the // ones we identify at runtime using ``ENVIRONMENT_IS_*``. Specifically: // @@ -1629,6 +1634,11 @@ var WASM_WORKERS = 0; // If true, enables targeting Wasm Web Audio AudioWorklets. Check out the // full documentation in site/source/docs/api_reference/wasm_audio_worklets.rst +// +// Note: The setting will implicitly add ``worklet`` to the :ref:`ENVIRONMENT`, +// (i.e. the resulting code and run in a worklet environment) but additionaly +// depends on ``WASM_WORKERS`` and Wasm SharedArrayBuffer to run new Audio +// Worklets. // [link] var AUDIO_WORKLET = 0; diff --git a/src/settings_internal.js b/src/settings_internal.js index 0932bf6519db7..9f1f8ca63b5a5 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -140,6 +140,7 @@ var ENVIRONMENT_MAY_BE_WORKER = true; var ENVIRONMENT_MAY_BE_NODE = true; var ENVIRONMENT_MAY_BE_SHELL = true; var ENVIRONMENT_MAY_BE_WEBVIEW = true; +var ENVIRONMENT_MAY_BE_AUDIO_WORKLET = true; // Whether to minify import and export names in the minify_wasm_js stage. // Currently always off for MEMORY64. diff --git a/src/shell.js b/src/shell.js index 51d82c1f91c38..d96d1c47599cf 100644 --- a/src/shell.js +++ b/src/shell.js @@ -34,7 +34,7 @@ var Module = moduleArg; var Module; // if (!Module)` is crucial for Closure Compiler here as it will otherwise replace every `Module` occurrence with a string if (!Module) /** @suppress{checkTypes}*/Module = {"__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__":1}; -#elif AUDIO_WORKLET +#elif ENVIRONMENT_MAY_BE_AUDIO_WORKLET var Module = globalThis.Module || (typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {}); #else var Module = typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : {}; @@ -53,8 +53,11 @@ var Module = typeof {{{ EXPORT_NAME }}} != 'undefined' ? {{{ EXPORT_NAME }}} : { var ENVIRONMENT_IS_WASM_WORKER = globalThis.name == 'em-ww'; #endif -#if AUDIO_WORKLET +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET var ENVIRONMENT_IS_AUDIO_WORKLET = !!globalThis.AudioWorkletGlobalScope; +#endif + +#if AUDIO_WORKLET // Audio worklets behave as wasm workers. if (ENVIRONMENT_IS_AUDIO_WORKLET) ENVIRONMENT_IS_WASM_WORKER = true; #endif @@ -79,7 +82,7 @@ var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; // N.b. Electron.js environment is simultaneously a NODE-environment, but // also a web environment. var ENVIRONMENT_IS_NODE = {{{ nodeDetectionCode() }}}; -#if AUDIO_WORKLET +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_AUDIO_WORKLET; #else var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; @@ -352,7 +355,9 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { } } else #endif // ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER -#if AUDIO_WORKLET && ASSERTIONS +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET +#endif +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET && ASSERTIONS if (!ENVIRONMENT_IS_AUDIO_WORKLET) #endif { @@ -401,7 +406,7 @@ if (ENVIRONMENT_IS_NODE) { // if an assertion fails it cannot print the message #if PTHREADS assert( -#if AUDIO_WORKLET +#if ENVIRONMENT_MAY_BE_AUDIO_WORKLET ENVIRONMENT_IS_AUDIO_WORKLET || #endif ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)'); diff --git a/test/test_browser.py b/test/test_browser.py index 3a072c4221d4d..89247368832c5 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5480,6 +5480,44 @@ def test_audio_worklet_params_mixing(self, args): def test_audio_worklet_emscripten_locks(self): self.btest_exit('webaudio/audioworklet_emscripten_locks.c', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread']) + def test_audio_worklet_direct(self): + self.add_browser_reporting() + self.emcc('hello_world.c', ['-o', 'hello_world.mjs', '-sEXPORT_ES6', '-sSINGLE_FILE', '-sENVIRONMENT=worklet']) + create_file('worklet.mjs', ''' + import Module from "./hello_world.mjs" + console.log("in worklet"); + class MyProcessor extends AudioWorkletProcessor { + constructor() { + super(); + Module().then(() => { + console.log("done Module constructor"); + this.port.postMessage("ready"); + }); + } + process(inputs, outputs, parameters) { + return true; + } + } + registerProcessor('my-processor', MyProcessor); + console.log("done register"); + ''') + create_file('test.html', ''' + + + ''') + self.run_browser('test.html', '/report_result?ready') + # Verifies setting audio context sample rate, and that emscripten_audio_context_sample_rate() works. @requires_sound_hardware @also_with_minimal_runtime diff --git a/tools/link.py b/tools/link.py index ecb2df25adaa6..67de2fbf49c08 100644 --- a/tools/link.py +++ b/tools/link.py @@ -67,7 +67,7 @@ '__main_argc_argv', ] -VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell') +VALID_ENVIRONMENTS = {'web', 'webview', 'worker', 'node', 'shell', 'worklet'} EXECUTABLE_EXTENSIONS = ['.wasm', '.html', '.js', '.mjs', '.out', ''] @@ -184,6 +184,9 @@ def setup_environment_settings(): if settings.SHARED_MEMORY and settings.ENVIRONMENT: settings.ENVIRONMENT.append('worker') + if settings.AUDIO_WORKLET: + settings.ENVIRONMENT.append('worklet') + # Environment setting based on user input if any(x for x in settings.ENVIRONMENT if x not in VALID_ENVIRONMENTS): exit_with_error(f'Invalid environment specified in "ENVIRONMENT": {settings.ENVIRONMENT}. Should be one of: {",".join(VALID_ENVIRONMENTS)}') @@ -193,6 +196,7 @@ def setup_environment_settings(): settings.ENVIRONMENT_MAY_BE_NODE = not settings.ENVIRONMENT or 'node' in settings.ENVIRONMENT settings.ENVIRONMENT_MAY_BE_SHELL = not settings.ENVIRONMENT or 'shell' in settings.ENVIRONMENT settings.ENVIRONMENT_MAY_BE_WORKER = not settings.ENVIRONMENT or 'worker' in settings.ENVIRONMENT + settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET = not settings.ENVIRONMENT or 'worklet' in settings.ENVIRONMENT if not settings.ENVIRONMENT_MAY_BE_NODE: if 'MIN_NODE_VERSION' in user_settings and settings.MIN_NODE_VERSION != feature_matrix.UNSUPPORTED: @@ -1175,7 +1179,7 @@ def limit_incoming_module_api(): # In Audio Worklets TextDecoder API is intentionally not exposed # (https://github.com/WebAudio/web-audio-api/issues/2499) so we also need to # keep the JavaScript-based fallback. - if settings.SHRINK_LEVEL >= 2 and not settings.AUDIO_WORKLET and \ + if settings.SHRINK_LEVEL >= 2 and not settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET and \ not settings.ENVIRONMENT_MAY_BE_SHELL: default_setting('TEXTDECODER', 2) @@ -2472,7 +2476,7 @@ def module_export_name_substitution(): logger.debug(f'Private module export name substitution with {settings.EXPORT_NAME}') src = read_file(final_js) final_js += '.module_export_name_substitution.js' - if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL and not settings.AUDIO_WORKLET: + if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL and not settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET: # On the web, with MINIMAL_RUNTIME, the Module object is always provided # via the shell html in order to provide the .asm.js/.wasm content. replacement = settings.EXPORT_NAME