// This file is the main bootstrap script for Wasm Audio Worklets loaded in an1// Emscripten application. Build with -sAUDIO_WORKLET linker flag to enable2// targeting Audio Worklets.34// AudioWorkletGlobalScope does not have a onmessage/postMessage() functionality5// at the global scope, which means that after creating an6// AudioWorkletGlobalScope and loading this script into it, we cannot7// postMessage() information into it like one would do with Web Workers.89// Instead, we must create an AudioWorkletProcessor class, then instantiate a10// Web Audio graph node from it on the main thread. Using its message port and11// the node constructor's "processorOptions" field, we can share the necessary12// bootstrap information from the main thread to the AudioWorkletGlobalScope.1314#if MINIMAL_RUNTIME15var instantiatePromise;16#endif1718if (ENVIRONMENT_IS_AUDIO_WORKLET) {1920#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS21function createWasmAudioWorkletProcessor(audioParams) {22#else23function createWasmAudioWorkletProcessor() {24#endif25class WasmAudioWorkletProcessor extends AudioWorkletProcessor {26constructor(args) {27super();2829// Capture the Wasm function callback to invoke.30let opts = args.processorOptions;31#if ASSERTIONS32assert(opts.callback)33assert(opts.samplesPerChannel)34#endif35this.callback = {{{ makeDynCall('iipipipp', 'opts.callback') }}};36this.userData = opts.userData;37// Then the samples per channel to process, fixed for the lifetime of the38// context that created this processor. Even though this 'render quantum39// size' is fixed at 128 samples in the 1.0 spec, it will be variable in40// the 1.1 spec. It's passed in now, just to prove it's settable, but will41// eventually be a property of the AudioWorkletGlobalScope (globalThis).42this.samplesPerChannel = opts.samplesPerChannel;43this.bytesPerChannel = this.samplesPerChannel * {{{ getNativeTypeSize('float') }}};4445// Prepare the output views; see createOutputViews(). The 'STACK_ALIGN'46// deduction stops the STACK_OVERFLOW_CHECK failing (since the stack will47// be full if we allocate all the available space) leaving room for a48// single AudioSampleFrame as a minimum. There's an arbitrary maximum of49// 64 frames, for the case where a multi-MB stack is passed.50this.outputViews = new Array(Math.min(((wwParams.stackSize - {{{ STACK_ALIGN }}}) / this.bytesPerChannel) | 0, /*sensible limit*/ 64));51#if ASSERTIONS52assert(this.outputViews.length > 0, `AudioWorklet needs more stack allocating (at least ${this.bytesPerChannel})`);53#endif54this.createOutputViews();5556#if ASSERTIONS57// Explicitly verify this later in process(). Note to self, stackSave is a58// bit of a misnomer as it simply gets the stack address.59this.ctorOldStackPtr = stackSave();60#endif61}6263/**64* Create up-front as many typed views for marshalling the output data as65* may be required, allocated at the *top* of the worklet's stack (and whose66* addresses are fixed).67*/68createOutputViews() {69// These are still alloc'd to take advantage of the overflow checks, etc.70var oldStackPtr = stackSave();71var viewDataIdx = {{{ getHeapOffset('stackAlloc(this.outputViews.length * this.bytesPerChannel)', 'float') }}};72#if WEBAUDIO_DEBUG73console.log(`AudioWorklet creating ${this.outputViews.length} buffer one-time views (for a stack size of ${wwParams.stackSize} at address ${ptrToString(viewDataIdx * 4)})`);74#endif75// Inserted in reverse so the lowest indices are closest to the stack top76for (var n = this.outputViews.length - 1; n >= 0; n--) {77this.outputViews[n] = HEAPF32.subarray(viewDataIdx, viewDataIdx += this.samplesPerChannel);78}79stackRestore(oldStackPtr);80}8182#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS83static get parameterDescriptors() {84return audioParams;85}86#endif8788/**89* Marshals all inputs and parameters to the Wasm memory on the thread's90* stack, then performs the wasm audio worklet call, and finally marshals91* audio output data back.92*93* @param {Object} parameters94*/95#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS96process(inputList, outputList, parameters) {97#else98/** @suppress {checkTypes} */99process(inputList, outputList) {100#endif101102#if ALLOW_MEMORY_GROWTH103// Recreate the output views if the heap has changed104// TODO: add support for GROWABLE_ARRAYBUFFERS105if (HEAPF32.buffer != this.outputViews[0].buffer) {106this.createOutputViews();107}108#endif109110var numInputs = inputList.length;111var numOutputs = outputList.length;112113var entry; // reused list entry or index114var subentry; // reused channel or other array in each list entry or index115116// Calculate the required stack and output buffer views (stack is further117// split into aligned structs and the raw float data).118var stackMemoryStruct = (numInputs + numOutputs) * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};119var stackMemoryData = 0;120for (entry of inputList) {121stackMemoryData += entry.length;122}123stackMemoryData *= this.bytesPerChannel;124// Collect the total number of output channels (mapped to array views)125var outputViewsNeeded = 0;126for (entry of outputList) {127outputViewsNeeded += entry.length;128}129stackMemoryData += outputViewsNeeded * this.bytesPerChannel;130var numParams = 0;131#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS132for (entry in parameters) {133++numParams;134stackMemoryStruct += {{{ C_STRUCTS.AudioParamFrame.__size__ }}};135stackMemoryData += parameters[entry].byteLength;136}137#endif138var oldStackPtr = stackSave();139#if ASSERTIONS140assert(oldStackPtr == this.ctorOldStackPtr, 'AudioWorklet stack address has unexpectedly moved');141assert(outputViewsNeeded <= this.outputViews.length, `Too many AudioWorklet outputs (need ${outputViewsNeeded} but have stack space for ${this.outputViews.length})`);142#endif143144// Allocate the necessary stack space. All pointer variables are in bytes;145// 'structPtr' starts at the first struct entry (all run sequentially)146// and is the working start to each record; 'dataPtr' is the same for the147// audio/params data, starting after *all* the structs.148// 'structPtr' begins 16-byte aligned, allocated from the internal149// _emscripten_stack_alloc(), as are the output views, and so to ensure150// the views fall on the correct addresses (and we finish at stacktop) we151// request additional bytes, taking this alignment into account, then152// offset `dataPtr` by the difference.153var stackMemoryAligned = (stackMemoryStruct + stackMemoryData + 15) & ~15;154var structPtr = stackAlloc(stackMemoryAligned);155var dataPtr = structPtr + (stackMemoryAligned - stackMemoryData);156#if ASSERTIONS157// TODO: look at why stackAlloc isn't tripping the assertions158assert(stackMemoryAligned <= wwParams.stackSize, `Not enough stack allocated to the AudioWorklet (need ${stackMemoryAligned}, got ${wwParams.stackSize})`);159#endif160161// Copy input audio descriptor structs and data to Wasm (recall, structs162// first, audio data after). 'inputsPtr' is the start of the C callback's163// input AudioSampleFrame.164var /*const*/ inputsPtr = structPtr;165for (entry of inputList) {166// Write the AudioSampleFrame struct instance167{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'entry.length', 'u32') }}};168{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}};169{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}};170structPtr += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};171// Marshal the input audio sample data for each audio channel of this input172for (subentry of entry) {173HEAPF32.set(subentry, {{{ getHeapOffset('dataPtr', 'float') }}});174dataPtr += this.bytesPerChannel;175}176}177178#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS179// Copy parameters descriptor structs and data to Wasm. 'paramsPtr' is the180// start of the C callback's input AudioParamFrame.181var /*const*/ paramsPtr = structPtr;182for (entry = 0; subentry = parameters[entry++];) {183// Write the AudioParamFrame struct instance184{{{ makeSetValue('structPtr', C_STRUCTS.AudioParamFrame.length, 'subentry.length', 'u32') }}};185{{{ makeSetValue('structPtr', C_STRUCTS.AudioParamFrame.data, 'dataPtr', '*') }}};186structPtr += {{{ C_STRUCTS.AudioParamFrame.__size__ }}};187// Marshal the audio parameters array188HEAPF32.set(subentry, {{{ getHeapOffset('dataPtr', 'float') }}});189dataPtr += subentry.length * {{{ getNativeTypeSize('float') }}};190}191#else192var paramsPtr = 0;193#endif194195// Copy output audio descriptor structs to Wasm. 'outputsPtr' is the start196// of the C callback's output AudioSampleFrame. 'dataPtr' will now be197// aligned with the output views, ending at stacktop (which is why this198// needs to be last).199var /*const*/ outputsPtr = structPtr;200for (entry of outputList) {201// Write the AudioSampleFrame struct instance202{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'entry.length', 'u32') }}};203{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}};204{{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}};205structPtr += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};206// Advance the output pointer to the next output (matching the pre-allocated views)207dataPtr += this.bytesPerChannel * entry.length;208}209210#if ASSERTIONS211// If all the maths worked out, we arrived at the original stack address212console.assert(dataPtr == oldStackPtr, `AudioWorklet stack mismatch (audio data finishes at ${dataPtr} instead of ${oldStackPtr})`);213214// Sanity checks. If these trip the most likely cause, beyond unforeseen215// stack shenanigans, is that the 'render quantum size' changed after216// construction (which shouldn't be possible).217if (numOutputs) {218// First that the output view addresses match the stack positions219dataPtr -= this.bytesPerChannel;220for (entry = 0; entry < outputViewsNeeded; entry++) {221console.assert(dataPtr == this.outputViews[entry].byteOffset, 'AudioWorklet internal error in addresses of the output array views');222dataPtr -= this.bytesPerChannel;223}224// And that the views' size match the passed in output buffers225for (entry of outputList) {226for (subentry of entry) {227assert(subentry.byteLength == this.bytesPerChannel, `AudioWorklet unexpected output buffer size (expected ${this.bytesPerChannel} got ${subentry.byteLength})`);228}229}230}231#endif232233// Call out to Wasm callback to perform audio processing234var didProduceAudio = this.callback(numInputs, inputsPtr, numOutputs, outputsPtr, numParams, paramsPtr, this.userData);235if (didProduceAudio) {236// Read back the produced audio data to all outputs and their channels.237// The preallocated 'outputViews' already have the correct offsets and238// sizes into the stack (recall from createOutputViews() that they run239// backwards).240for (entry of outputList) {241for (subentry of entry) {242subentry.set(this.outputViews[--outputViewsNeeded]);243}244}245}246247stackRestore(oldStackPtr);248249// Return 'true' to tell the browser to continue running this processor.250// (Returning 1 or any other truthy value won't work in Chrome)251return !!didProduceAudio;252}253}254return WasmAudioWorkletProcessor;255}256257#if MIN_FIREFOX_VERSION < 138 || MIN_CHROME_VERSION != TARGET_NOT_SUPPORTED || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED258// If this browser does not support the up-to-date AudioWorklet standard259// that has a MessagePort over to the AudioWorklet, then polyfill that by260// a hacky AudioWorkletProcessor that provides the MessagePort.261// Firefox added support in https://hg-edge.mozilla.org/integration/autoland/rev/ab38a1796126f2b3fc06475ffc5a625059af59c1262// Chrome ticket: https://crbug.com/446920095263// Safari ticket: https://webkit.org/b/299386264/**265* @suppress {duplicate, checkTypes}266*/267var port = globalThis.port || {};268269// Specify a worklet processor that will be used to receive messages to this270// AudioWorkletGlobalScope. We never connect this initial AudioWorkletProcessor271// to the audio graph to do any audio processing.272class BootstrapMessages extends AudioWorkletProcessor {273constructor(arg) {274super();275startWasmWorker(arg.processorOptions)276// Listen to messages from the main thread. These messages will ask this277// scope to create the real AudioWorkletProcessors that call out to Wasm to278// do audio processing.279if (!(port instanceof MessagePort)) {280this.port.onmessage = port.onmessage;281/** @suppress {checkTypes} */282port = this.port;283}284}285286// No-op, not doing audio processing in this processor. It is just for287// receiving bootstrap messages. However browsers require it to still be288// present. It should never be called because we never add a node to the graph289// with this processor, although it does look like Chrome does still call this290// function.291process() {292// keep this function a no-op. Chrome redundantly wants to call this even293// though this processor is never added to the graph.294}295};296297// Register the dummy processor that will just receive messages.298registerProcessor('em-bootstrap', BootstrapMessages);299#endif300301port.onmessage = async (msg) => {302#if MINIMAL_RUNTIME303// Wait for the module instantiation before processing messages.304await instantiatePromise;305#endif306let d = msg.data;307if (d['_boot']) {308startWasmWorker(d);309#if WEBAUDIO_DEBUG310console.log('AudioWorklet global scope looks like this:');311console.dir(globalThis);312#endif313} else if (d['_wpn']) {314// '_wpn' is short for 'Worklet Processor Node', using an identifier315// that will never conflict with user messages316// Register a real AudioWorkletProcessor that will actually do audio processing.317#if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS318registerProcessor(d['_wpn'], createWasmAudioWorkletProcessor(d.audioParams));319#else320registerProcessor(d['_wpn'], createWasmAudioWorkletProcessor());321#endif322#if WEBAUDIO_DEBUG323console.log(`Registered a new WasmAudioWorkletProcessor "${d['_wpn']}" with AudioParams: ${d.audioParams}`);324#endif325// Post a Wasm Call message back telling that we have now registered the326// AudioWorkletProcessor, and should trigger the user onSuccess callback327// of the emscripten_create_wasm_audio_worklet_processor_async() call.328//329// '_wsc' is short for 'wasm call', using an identifier that will never330// conflict with user messages.331//332// Note: we convert the pointer arg manually here since the call site333// ($_EmAudioDispatchProcessorCallback) is used with various signatures334// and we do not know the types in advance.335port.postMessage({'_wsc': d.callback, args: [d.contextHandle, 1/*EM_TRUE*/, {{{ to64('d.userData') }}}] });336} else if (d['_wsc']) {337getWasmTableEntry(d['_wsc'])(...d.args);338};339}340341} // ENVIRONMENT_IS_AUDIO_WORKLET342343344