diff --git a/src/audio_worklet.js b/src/audio_worklet.js index aa21c589379be..e57792885ede7 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -1,14 +1,16 @@ -// This file is the main bootstrap script for Wasm Audio Worklets loaded in an Emscripten application. -// Build with -sAUDIO_WORKLET=1 linker flag to enable targeting Audio Worklets. +// This file is the main bootstrap script for Wasm Audio Worklets loaded in an +// Emscripten application. Build with -sAUDIO_WORKLET=1 linker flag to enable +// targeting Audio Worklets. -// AudioWorkletGlobalScope does not have a onmessage/postMessage() functionality at the global scope, which -// means that after creating an AudioWorkletGlobalScope and loading this script into it, we cannot +// AudioWorkletGlobalScope does not have a onmessage/postMessage() functionality +// at the global scope, which means that after creating an +// AudioWorkletGlobalScope and loading this script into it, we cannot // postMessage() information into it like one would do with Web Workers. -// Instead, we must create an AudioWorkletProcessor class, then instantiate a Web Audio graph node from it -// on the main thread. Using its message port and the node constructor's -// "processorOptions" field, we can share the necessary bootstrap information from the main thread to -// the AudioWorkletGlobalScope. +// Instead, we must create an AudioWorkletProcessor class, then instantiate a +// Web Audio graph node from it on the main thread. Using its message port and +// the node constructor's "processorOptions" field, we can share the necessary +// bootstrap information from the main thread to the AudioWorkletGlobalScope. function createWasmAudioWorkletProcessor(audioParams) { class WasmAudioWorkletProcessor extends AudioWorkletProcessor { @@ -98,8 +100,9 @@ function createWasmAudioWorkletProcessor(audioParams) { // Call out to Wasm callback to perform audio processing if (didProduceAudio = this.callbackFunction(numInputs, inputsPtr, numOutputs, outputsPtr, numParams, paramsPtr, this.userData)) { // Read back the produced audio data to all outputs and their channels. - // (A garbage-free function TypedArray.copy(dstTypedArray, dstOffset, srcTypedArray, srcOffset, count) would sure be handy.. - // but web does not have one, so manually copy all bytes in) + // (A garbage-free function TypedArray.copy(dstTypedArray, dstOffset, + // srcTypedArray, srcOffset, count) would sure be handy.. but web does + // not have one, so manually copy all bytes in) for (i of outputList) { for (j of i) { for (k = 0; k < 128; ++k) { @@ -111,24 +114,28 @@ function createWasmAudioWorkletProcessor(audioParams) { stackRestore(oldStackPtr); - // Return 'true' to tell the browser to continue running this processor. (Returning 1 or any other truthy value won't work in Chrome) + // Return 'true' to tell the browser to continue running this processor. + // (Returning 1 or any other truthy value won't work in Chrome) return !!didProduceAudio; } } return WasmAudioWorkletProcessor; } -// Specify a worklet processor that will be used to receive messages to this AudioWorkletGlobalScope. -// We never connect this initial AudioWorkletProcessor to the audio graph to do any audio processing. +// Specify a worklet processor that will be used to receive messages to this +// AudioWorkletGlobalScope. We never connect this initial AudioWorkletProcessor +// to the audio graph to do any audio processing. class BootstrapMessages extends AudioWorkletProcessor { constructor(arg) { super(); - // Initialize the global Emscripten Module object that contains e.g. the Wasm Module and Memory objects. - // After this we are ready to load in the main application JS script, which the main thread will addModule() + // Initialize the global Emscripten Module object that contains e.g. the + // Wasm Module and Memory objects. After this we are ready to load in the + // main application JS script, which the main thread will addModule() // to this scope. globalThis.Module = arg['processorOptions']; #if !MINIMAL_RUNTIME - // Default runtime relies on an injected instantiateWasm() function to initialize the Wasm Module. + // Default runtime relies on an injected instantiateWasm() function to + // initialize the Wasm Module. globalThis.Module['instantiateWasm'] = (info, receiveInstance) => { var instance = new WebAssembly.Instance(Module['wasm'], info); receiveInstance(instance, Module['wasm']); @@ -139,18 +146,25 @@ class BootstrapMessages extends AudioWorkletProcessor { console.log('AudioWorklet global scope looks like this:'); console.dir(globalThis); #endif - // Listen to messages from the main thread. These messages will ask this scope to create the real - // AudioWorkletProcessors that call out to Wasm to do audio processing. + // Listen to messages from the main thread. These messages will ask this + // scope to create the real AudioWorkletProcessors that call out to Wasm to + // do audio processing. let p = globalThis['messagePort'] = this.port; p.onmessage = (msg) => { let d = msg.data; - if (d['_wpn']) { // '_wpn' is short for 'Worklet Processor Node', using an identifier that will never conflict with user messages + if (d['_wpn']) { + // '_wpn' is short for 'Worklet Processor Node', using an identifier + // that will never conflict with user messages #if MODULARIZE - // Instantiate the MODULARIZEd Module function, which is stored for us under the special global - // name AudioWorkletModule in MODULARIZE+AUDIO_WORKLET builds. + // Instantiate the MODULARIZEd Module function, which is stored for us + // under the special global name AudioWorkletModule in + // MODULARIZE+AUDIO_WORKLET builds. if (globalThis.AudioWorkletModule) { - AudioWorkletModule(Module); // This populates the Module object with all the Wasm properties - delete globalThis.AudioWorkletModule; // We have now instantiated the Module function, can discard it from global scope + // This populates the Module object with all the Wasm properties + AudioWorkletModule(Module); + // We have now instantiated the Module function, can discard it from + // global scope + delete globalThis.AudioWorkletModule; } #endif // Register a real AudioWorkletProcessor that will actually do audio processing. @@ -158,22 +172,29 @@ class BootstrapMessages extends AudioWorkletProcessor { #if WEBAUDIO_DEBUG console.log(`Registered a new WasmAudioWorkletProcessor "${d['_wpn']}" with AudioParams: ${d['audioParams']}`); #endif - // Post a Wasm Call message back telling that we have now registered the AudioWorkletProcessor class, - // and should trigger the user onSuccess callback of the emscripten_create_wasm_audio_worklet_processor_async() call. + // Post a Wasm Call message back telling that we have now registered the + // AudioWorkletProcessor class, and should trigger the user onSuccess + // callback of the + // emscripten_create_wasm_audio_worklet_processor_async() call. p.postMessage({'_wsc': d['callback'], 'x': [d['contextHandle'], 1/*EM_TRUE*/, d['userData']] }); // "WaSm Call" - } else if (d['_wsc']) { // '_wsc' is short for 'wasm call', using an identifier that will never conflict with user messages + } else if (d['_wsc']) { + // '_wsc' is short for 'wasm call', using an identifier that will never + // conflict with user messages Module['wasmTable'].get(d['_wsc'])(...d['x']); }; } } - // No-op, not doing audio processing in this processor. It is just for receiving bootstrap messages. - // However browsers require it to still be present. It should never be called because we never add a - // node to the graph with this processor, although it does look like Chrome does still call this function. + // No-op, not doing audio processing in this processor. It is just for + // receiving bootstrap messages. However browsers require it to still be + // present. It should never be called because we never add a node to the graph + // with this processor, although it does look like Chrome does still call this + // function. process() { - // keep this function a no-op. Chrome redundantly wants to call this even though this processor is never added to the graph. + // keep this function a no-op. Chrome redundantly wants to call this even + // though this processor is never added to the graph. } }; // Register the dummy processor that will just receive messages. -registerProcessor("message", BootstrapMessages); +registerProcessor('message', BootstrapMessages); diff --git a/src/library_webaudio.js b/src/library_webaudio.js index b37231ada4d15..b02d3706bda7a 100644 --- a/src/library_webaudio.js +++ b/src/library_webaudio.js @@ -37,9 +37,11 @@ let LibraryWebAudio = { // Wasm handle ID. $emscriptenGetAudioObject: (objectHandle) => EmAudio[objectHandle], - // emscripten_create_audio_context() does not itself use emscriptenGetAudioObject() function, but mark it as a - // dependency, because the user will not be able to utilize the node unless they call emscriptenGetAudioObject() - // on it on JS side to connect it to the graph, so this avoids the user needing to manually do it on the command line. + // emscripten_create_audio_context() does not itself use + // emscriptenGetAudioObject() function, but mark it as a dependency, because + // the user will not be able to utilize the node unless they call + // emscriptenGetAudioObject() on it on JS side to connect it to the graph, so + // this avoids the user needing to manually do it on the command line. emscripten_create_audio_context__deps: ['$emscriptenRegisterAudioObject', '$emscriptenGetAudioObject'], emscripten_create_audio_context: (options) => { let ctx = window.AudioContext || window.webkitAudioContext; @@ -58,7 +60,7 @@ let LibraryWebAudio = { console.dir(opts); #endif - return ctx && emscriptenRegisterAudioObject(new ctx(opts)); + return ctx && emscriptenRegisterAudioObject(new ctx(opts)); }, emscripten_resume_audio_context_async: (contextHandle, callback, userData) => { @@ -166,14 +168,18 @@ let LibraryWebAudio = { return audioWorkletCreationFailed(); } - // TODO: In MINIMAL_RUNTIME builds, read this file off of a preloaded Blob, and/or embed from a string like with WASM_WORKERS==2 mode. + // TODO: In MINIMAL_RUNTIME builds, read this file off of a preloaded Blob, + // and/or embed from a string like with WASM_WORKERS==2 mode. audioWorklet.addModule('{{{ TARGET_BASENAME }}}.aw.js').then(() => { #if WEBAUDIO_DEBUG console.log(`emscripten_start_wasm_audio_worklet_thread_async() addModule('audioworklet.js') completed`); #endif audioWorklet.bootstrapMessage = new AudioWorkletNode(audioContext, 'message', { processorOptions: { - '$ww': _wasmWorkersID++, // Assign the loaded AudioWorkletGlobalScope a Wasm Worker ID so that it can utilized its own TLS slots, and it is recognized to not be the main browser thread. + // Assign the loaded AudioWorkletGlobalScope a Wasm Worker ID so that + // it can utilized its own TLS slots, and it is recognized to not be + // the main browser thread. + '$ww': _wasmWorkersID++, #if MINIMAL_RUNTIME 'wasm': Module['wasm'], 'mem': wasmMemory, @@ -187,8 +193,11 @@ let LibraryWebAudio = { }); audioWorklet.bootstrapMessage.port.onmessage = _EmAudioDispatchProcessorCallback; - // AudioWorklets do not have a importScripts() function like Web Workers do (and AudioWorkletGlobalScope does not allow dynamic import() either), - // but instead, the main thread must load all JS code into the worklet scope. Send the application main JS script to the audio worklet. + // AudioWorklets do not have a importScripts() function like Web Workers + // do (and AudioWorkletGlobalScope does not allow dynamic import() + // either), but instead, the main thread must load all JS code into the + // worklet scope. Send the application main JS script to the audio + // worklet. return audioWorklet.addModule( #if MINIMAL_RUNTIME Module['js'] @@ -205,7 +214,10 @@ let LibraryWebAudio = { }, $_EmAudioDispatchProcessorCallback: (e) => { - let data = e.data, wasmCall = data['_wsc']; // '_wsc' is short for 'wasm call', trying to use an identifier name that will never conflict with user code + let data = e.data; + // '_wsc' is short for 'wasm call', trying to use an identifier name that + // will never conflict with user code + let wasmCall = data['_wsc']; wasmCall && getWasmTableEntry(wasmCall)(...data['x']); }, @@ -237,7 +249,10 @@ let LibraryWebAudio = { #endif EmAudio[contextHandle].audioWorklet.bootstrapMessage.port.postMessage({ - _wpn: UTF8ToString(HEAPU32[options]), // '_wpn' == 'Worklet Processor Name', use a deliberately mangled name so that this field won't accidentally be mixed with user submitted messages. + // '_wpn' == 'Worklet Processor Name', use a deliberately mangled name so + // that this field won't accidentally be mixed with user submitted + // messages. + _wpn: UTF8ToString(HEAPU32[options]), audioParams, contextHandle, callback, diff --git a/test/test_interactive.py b/test/test_interactive.py index e01df7294238f..c564220fb8ea8 100644 --- a/test/test_interactive.py +++ b/test/test_interactive.py @@ -278,7 +278,7 @@ def test_audio_worklet_emscripten_futex_wake(self): # Tests a second AudioWorklet example: sine wave tone generator. def test_audio_worklet_tone_generator(self): - self.btest('webaudio/tone_generator.c', expected='0', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS']) + self.btest('webaudio/audio_worklet_tone_generator.c', expected='0', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS']) # Tests that AUDIO_WORKLET+MINIMAL_RUNTIME+MODULARIZE combination works together. def test_audio_worklet_modularize(self): diff --git a/test/webaudio/audio_worklet_tone_generator.c b/test/webaudio/audio_worklet_tone_generator.c new file mode 100644 index 0000000000000..ef5885018764e --- /dev/null +++ b/test/webaudio/audio_worklet_tone_generator.c @@ -0,0 +1,157 @@ +#include +#include + +// This program tests that sharing the WebAssembly Memory works between the +// audio generator thread and the main browser UI thread. Two sliders, +// frequency and volume, can be adjusted on the HTML page, and the audio thread +// generates a sine wave tone based on these parameters. + +// Implement smooth transition between the UI values and the values that the +// audio callback are actually processing, to avoid crackling when user adjusts +// the sliders. +float targetToneFrequency = 440.0f; // [shared variable between main thread and audio thread] +float targetVolume = 0.3f; // [shared variable between main thread and audio thread] + +#define SAMPLE_RATE 48000 +#define PI 3.14159265359 + +float phase = 0.f; // [local variable to the audio thread] +float phaseIncrement = 440 * 2.f * PI / SAMPLE_RATE; // [local variable to the audio thread] +float currentVolume = 0.3; // [local variable to the audio thread] + +// REPORT_RESULT is defined when running in Emscripten test harness. You can +// strip these out in your own project. +#ifdef REPORT_RESULT +volatile int audioProcessedCount = 0; +#endif + +// This function will be called for every fixed 128 samples of audio to be processed. +EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { +#ifdef REPORT_RESULT + ++audioProcessedCount; +#endif + + // Interpolate towards the target frequency and volume values. + float targetPhaseIncrement = targetToneFrequency * 2.f * PI / SAMPLE_RATE; + phaseIncrement = phaseIncrement * 0.95f + 0.05f * targetPhaseIncrement; + currentVolume = currentVolume * 0.95f + 0.05f * targetVolume; + + // Produce a sine wave tone of desired frequency to all output channels. + for(int o = 0; o < numOutputs; ++o) + for(int i = 0; i < 128; ++i) + { + float s = emscripten_math_sin(phase); + phase += phaseIncrement; + for(int ch = 0; ch < outputs[o].numberOfChannels; ++ch) + outputs[o].data[ch*128 + i] = s * currentVolume; + } + + // Range reduce to keep precision around zero. + phase = emscripten_math_fmod(phase, 2.f * PI); + + // We generated audio and want to keep this processor going. Return EM_FALSE here to shut down. + return EM_TRUE; +} + +#ifdef REPORT_RESULT +EM_BOOL observe_test_end(double time, void *userData) { + if (audioProcessedCount >= 100) { + REPORT_RESULT(0); + return EM_FALSE; + } + return EM_TRUE; +} +#endif + +// This callback will fire after the Audio Worklet Processor has finished being +// added to the Worklet global scope. +void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { + if (!success) return; + + // 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 noise-generator Audio Worklet Processor. + EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "tone-generator", &options, &ProcessAudio, 0); + + EM_ASM({ + let audioContext = emscriptenGetAudioObject($0); + + // Add a button on the page to toggle playback as a response to user click. + let startButton = document.createElement('button'); + startButton.innerHTML = 'Toggle playback'; + document.body.appendChild(startButton); + + startButton.onclick = () => { + if (audioContext.state != 'running') { + audioContext.resume(); + let audioWorkletNode = emscriptenGetAudioObject($1); + + // Connect the audio worklet node to the graph. + audioWorkletNode.connect(audioContext.destination); + } else { + audioContext.suspend(); + } + }; + }, audioContext, wasmAudioWorklet); + +#ifdef REPORT_RESULT + emscripten_set_timeout_loop(observe_test_end, 10, 0); +#endif +} + +// This callback will fire when the Wasm Module has been shared to the +// AudioWorklet global scope, and is now ready to begin adding Audio Worklet +// Processors. +void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { + if (!success) return; + + WebAudioWorkletProcessorCreateOptions opts = { + .name = "tone-generator", + }; + emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0); +} + +// Define a global stack space for the AudioWorkletGlobalScope. Note that all +// AudioWorkletProcessors and/or AudioWorkletNodes on the given Audio Context +// all share the same AudioWorkerGlobalScope, i.e. they all run on the same one +// audio thread (multiple nodes/processors do not each get their own thread). +// Hence one stack is enough. +uint8_t wasmAudioWorkletStack[4096]; + +int main() { + // Add a UI slider to the page to adjust the pitch of the tone. + EM_ASM({ + let div = document.createElement('div'); + div.innerHTML = 'Choose frequency: 440
' + + 'Choose volume: 30%
'; + document.body.appendChild(div); + document.querySelector('#pitch').oninput = (e) => { + document.querySelector('#pitchValue').innerHTML = HEAPF32[$0>>2] = parseInt(e.target.value); + }; + document.querySelector('#volume').oninput = (e) => { + HEAPF32[$1>>2] = parseInt(e.target.value) / 100; + document.querySelector('#volumeValue').innerHTML = parseInt(e.target.value) + '%'; + }; + }, &targetToneFrequency, &targetVolume); + + // Create an audio context + EmscriptenWebAudioCreateAttributes attrs = { + .latencyHint = "interactive", + .sampleRate = SAMPLE_RATE + }; + + EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(&attrs); + + // and kick off Audio Worklet scope initialization, which shares the Wasm + // Module and Memory to the AudioWorklet scope and initializes its stack. + emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); +} diff --git a/test/webaudio/audioworklet.c b/test/webaudio/audioworklet.c index 712dd64969c22..1d8d73793acf4 100644 --- a/test/webaudio/audioworklet.c +++ b/test/webaudio/audioworklet.c @@ -4,23 +4,33 @@ #include /* Steps to use Wasm-based AudioWorklets: - 1. Create a Web Audio AudioContext either via manual JS code and calling emscriptenRegisterAudioObject() from JS, or by calling emscripten_create_audio_context() (shown in this sample) - 2. Initialize a Wasm AudioWorklet scope on the audio context by calling emscripten_start_wasm_audio_worklet_thread_async(). This shares the Wasm Module, Memory, etc. to the AudioWorklet scope, - and establishes the stack space for the Audio Worklet. - This needs to be called exactly once during page's lifetime. There is no mechanism in Web Audio to shut down/uninitialize the scope. - 3. Create one or more of Audio Worklet Processors with the desired name and AudioParam configuration. - 4. Instantiate Web Audio audio graph nodes from the above created worklet processors, specifying the desired input-output configurations and Wasm-side function callbacks to call for each node. - 5. Add the graph nodes to the Web Audio graph, and the audio callbacks should begin to fire. + 1. Create a Web Audio AudioContext either via manual JS code and calling + emscriptenRegisterAudioObject() from JS, or by calling + emscripten_create_audio_context() (shown in this sample) + 2. Initialize a Wasm AudioWorklet scope on the audio context by calling + emscripten_start_wasm_audio_worklet_thread_async(). This shares the Wasm + Module, Memory, etc. to the AudioWorklet scope, and establishes the stack + space for the Audio Worklet. + This needs to be called exactly once during page's lifetime. There is no + mechanism in Web Audio to shut down/uninitialize the scope. + 3. Create one or more of Audio Worklet Processors with the desired name and + AudioParam configuration. + 4. Instantiate Web Audio audio graph nodes from the above created worklet + processors, specifying the desired input-output configurations and Wasm-side + function callbacks to call for each node. + 5. Add the graph nodes to the Web Audio graph, and the audio callbacks should + begin to fire. */ -#ifdef REPORT_RESULT // This is defined when running in Emscripten test harness. You can strip these out in your own project. +// REPORT_RESULT is defined when running in Emscripten test harness. You can +// strip these out in your own project. +#ifdef REPORT_RESULT _Thread_local int testTlsVariable = 1; int lastTlsVariableValueInAudioThread = 1; #endif // This function will be called for every fixed 128 samples of audio to be processed. -EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) -{ +EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { #ifdef REPORT_RESULT assert(testTlsVariable == lastTlsVariableValueInAudioThread); ++testTlsVariable; @@ -58,13 +68,11 @@ EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext, EMSCRIPTEN_AUDIO_WO }); #ifdef REPORT_RESULT -EM_BOOL main_thread_tls_access(double time, void *userData) -{ - // Try to mess the TLS variable on the main thread, with the expectation that it should not change - // the TLS value on the AudioWorklet thread. +EM_BOOL main_thread_tls_access(double time, void *userData) { + // Try to mess the TLS variable on the main thread, with the expectation that + // it should not change the TLS value on the AudioWorklet thread. testTlsVariable = (int)time; - if (lastTlsVariableValueInAudioThread >= 100) - { + if (lastTlsVariableValueInAudioThread >= 100) { REPORT_RESULT(0); return EM_FALSE; } @@ -72,12 +80,14 @@ EM_BOOL main_thread_tls_access(double time, void *userData) } #endif -// This callback will fire after the Audio Worklet Processor has finished being added to the Worklet global scope. -void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ +// This callback will fire after the Audio Worklet Processor has finished being +// added to the Worklet global scope. +void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { if (!success) return; - // Specify the input and output node configurations for the Wasm Audio Worklet. A simple setup with single mono output channel here, and no inputs. + // 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 = { @@ -96,9 +106,10 @@ void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL su InitHtmlUi(audioContext, wasmAudioWorklet); } -// This callback will fire when the Wasm Module has been shared to the AudioWorklet global scope, and is now ready to begin adding Audio Worklet Processors. -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ +// This callback will fire when the Wasm Module has been shared to the +// AudioWorklet global scope, and is now ready to begin adding Audio Worklet +// Processors. +void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { if (!success) return; WebAudioWorkletProcessorCreateOptions opts = { @@ -107,12 +118,14 @@ void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOO emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0); } -// Define a global stack space for the AudioWorkletGlobalScope. Note that all AudioWorkletProcessors and/or AudioWorkletNodes on the given Audio Context all share the same AudioWorkerGlobalScope, -// i.e. they all run on the same one audio thread (multiple nodes/processors do not each get their own thread). Hence one stack is enough. +// Define a global stack space for the AudioWorkletGlobalScope. Note that all +// AudioWorkletProcessors and/or AudioWorkletNodes on the given Audio Context +// all share the same AudioWorkerGlobalScope, i.e. they all run on the same one +// audio thread (multiple nodes/processors do not each get their own thread). +// Hence one stack is enough. uint8_t wasmAudioWorkletStack[4096]; -int main() -{ +int main() { srand(time(NULL)); assert(!emscripten_current_thread_is_audio_worklet()); @@ -120,6 +133,7 @@ int main() // Create an audio context EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0 /* use default constructor options */); - // and kick off Audio Worklet scope initialization, which shares the Wasm Module and Memory to the AudioWorklet scope and initializes its stack. + // and kick off Audio Worklet scope initialization, which shares the Wasm + // Module and Memory to the AudioWorklet scope and initializes its stack. emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); } diff --git a/test/webaudio/audioworklet_emscripten_futex_wake.cpp b/test/webaudio/audioworklet_emscripten_futex_wake.cpp index 132d03bf4bb3f..d70af1d672e54 100644 --- a/test/webaudio/audioworklet_emscripten_futex_wake.cpp +++ b/test/webaudio/audioworklet_emscripten_futex_wake.cpp @@ -13,8 +13,7 @@ int futexLocation = 0; int testSuccess = 0; -EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) -{ +EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { int supportsAtomicWait = _emscripten_thread_supports_atomics_wait(); printf("supportsAtomicWait: %d\n", supportsAtomicWait); assert(!supportsAtomicWait); @@ -40,10 +39,8 @@ EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext, EMSCRIPTEN_AUDIO_WO }; }); -EM_BOOL PollTestSuccess(double, void *) -{ - if (testSuccess) - { +EM_BOOL PollTestSuccess(double, void *) { + if (testSuccess) { printf("Test success!\n"); #ifdef REPORT_RESULT REPORT_RESULT(0); @@ -53,24 +50,21 @@ EM_BOOL PollTestSuccess(double, void *) return EM_TRUE; } -void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ +void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { int outputChannelCounts[1] = { 1 }; EmscriptenAudioWorkletNodeCreateOptions options = { .numberOfInputs = 0, .numberOfOutputs = 1, .outputChannelCounts = outputChannelCounts }; EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "noise-generator", &options, &ProcessAudio, 0); InitHtmlUi(audioContext, wasmAudioWorklet); } -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ +void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { WebAudioWorkletProcessorCreateOptions opts = { .name = "noise-generator" }; emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0); } uint8_t wasmAudioWorkletStack[4096]; -int main() -{ +int main() { emscripten_set_timeout_loop(PollTestSuccess, 10, 0); EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0); emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); diff --git a/test/webaudio/audioworklet_post_function.c b/test/webaudio/audioworklet_post_function.c index 403ddbb1bbf87..1619fe267364d 100644 --- a/test/webaudio/audioworklet_post_function.c +++ b/test/webaudio/audioworklet_post_function.c @@ -2,12 +2,12 @@ #include #include -// This test showcases posting messages (function calls) between the main thread and the Audio Worklet thread -// using the emscripten_audio_worklet_post_function_*() API. +// This test showcases posting messages (function calls) between the main thread +// and the Audio Worklet thread using the +// emscripten_audio_worklet_post_function_*() API. // This event will fire on the main thread. -void MessageReceivedOnMainThread(int d, int e, int f) -{ +void MessageReceivedOnMainThread(int d, int e, int f) { printf("MessageReceivedOnMainThread: d=%d, e=%d, f=%d\n", d, e, f); assert(!emscripten_current_thread_is_audio_worklet()); assert(d == 1 && e == 2 && f == 3); @@ -17,8 +17,7 @@ void MessageReceivedOnMainThread(int d, int e, int f) } // This event will fire on the audio worklet thread. -void MessageReceivedInAudioWorkletThread(int a, int b) -{ +void MessageReceivedInAudioWorkletThread(int a, int b) { printf("MessageReceivedInAudioWorkletThread: a=%d, b=%d\n", a, b); assert(emscripten_current_thread_is_audio_worklet()); assert(a == 42 && b == 9000); @@ -26,19 +25,18 @@ void MessageReceivedInAudioWorkletThread(int a, int b) } // This callback will fire when the audio worklet thread has been initialized. -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ +void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) { printf("WebAudioWorkletThreadInitialized\n"); emscripten_audio_worklet_post_function_vii(audioContext, MessageReceivedInAudioWorkletThread, /*a=*/42, /*b=*/9000); } uint8_t wasmAudioWorkletStack[4096]; -int main() -{ +int main() { // Create an audio context EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0 /* use default constructor options */); - // and kick off Audio Worklet scope initialization, which shares the Wasm Module and Memory to the AudioWorklet scope and initializes its stack. + // and kick off Audio Worklet scope initialization, which shares the Wasm + // Module and Memory to the AudioWorklet scope and initializes its stack. emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); } diff --git a/test/webaudio/tone_generator.c b/test/webaudio/tone_generator.c deleted file mode 100644 index e19915658987e..0000000000000 --- a/test/webaudio/tone_generator.c +++ /dev/null @@ -1,149 +0,0 @@ -#include -#include - -// This program tests that sharing the WebAssembly Memory works between the audio generator thread and the main browser UI thread. -// Two sliders, frequency and volume, can be adjusted on the HTML page, and the audio thread generates a sine wave tone based on -// these parameters. - -// Implement smooth transition between the UI values and the values that the audio callback are actually processing, to avoid crackling when user adjusts the sliders. -float targetToneFrequency = 440.0f; // [shared variable between main thread and audio thread] -float targetVolume = 0.3f; // [shared variable between main thread and audio thread] - -#define SAMPLE_RATE 48000 -#define PI 3.14159265359 - -float phase = 0.f; // [local variable to the audio thread] -float phaseIncrement = 440 * 2.f * PI / SAMPLE_RATE; // [local variable to the audio thread] -float currentVolume = 0.3; // [local variable to the audio thread] - -#ifdef REPORT_RESULT // This is defined when running in Emscripten test harness. You can strip these out in your own project. -volatile int audioProcessedCount = 0; -#endif - -// This function will be called for every fixed 128 samples of audio to be processed. -EM_BOOL ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) -{ -#ifdef REPORT_RESULT - ++audioProcessedCount; -#endif - - // Interpolate towards the target frequency and volume values. - float targetPhaseIncrement = targetToneFrequency * 2.f * PI / SAMPLE_RATE; - phaseIncrement = phaseIncrement * 0.95f + 0.05f * targetPhaseIncrement; - currentVolume = currentVolume * 0.95f + 0.05f * targetVolume; - - // Produce a sine wave tone of desired frequency to all output channels. - for(int o = 0; o < numOutputs; ++o) - for(int i = 0; i < 128; ++i) - { - float s = emscripten_math_sin(phase); - phase += phaseIncrement; - for(int ch = 0; ch < outputs[o].numberOfChannels; ++ch) - outputs[o].data[ch*128 + i] = s * currentVolume; - } - - // Range reduce to keep precision around zero. - phase = emscripten_math_fmod(phase, 2.f * PI); - - // We generated audio and want to keep this processor going. Return EM_FALSE here to shut down. - return EM_TRUE; -} - -#ifdef REPORT_RESULT -EM_BOOL observe_test_end(double time, void *userData) -{ - if (audioProcessedCount >= 100) - { - REPORT_RESULT(0); - return EM_FALSE; - } - return EM_TRUE; -} -#endif - -// This callback will fire after the Audio Worklet Processor has finished being added to the Worklet global scope. -void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ - if (!success) return; - - // 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 noise-generator Audio Worklet Processor. - EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "tone-generator", &options, &ProcessAudio, 0); - - EM_ASM({ - let audioContext = emscriptenGetAudioObject($0); - - // Add a button on the page to toggle playback as a response to user click. - let startButton = document.createElement('button'); - startButton.innerHTML = 'Toggle playback'; - document.body.appendChild(startButton); - - startButton.onclick = () => { - if (audioContext.state != 'running') { - audioContext.resume(); - let audioWorkletNode = emscriptenGetAudioObject($1); - - // Connect the audio worklet node to the graph. - audioWorkletNode.connect(audioContext.destination); - } else { - audioContext.suspend(); - } - }; - }, audioContext, wasmAudioWorklet); - -#ifdef REPORT_RESULT - emscripten_set_timeout_loop(observe_test_end, 10, 0); -#endif -} - -// This callback will fire when the Wasm Module has been shared to the AudioWorklet global scope, and is now ready to begin adding Audio Worklet Processors. -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void *userData) -{ - if (!success) return; - - WebAudioWorkletProcessorCreateOptions opts = { - .name = "tone-generator", - }; - emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0); -} - -// Define a global stack space for the AudioWorkletGlobalScope. Note that all AudioWorkletProcessors and/or AudioWorkletNodes on the given Audio Context all share the same AudioWorkerGlobalScope, -// i.e. they all run on the same one audio thread (multiple nodes/processors do not each get their own thread). Hence one stack is enough. -uint8_t wasmAudioWorkletStack[4096]; - -int main() -{ - // Add a UI slider to the page to adjust the pitch of the tone. - EM_ASM({ - let div = document.createElement('div'); - div.innerHTML = 'Choose frequency: 440
' + - 'Choose volume: 30%
'; - document.body.appendChild(div); - document.querySelector('#pitch').oninput = (e) => { - document.querySelector('#pitchValue').innerHTML = HEAPF32[$0>>2] = parseInt(e.target.value); - }; - document.querySelector('#volume').oninput = (e) => { - HEAPF32[$1>>2] = parseInt(e.target.value) / 100; - document.querySelector('#volumeValue').innerHTML = parseInt(e.target.value) + '%'; - }; - }, &targetToneFrequency, &targetVolume); - - // Create an audio context - EmscriptenWebAudioCreateAttributes attrs = { - .latencyHint = "interactive", - .sampleRate = SAMPLE_RATE - }; - - EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(&attrs); - - // and kick off Audio Worklet scope initialization, which shares the Wasm Module and Memory to the AudioWorklet scope and initializes its stack. - emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0); -}