Skip to content

[AUDIO_WORKLET] Add support for MEMORY64 with 2GB and 4GB heap #24732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions src/audio_worklet.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function createWasmAudioWorkletProcessor(audioParams) {
assert(opts.callback)
assert(opts.samplesPerChannel)
#endif
this.callback = getWasmTableEntry(opts.callback);
this.callback = {{{ makeDynCall('iipipipp', 'opts.callback') }}};
this.userData = opts.userData;
// Then the samples per channel to process, fixed for the lifetime of the
// context that created this processor. Note for when moving to Web Audio
Expand Down Expand Up @@ -65,48 +65,48 @@ function createWasmAudioWorkletProcessor(audioParams) {
inputsPtr = stackAlloc(stackMemoryNeeded);

// Copy input audio descriptor structs and data to Wasm
k = inputsPtr >> 2;
k = inputsPtr;
dataPtr = inputsPtr + numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
for (i of inputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.samplesPerChannel / 4 }}}] = this.samplesPerChannel;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'i.length', 'u32') }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}};
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
// Marshal the input audio sample data for each audio channel of this input
for (j of i) {
HEAPF32.set(j, dataPtr>>2);
HEAPF32.set(j, {{{ getHeapOffset('dataPtr', 'float') }}});
dataPtr += bytesPerChannel;
}
}

// Copy output audio descriptor structs to Wasm
outputsPtr = dataPtr;
k = outputsPtr >> 2;
outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}) >> 2;
k = outputsPtr;
outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}});
for (i of outputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.samplesPerChannel / 4 }}}] = this.samplesPerChannel;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'i.length', 'u32') }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}};
{{{ makeSetValue('k', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}};
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
// Reserve space for the output data
dataPtr += bytesPerChannel * i.length;
}

// Copy parameters descriptor structs and data to Wasm
paramsPtr = dataPtr;
k = paramsPtr >> 2;
k = paramsPtr;
dataPtr += numParams * {{{ C_STRUCTS.AudioParamFrame.__size__ }}};

for (i = 0; paramArray = parameters[i++];) {
// Write the AudioParamFrame struct instance
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.length / 4 }}}] = paramArray.length;
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioParamFrame.__size__ / 4 }}};
{{{ makeSetValue('k', C_STRUCTS.AudioParamFrame.length, 'paramArray.length', 'u32') }}};
{{{ makeSetValue('k', C_STRUCTS.AudioParamFrame.data, 'dataPtr', '*') }}};
k += {{{ C_STRUCTS.AudioParamFrame.__size__ }}};
// Marshal the audio parameters array
HEAPF32.set(paramArray, dataPtr>>2);
dataPtr += paramArray.length*4;
HEAPF32.set(paramArray, {{{ getHeapOffset('dataPtr', 'float') }}});
dataPtr += paramArray.length * {{{ getNativeTypeSize('float') }}};
}

// Call out to Wasm callback to perform audio processing
Expand All @@ -115,6 +115,7 @@ function createWasmAudioWorkletProcessor(audioParams) {
// (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)
outputDataPtr = {{{ getHeapOffset('outputDataPtr', 'float') }}};
for (i of outputList) {
for (j of i) {
for (k = 0; k < this.samplesPerChannel; ++k) {
Expand Down Expand Up @@ -167,8 +168,12 @@ class BootstrapMessages extends AudioWorkletProcessor {
// of the emscripten_create_wasm_audio_worklet_processor_async() call.
//
// '_wsc' is short for 'wasm call', using an identifier that will never
// conflict with user messages
messagePort.postMessage({'_wsc': d.callback, args: [d.contextHandle, 1/*EM_TRUE*/, d.userData] });
// conflict with user messages.
//
// Note: we convert the pointer arg manually here since the call site
// ($_EmAudioDispatchProcessorCallback) is used with various signatures
// and we do not know the types in advance.
messagePort.postMessage({'_wsc': d.callback, args: [d.contextHandle, 1/*EM_TRUE*/, {{{ to64('d.userData') }}}] });
} else if (d['_wsc']) {
getWasmTableEntry(d['_wsc'])(...d.args);
};
Expand Down
42 changes: 23 additions & 19 deletions src/lib/libwebaudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ var LibraryWebAudio = {
#if WEBAUDIO_DEBUG
console.log(`emscripten_resume_audio_context_async() callback: New audio state="${EmAudio[contextHandle].state}", ID=${state}`);
#endif
{{{ makeDynCall('viii', 'callback') }}}(contextHandle, state, userData);
{{{ makeDynCall('viip', 'callback') }}}(contextHandle, state, userData);
}
#if WEBAUDIO_DEBUG
console.log(`emscripten_resume_audio_context_async() resuming...`);
Expand Down Expand Up @@ -202,15 +202,16 @@ var LibraryWebAudio = {
}
});
audioWorklet.bootstrapMessage.port.onmessage = _EmAudioDispatchProcessorCallback;
{{{ makeDynCall('viii', 'callback') }}}(contextHandle, 1/*EM_TRUE*/, userData);
{{{ makeDynCall('viip', 'callback') }}}(contextHandle, 1/*EM_TRUE*/, userData);
}).catch(audioWorkletCreationFailed);
},

$_EmAudioDispatchProcessorCallback__deps: ['$getWasmTableEntry'],
$_EmAudioDispatchProcessorCallback: (e) => {
var data = e.data;
// '_wsc' is short for 'wasm call', trying to use an identifier name that
// will never conflict with user code
// will never conflict with user code. This is used to call both the 3-param
// call (handle, true, userData) and the variable argument post functions.
var wasmCall = data['_wsc'];
wasmCall && getWasmTableEntry(wasmCall)(...data.args);
},
Expand All @@ -222,32 +223,33 @@ var LibraryWebAudio = {
assert(EmAudio[contextHandle] instanceof (window.AudioContext || window.webkitAudioContext), `Called emscripten_create_wasm_audio_worklet_processor_async() on a context handle ${contextHandle} that is not an AudioContext, but of type ${typeof EmAudio[contextHandle]}`);
#endif

options >>= 2;
var audioParams = [],
numAudioParams = HEAPU32[options+1],
audioParamDescriptors = HEAPU32[options+2] >> 2,
i = 0;
var audioParams = [];
var processorName = UTF8ToString({{{ makeGetValue('options', C_STRUCTS.WebAudioWorkletProcessorCreateOptions.name, '*') }}});
var numAudioParams = {{{ makeGetValue('options', C_STRUCTS.WebAudioWorkletProcessorCreateOptions.numAudioParams, 'i32') }}};
var audioParamDescriptors = {{{ makeGetValue('options', C_STRUCTS.WebAudioWorkletProcessorCreateOptions.audioParamDescriptors, '*') }}};
var paramIndex = 0;

while (numAudioParams--) {
audioParams.push({
name: i++,
defaultValue: HEAPF32[audioParamDescriptors++],
minValue: HEAPF32[audioParamDescriptors++],
maxValue: HEAPF32[audioParamDescriptors++],
automationRate: ['a','k'][HEAPU32[audioParamDescriptors++]] + '-rate',
name: paramIndex++,
defaultValue: {{{ makeGetValue('audioParamDescriptors', C_STRUCTS.WebAudioParamDescriptor.defaultValue, 'float') }}},
minValue: {{{ makeGetValue('audioParamDescriptors', C_STRUCTS.WebAudioParamDescriptor.minValue, 'float') }}},
maxValue: {{{ makeGetValue('audioParamDescriptors', C_STRUCTS.WebAudioParamDescriptor.maxValue, 'float') }}},
automationRate: ({{{ makeGetValue('audioParamDescriptors', C_STRUCTS.WebAudioParamDescriptor.automationRate, 'i32') }}} ? 'k' : 'a') + '-rate',
});
audioParamDescriptors += {{{ C_STRUCTS.WebAudioParamDescriptor.__size__ }}};
}

#if WEBAUDIO_DEBUG
console.log(`emscripten_create_wasm_audio_worklet_processor_async() creating a new AudioWorklet processor with name ${UTF8ToString(HEAPU32[options])}`);
console.log(`emscripten_create_wasm_audio_worklet_processor_async() creating a new AudioWorklet processor with name ${processorName}`);
#endif

EmAudio[contextHandle].audioWorklet.bootstrapMessage.port.postMessage({
// Deliberately mangled and short names used here ('_wpn', the 'Worklet
// Processor Name' used as a 'key' to verify the message type so as to
// not get accidentally mixed with user submitted messages, the remainder
// for space saving reasons, abbreviated from their variable names).
'_wpn': UTF8ToString(HEAPU32[options]),
'_wpn': processorName,
audioParams,
contextHandle,
callback,
Expand All @@ -262,18 +264,20 @@ var LibraryWebAudio = {
assert(EmAudio[contextHandle], `Called emscripten_create_wasm_audio_worklet_node() with a nonexisting/already freed Web Audio Context handle ${contextHandle}!`);
assert(EmAudio[contextHandle] instanceof (window.AudioContext || window.webkitAudioContext), `Called emscripten_create_wasm_audio_worklet_node() on a context handle ${contextHandle} that is not an AudioContext, but of type ${typeof EmAudio[contextHandle]}`);
#endif
options >>= 2;

function readChannelCountArray(heapIndex, numOutputs) {
if (!heapIndex) return undefined;
heapIndex = {{{ getHeapOffset('heapIndex', 'i32') }}};
var channelCounts = [];
while (numOutputs--) channelCounts.push(HEAPU32[heapIndex++]);
return channelCounts;
}

var optionsOutputs = options ? {{{ makeGetValue('options', C_STRUCTS.EmscriptenAudioWorkletNodeCreateOptions.numberOfOutputs, 'i32') }}} : 0;
var opts = options ? {
numberOfInputs: HEAP32[options],
numberOfOutputs: HEAP32[options+1],
outputChannelCount: HEAPU32[options+2] ? readChannelCountArray(HEAPU32[options+2]>>2, HEAP32[options+1]) : undefined,
numberOfInputs: {{{ makeGetValue('options', C_STRUCTS.EmscriptenAudioWorkletNodeCreateOptions.numberOfInputs, 'i32') }}},
numberOfOutputs: optionsOutputs,
outputChannelCount: readChannelCountArray({{{ makeGetValue('options', C_STRUCTS.EmscriptenAudioWorkletNodeCreateOptions.outputChannelCounts, 'i32*') }}}, optionsOutputs),
processorOptions: {
callback,
userData,
Expand Down
16 changes: 16 additions & 0 deletions src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,17 @@
{
"file": "emscripten/webaudio.h",
"structs": {
"WebAudioParamDescriptor": [
"defaultValue",
"minValue",
"maxValue",
"automationRate"
],
"WebAudioWorkletProcessorCreateOptions": [
"name",
"numAudioParams",
"audioParamDescriptors"
],
"AudioSampleFrame": [
"numberOfChannels",
"samplesPerChannel",
Expand All @@ -1274,6 +1285,11 @@
"AudioParamFrame": [
"length",
"data"
],
"EmscriptenAudioWorkletNodeCreateOptions": [
"numberOfInputs",
"numberOfOutputs",
"outputChannelCounts"
]
}
},
Expand Down
19 changes: 19 additions & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@
"numberOfChannels": 0,
"samplesPerChannel": 4
},
"EmscriptenAudioWorkletNodeCreateOptions": {
"__size__": 12,
"numberOfInputs": 0,
"numberOfOutputs": 4,
"outputChannelCounts": 8
},
"EmscriptenBatteryEvent": {
"__size__": 32,
"charging": 24,
Expand Down Expand Up @@ -1479,6 +1485,19 @@
"module": 4,
"nextInChain": 0
},
"WebAudioParamDescriptor": {
"__size__": 16,
"automationRate": 12,
"defaultValue": 0,
"maxValue": 8,
"minValue": 4
},
"WebAudioWorkletProcessorCreateOptions": {
"__size__": 12,
"audioParamDescriptors": 8,
"name": 0,
"numAudioParams": 4
},
"__cxa_exception": {
"__size__": 24,
"adjustedPtr": 16,
Expand Down
19 changes: 19 additions & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@
"numberOfChannels": 0,
"samplesPerChannel": 4
},
"EmscriptenAudioWorkletNodeCreateOptions": {
"__size__": 16,
"numberOfInputs": 0,
"numberOfOutputs": 4,
"outputChannelCounts": 8
},
"EmscriptenBatteryEvent": {
"__size__": 32,
"charging": 24,
Expand Down Expand Up @@ -1479,6 +1485,19 @@
"module": 8,
"nextInChain": 0
},
"WebAudioParamDescriptor": {
"__size__": 16,
"automationRate": 12,
"defaultValue": 0,
"maxValue": 8,
"minValue": 4
},
"WebAudioWorkletProcessorCreateOptions": {
"__size__": 24,
"audioParamDescriptors": 16,
"name": 0,
"numAudioParams": 8
},
"__cxa_exception": {
"__size__": 48,
"adjustedPtr": 32,
Expand Down
Loading