From 00cc27db15a0ca33116f8a376d49693b30421426 Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Fri, 28 Nov 2025 11:19:07 +0100 Subject: [PATCH 1/7] POC of ensuring Audio worklet destroying --- src/audio_worklet.js | 9 +++++++++ src/lib/libwebaudio.js | 1 + 2 files changed, 10 insertions(+) diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 26dfa5d3ad823..81b119ce4cd06 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -33,6 +33,7 @@ function createWasmAudioWorkletProcessor() { assert(opts.callback) assert(opts.samplesPerChannel) #endif + this.port.onmessage = this.onmessage; this.callback = {{{ makeDynCall('iipipipp', 'opts.callback') }}}; this.userData = opts.userData; // Then the samples per channel to process, fixed for the lifetime of the @@ -86,6 +87,12 @@ function createWasmAudioWorkletProcessor() { } #endif + onmessage(msg) { + if (msg['stop']) { + this.stopped = true; + } + } + /** * Marshals all inputs and parameters to the Wasm memory on the thread's * stack, then performs the wasm audio worklet call, and finally marshals @@ -100,6 +107,8 @@ function createWasmAudioWorkletProcessor() { process(inputList, outputList) { #endif + if (this.stopped) return false; + #if ALLOW_MEMORY_GROWTH // Recreate the output views if the heap has changed // TODO: add support for GROWABLE_ARRAYBUFFERS diff --git a/src/lib/libwebaudio.js b/src/lib/libwebaudio.js index 2c028659ff796..a43a799994534 100644 --- a/src/lib/libwebaudio.js +++ b/src/lib/libwebaudio.js @@ -160,6 +160,7 @@ var LibraryWebAudio = { #endif // Explicitly disconnect the node from Web Audio graph before letting it GC, // to work around browser bugs such as https://webkit.org/b/222098#c23 + EmAudio[objectHandle].port.postMessage({'stop': true}); EmAudio[objectHandle].disconnect(); delete EmAudio[objectHandle]; }, From 1d45ba9c074c63f7cb738ae0babfd96821e13004 Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Fri, 28 Nov 2025 12:45:56 +0100 Subject: [PATCH 2/7] Add emscripten_destroy_web_audio_node_async --- src/audio_worklet.js | 5 +++++ src/lib/libsigs.js | 1 + src/lib/libwebaudio.js | 28 ++++++++++++++++++++++++++-- system/include/emscripten/webaudio.h | 13 ++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 81b119ce4cd06..e127e73d1a046 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -90,6 +90,11 @@ function createWasmAudioWorkletProcessor() { onmessage(msg) { if (msg['stop']) { this.stopped = true; + if (msg['cb']) { + // Send the same message back so that the main thread can verify that + // the Worklet has stopped + this.postMessage(msg); + } } } diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 932ec27daaf4f..0a031ebc2192f 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -628,6 +628,7 @@ sigs = { emscripten_debugger__sig: 'v', emscripten_destroy_audio_context__sig: 'vi', emscripten_destroy_web_audio_node__sig: 'vi', + emscripten_destroy_web_audio_node_async__sig: 'vipp', emscripten_destroy_worker__sig: 'vi', emscripten_enter_soft_fullscreen__sig: 'ipp', emscripten_err__sig: 'vp', diff --git a/src/lib/libwebaudio.js b/src/lib/libwebaudio.js index a43a799994534..6155aa9c83a03 100644 --- a/src/lib/libwebaudio.js +++ b/src/lib/libwebaudio.js @@ -160,7 +160,22 @@ var LibraryWebAudio = { #endif // Explicitly disconnect the node from Web Audio graph before letting it GC, // to work around browser bugs such as https://webkit.org/b/222098#c23 - EmAudio[objectHandle].port.postMessage({'stop': true}); + EmAudio[objectHandle].port.postMessage({'stop': 1}); + EmAudio[objectHandle].disconnect(); + delete EmAudio[objectHandle]; + }, + + emscripten_destroy_web_audio_node_async: (objectHandle, callback, userData) => { +#if ASSERTIONS || WEBAUDIO_DEBUG + emAudioExpectNode(objectHandle, 'emscripten_destroy_web_audio_node_async'); +#endif + // Explicitly disconnect the node from Web Audio graph before letting it GC, + // to work around browser bugs such as https://webkit.org/b/222098#c23 + EmAudio[objectHandle].port.postMessage({ + 'stop': 1, + 'cb': callback, + 'ud': userData, + }); EmAudio[objectHandle].disconnect(); delete EmAudio[objectHandle]; }, @@ -353,7 +368,16 @@ var LibraryWebAudio = { dbg(`Creating AudioWorkletNode "${UTF8ToString(name)}" on context=${contextHandle} with options:`); console.dir(opts); #endif - return emscriptenRegisterAudioObject(new AudioWorkletNode(EmAudio[contextHandle], UTF8ToString(name), opts)); + + const node = new AudioWorkletNode(EmAudio[contextHandle], UTF8ToString(name), opts); + node.port.onmessage = (msg) => { + if (msg['stop']) { + var cb = msg['cb']; + callUserCallback(() => {{{ makeDynCall('vp', 'cb') }}}(msg['ud'])); + } + }; + + return emscriptenRegisterAudioObject(node); }, #endif // ~AUDIO_WORKLET diff --git a/system/include/emscripten/webaudio.h b/system/include/emscripten/webaudio.h index 1daa3521d9e46..2365a1079dd5f 100644 --- a/system/include/emscripten/webaudio.h +++ b/system/include/emscripten/webaudio.h @@ -60,11 +60,22 @@ typedef void (*EmscriptenStartWebAudioWorkletCallback)(EMSCRIPTEN_WEBAUDIO_T aud // after calling this function. void emscripten_destroy_audio_context(EMSCRIPTEN_WEBAUDIO_T audioContext); -// Disconnects the given audio node from its audio graph, and then releases +// Disconnects the given audio node from its audio graph, make sure the +// process callback is not called anymore and then releases // the JS object table reference to the given audio node. The specified handle // is invalid after calling this function. +// The process callback can be called after this function is called. +// If you need to ensure that the process callback is not called anymore, use +// emscripten_destroy_web_audio_node_async() instead. void emscripten_destroy_web_audio_node(EMSCRIPTEN_WEBAUDIO_T objectHandle); +// Disconnects the given audio node from its audio graph, make sure the +// process callback is not called anymore and then releases +// the JS object table reference to the given audio node. The specified handle +// is invalid after calling this function. +// Once the node has been verified to be stopped, the callback will be called. +void emscripten_destroy_web_audio_node_async(EMSCRIPTEN_WEBAUDIO_T objectHandle, EmscriptenDestroyWebAudioNodeCallback callback, void *userData3); + // Create Wasm AudioWorklet thread. Call this function once at application startup to establish an AudioWorkletGlobalScope for your app. // After the scope has been initialized, the given callback will fire. // audioContext: The Web Audio context object to initialize the Wasm AudioWorklet thread on. Each AudioContext can have only one AudioWorklet From ab997130c846cb48a99fe54c201d36724e0d1ce2 Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Mon, 1 Dec 2025 12:25:52 +0100 Subject: [PATCH 3/7] Fixed so that the .data is checked instead of directly on the message --- src/audio_worklet.js | 9 +++++---- src/lib/libwebaudio.js | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/audio_worklet.js b/src/audio_worklet.js index e127e73d1a046..968ec5a37ebe5 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -33,7 +33,7 @@ function createWasmAudioWorkletProcessor() { assert(opts.callback) assert(opts.samplesPerChannel) #endif - this.port.onmessage = this.onmessage; + this.port.onmessage = this.onmessage.bind(this); this.callback = {{{ makeDynCall('iipipipp', 'opts.callback') }}}; this.userData = opts.userData; // Then the samples per channel to process, fixed for the lifetime of the @@ -88,12 +88,13 @@ function createWasmAudioWorkletProcessor() { #endif onmessage(msg) { - if (msg['stop']) { + var data = msg.data; + if (data['stop']) { this.stopped = true; - if (msg['cb']) { + if (data['cb']) { // Send the same message back so that the main thread can verify that // the Worklet has stopped - this.postMessage(msg); + this.port.postMessage(data); } } } diff --git a/src/lib/libwebaudio.js b/src/lib/libwebaudio.js index 6155aa9c83a03..b34d7ef40472d 100644 --- a/src/lib/libwebaudio.js +++ b/src/lib/libwebaudio.js @@ -371,9 +371,10 @@ var LibraryWebAudio = { const node = new AudioWorkletNode(EmAudio[contextHandle], UTF8ToString(name), opts); node.port.onmessage = (msg) => { - if (msg['stop']) { - var cb = msg['cb']; - callUserCallback(() => {{{ makeDynCall('vp', 'cb') }}}(msg['ud'])); + var data = msg.data; + if (data['stop']) { + var cb = data['cb']; + callUserCallback(() => {{{ makeDynCall('vp', 'cb') }}}(data['ud'])); } }; From 90d6bee4574685a143737d8d5234019609f8a593 Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Mon, 1 Dec 2025 12:29:55 +0100 Subject: [PATCH 4/7] Add missing function def --- system/include/emscripten/webaudio.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/include/emscripten/webaudio.h b/system/include/emscripten/webaudio.h index 2365a1079dd5f..8cee21de83ac1 100644 --- a/system/include/emscripten/webaudio.h +++ b/system/include/emscripten/webaudio.h @@ -69,6 +69,8 @@ void emscripten_destroy_audio_context(EMSCRIPTEN_WEBAUDIO_T audioContext); // emscripten_destroy_web_audio_node_async() instead. void emscripten_destroy_web_audio_node(EMSCRIPTEN_WEBAUDIO_T objectHandle); +typedef void (*EmscriptenDestroyWebAudioNodeCallback)(void *userData3); + // Disconnects the given audio node from its audio graph, make sure the // process callback is not called anymore and then releases // the JS object table reference to the given audio node. The specified handle From bfcc86b22e072349a9e63fae1de9b19cf7f8892c Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Mon, 1 Dec 2025 12:32:11 +0100 Subject: [PATCH 5/7] Added interactive.test_audio_worklet_destroy_async test --- test/test_interactive.py | 3 + test/webaudio/audioworket_destroy_async.c | 107 ++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 test/webaudio/audioworket_destroy_async.c diff --git a/test/test_interactive.py b/test/test_interactive.py index 5581394e60058..b36ab3366ca24 100644 --- a/test/test_interactive.py +++ b/test/test_interactive.py @@ -298,6 +298,9 @@ def test_audio_worklet(self): self.btest('webaudio/audioworklet.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '--preload-file', test_file('hello_world.c') + '@/']) self.btest('webaudio/audioworklet.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread']) + def test_audio_worklet_destroy_async(self): + self.btest('webaudio/audioworket_destroy_async.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread', '-lwebaudio.js']) + # Tests a second AudioWorklet example: sine wave tone generator. def test_audio_worklet_tone_generator(self): self.btest('webaudio/audio_worklet_tone_generator.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS']) diff --git a/test/webaudio/audioworket_destroy_async.c b/test/webaudio/audioworket_destroy_async.c new file mode 100644 index 0000000000000..516004c15cd49 --- /dev/null +++ b/test/webaudio/audioworket_destroy_async.c @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include + +#ifdef REPORT_RESULT +volatile int audioProcessedCount = 0; +int valueAfterDestroy; +#endif + +EMSCRIPTEN_AUDIO_WORKLET_NODE_T node_id; + +bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { +#ifdef REPORT_RESULT + ++audioProcessedCount; +#endif + + // Produce noise in all output channels. + for(int i = 0; i < numOutputs; ++i) + for(int j = 0; j < outputs[i].samplesPerChannel*outputs[i].numberOfChannels; ++j) + outputs[i].data[j] = (rand() / (float)RAND_MAX * 2.0f - 1.0f) * 0.3f; + + return true; +} + +void observe_after_destroy(void * userData) { + printf("Expected processed count to be %d, was %d\n", valueAfterDestroy, audioProcessedCount); + +#ifdef REPORT_RESULT + if (audioProcessedCount == valueAfterDestroy) { + printf("Test PASSED!\n"); + REPORT_RESULT(0); + } else { + printf("Test FAILED!\n"); + REPORT_RESULT(1); + } +#endif +} + +void AudioWorkletDestroyed(void* userData) { + emscripten_out("AudioWorkletDestroyed"); +#ifdef REPORT_RESULT + valueAfterDestroy = audioProcessedCount; +#endif + emscripten_set_timeout(observe_after_destroy, 1000, 0); +} + +void observe_after_start(void *userData) { +#ifdef REPORT_RESULT + if (audioProcessedCount == 0) { + printf("Test FAILED!\n"); + REPORT_RESULT(1); + } +#endif + + emscripten_destroy_web_audio_node_async(node_id, &AudioWorkletDestroyed, 0); +} + +// This callback will fire after the Audio Worklet Processor has finished being +// added to the Worklet global scope. +void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { + if (!success) return; + + emscripten_out("AudioWorkletProcessorCreated"); + + // Specify the input and output node configurations for the Wasm Audio + // Worklet. A simple setup with single mono output channel here, and no + // inputs. + int outputChannelCounts[1] = { 1 }; + + EmscriptenAudioWorkletNodeCreateOptions options = { + .numberOfInputs = 0, + .numberOfOutputs = 1, + .outputChannelCounts = outputChannelCounts + }; + + // Instantiate the counter-incrementer Audio Worklet Processor. + node_id = emscripten_create_wasm_audio_worklet_node(audioContext, "counter-incrementer", &options, &ProcessAudio, 0); + emscripten_audio_node_connect(node_id, audioContext, 0, 0); + + // Wait 1s to check that the counter has started incrementing + emscripten_set_timeout(observe_after_start, 1000, 0); +} + +// This callback will fire when the audio worklet thread has been initialized. +void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { + if (!success) return; + + emscripten_out("WebAudioWorkletThreadInitialized"); + + WebAudioWorkletProcessorCreateOptions opts = { + .name = "counter-incrementer", + }; + emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0); +} + +uint8_t wasmAudioWorkletStack[4096]; + +int main() { + EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL); + + emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); + + return 0; +} From c5588c61094b8bbe22abeacb37417d1a73d544e6 Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Mon, 1 Dec 2025 13:09:32 +0100 Subject: [PATCH 6/7] Fixed filename --- test/test_interactive.py | 2 +- ...audioworket_destroy_async.c => audioworklet_destroy_async.c} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/webaudio/{audioworket_destroy_async.c => audioworklet_destroy_async.c} (100%) diff --git a/test/test_interactive.py b/test/test_interactive.py index b36ab3366ca24..55e337d4bf1d5 100644 --- a/test/test_interactive.py +++ b/test/test_interactive.py @@ -299,7 +299,7 @@ def test_audio_worklet(self): self.btest('webaudio/audioworklet.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread']) def test_audio_worklet_destroy_async(self): - self.btest('webaudio/audioworket_destroy_async.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread', '-lwebaudio.js']) + self.btest('webaudio/audioworklet_destroy_async.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread', '-lwebaudio.js']) # Tests a second AudioWorklet example: sine wave tone generator. def test_audio_worklet_tone_generator(self): diff --git a/test/webaudio/audioworket_destroy_async.c b/test/webaudio/audioworklet_destroy_async.c similarity index 100% rename from test/webaudio/audioworket_destroy_async.c rename to test/webaudio/audioworklet_destroy_async.c From 1c190919712ab492f93c3e925755b28b4e91796c Mon Sep 17 00:00:00 2001 From: Johan Lindell Date: Mon, 1 Dec 2025 13:32:36 +0100 Subject: [PATCH 7/7] Added `this.stopped = false;` in contructor --- src/audio_worklet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 968ec5a37ebe5..b2d4829e6dcea 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -33,6 +33,7 @@ function createWasmAudioWorkletProcessor() { assert(opts.callback) assert(opts.samplesPerChannel) #endif + this.stopped = false; this.port.onmessage = this.onmessage.bind(this); this.callback = {{{ makeDynCall('iipipipp', 'opts.callback') }}}; this.userData = opts.userData;