diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 26dfa5d3ad823..b2d4829e6dcea 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -33,6 +33,8 @@ 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; // Then the samples per channel to process, fixed for the lifetime of the @@ -86,6 +88,18 @@ function createWasmAudioWorkletProcessor() { } #endif + onmessage(msg) { + var data = msg.data; + if (data['stop']) { + this.stopped = true; + if (data['cb']) { + // Send the same message back so that the main thread can verify that + // the Worklet has stopped + this.port.postMessage(data); + } + } + } + /** * 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 +114,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/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 2c028659ff796..b34d7ef40472d 100644 --- a/src/lib/libwebaudio.js +++ b/src/lib/libwebaudio.js @@ -160,6 +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': 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]; }, @@ -352,7 +368,17 @@ 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) => { + var data = msg.data; + if (data['stop']) { + var cb = data['cb']; + callUserCallback(() => {{{ makeDynCall('vp', 'cb') }}}(data['ud'])); + } + }; + + return emscriptenRegisterAudioObject(node); }, #endif // ~AUDIO_WORKLET diff --git a/system/include/emscripten/webaudio.h b/system/include/emscripten/webaudio.h index 1daa3521d9e46..8cee21de83ac1 100644 --- a/system/include/emscripten/webaudio.h +++ b/system/include/emscripten/webaudio.h @@ -60,11 +60,24 @@ 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); +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 +// 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 diff --git a/test/test_interactive.py b/test/test_interactive.py index 5581394e60058..55e337d4bf1d5 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/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): self.btest('webaudio/audio_worklet_tone_generator.c', expected='0', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS']) diff --git a/test/webaudio/audioworklet_destroy_async.c b/test/webaudio/audioworklet_destroy_async.c new file mode 100644 index 0000000000000..516004c15cd49 --- /dev/null +++ b/test/webaudio/audioworklet_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; +}