Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cc43cd8
Logging and notes for me
cwoffenden Oct 16, 2024
5fe9631
Better error message (to see why it fails)
cwoffenden Oct 16, 2024
8a711fe
Create one-time fixed views into the heap
cwoffenden Oct 16, 2024
575027a
Allow the number of channels to increase (or the audio chain to change)
cwoffenden Oct 17, 2024
fe509b6
Work in progress, moved the output buffers first
cwoffenden Oct 18, 2024
ea4f9a9
Interim commit, work-in-progress
cwoffenden Oct 22, 2024
c39928a
Work-in-progress: using a single stack allocation
cwoffenden Oct 25, 2024
8f6a793
WIP: notes and findings
cwoffenden Oct 25, 2024
0bfa410
Correct stack offsets and verified code
cwoffenden Oct 28, 2024
0935e01
Added more assertions, minor docs
cwoffenden Oct 29, 2024
8dfe26d
Explicitly assert any changes to the stack address
cwoffenden Oct 31, 2024
9afee91
Added sample files
cwoffenden Nov 1, 2024
663ef89
Work-in-progress
cwoffenden Nov 1, 2024
4024368
Initial mixer
cwoffenden Nov 8, 2024
38d3425
Missing blank line
cwoffenden Nov 8, 2024
fc3476a
Work-in-progress (reusable audio creation and playback)
cwoffenden Nov 14, 2024
f6a78ae
Tidied mixer
cwoffenden Nov 15, 2024
ac98c9e
Typo
cwoffenden Nov 15, 2024
5163588
Added test harness hooks
cwoffenden Nov 15, 2024
dbc7cee
Added description of the test
cwoffenden Nov 15, 2024
4e9d358
Added the web audio mixer to the browser tests
cwoffenden Nov 15, 2024
55e70fa
STRICT will fail without a filled INCOMING_MODULE_JS_API
cwoffenden Nov 15, 2024
392fede
Added two audio ins to two audio outs test
cwoffenden Nov 15, 2024
56676a4
Added the mono tests
cwoffenden Nov 15, 2024
2ff5c87
Formatting
cwoffenden Nov 15, 2024
20b222b
Fixes to build with MEMORY64
cwoffenden Nov 16, 2024
db3528a
Suggestions and prep for moving work to link.py
cwoffenden Nov 20, 2024
b14084f
Tabs to spaces
cwoffenden Nov 20, 2024
dff172c
Migrated the interactive tests to btest_exit
cwoffenden Nov 22, 2024
6b92ee4
Migrated the interactive tests to btest_exit
cwoffenden Nov 22, 2024
34a5422
Test audio files are needed, browser test needs to exit
cwoffenden Nov 22, 2024
49ad4d1
Comment reflects behaviour
cwoffenden Nov 27, 2024
90c4bae
Reverted assignments to the original order
cwoffenden Nov 27, 2024
8fd7016
Removed tests (to standalone PR #23394)
cwoffenden Jan 14, 2025
88ac28e
Merge branch 'main' into cw-audio-tweaks-3
cwoffenden Jan 16, 2025
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
127 changes: 97 additions & 30 deletions src/audio_worklet.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,44 @@ function createWasmAudioWorkletProcessor(audioParams) {
let opts = args.processorOptions;
this.callbackFunction = Module['wasmTable'].get(opts['cb']);
this.userData = opts['ud'];

// 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
// 1.1: the typed array passed to process() should be the same size as this
// 'render quantum size', and this exercise of passing in the value
// shouldn't be required (to be verified).
// context that created this processor. Even though this 'render quantum
// size' is fixed at 128 samples in the 1.0 spec, it will be variable in
// the 1.1 spec. It's passed in now, just to prove it's settable, but will
// eventually be a property of the AudioWorkletGlobalScope (globalThis).
this.samplesPerChannel = opts['sc'];

// Create up-front as many typed views for marshalling the output data as
// may be required (with an arbitrary maximum of 10, for the case where a
// multi-MB stack is passed), allocated at the *top* of the worklet's
// stack (and whose addresses are fixed). The 'minimum alloc' firstly
// stops STACK_OVERFLOW_CHECK failing (since the stack will be full, and
// 16 being the minimum allocation size due to alignments) and leaves room
// for a single AudioSampleFrame as a minumum.
this.maxBuffers = Math.min(((Module['sz'] - /*minimum alloc*/ 16) / (this.samplesPerChannel * 4)) | 0, /*sensible limit*/ 10);
#if ASSERTIONS
console.assert(this.maxBuffers > 0, `AudioWorklet needs more stack allocating (at least ${this.samplesPerChannel * 4})`);
#endif
// These are still alloc'd to take advantage of the overflow checks, etc.
var oldStackPtr = stackSave();
var viewDataIdx = stackAlloc(this.maxBuffers * this.samplesPerChannel * 4) >> 2;
#if WEBAUDIO_DEBUG
console.log(`AudioWorklet creating ${this.maxBuffers} buffer one-time views (for a stack size of ${Module['sz']})`);
#endif
this.outputViews = [];
for (var i = this.maxBuffers; i > 0; i--) {
// Added in reverse so the lowest indices are closest to the stack top
this.outputViews.unshift(
HEAPF32.subarray(viewDataIdx, viewDataIdx += this.samplesPerChannel)
);
}
stackRestore(oldStackPtr);

#if ASSERTIONS
// Explicitly verify this later in process()
this.ctorOldStackPtr = oldStackPtr;
#endif
}

static get parameterDescriptors() {
Expand All @@ -52,22 +84,36 @@ function createWasmAudioWorkletProcessor(audioParams) {
numOutputs = outputList.length,
numParams = 0, i, j, k, dataPtr,
bytesPerChannel = this.samplesPerChannel * 4,
outputViewsNeeded = 0,
stackMemoryNeeded = (numInputs + numOutputs) * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}},
oldStackPtr = stackSave(),
inputsPtr, outputsPtr, outputDataPtr, paramsPtr,
inputsPtr, outputsPtr, paramsPtr,
didProduceAudio, paramArray;

// Calculate how much stack space is needed.
// Calculate how much stack space is needed
for (i of inputList) stackMemoryNeeded += i.length * bytesPerChannel;
for (i of outputList) stackMemoryNeeded += i.length * bytesPerChannel;
for (i of outputList) outputViewsNeeded += i.length;
stackMemoryNeeded += outputViewsNeeded * bytesPerChannel;
for (i in parameters) stackMemoryNeeded += parameters[i].byteLength + {{{ C_STRUCTS.AudioParamFrame.__size__ }}}, ++numParams;

// Allocate the necessary stack space.
inputsPtr = stackAlloc(stackMemoryNeeded);
#if ASSERTIONS
console.assert(oldStackPtr == this.ctorOldStackPtr, 'AudioWorklet stack address has unexpectedly moved');
console.assert(outputViewsNeeded <= this.outputViews.length, `Too many AudioWorklet outputs (need ${outputViewsNeeded} but have stack space for ${this.outputViews.length})`);
#endif

// Allocate the necessary stack space (dataPtr is always in bytes, and
// advances as space for structs and data is taken, but note the switching
// between bytes and indices into the various heaps, usually in 'k'). This
// will be 16-byte aligned (from _emscripten_stack_alloc()), as were the
// output views, so we round up and advance the required bytes to ensure
// the addresses all work out at the end.
i = (stackMemoryNeeded + 15) & ~15;
dataPtr = stackAlloc(i) + (i - stackMemoryNeeded);

// Copy input audio descriptor structs and data to Wasm
inputsPtr = dataPtr;
k = inputsPtr >> 2;
dataPtr = inputsPtr + numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
dataPtr += numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
for (i of inputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
Expand All @@ -81,20 +127,6 @@ function createWasmAudioWorkletProcessor(audioParams) {
}
}

// Copy output audio descriptor structs to Wasm
outputsPtr = dataPtr;
k = outputsPtr >> 2;
outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}) >> 2;
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 }}};
// Reserve space for the output data
dataPtr += bytesPerChannel * i.length;
}

// Copy parameters descriptor structs and data to Wasm
paramsPtr = dataPtr;
k = paramsPtr >> 2;
Expand All @@ -109,17 +141,52 @@ function createWasmAudioWorkletProcessor(audioParams) {
dataPtr += paramArray.length*4;
}

// Copy output audio descriptor structs to Wasm (note that dataPtr after
// the struct offsets should now be 16-byte aligned).
outputsPtr = dataPtr;
k = outputsPtr >> 2;
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 }}};
// Advance the output pointer to the next output (matching the pre-allocated views)
dataPtr += bytesPerChannel * i.length;
}

#if ASSERTIONS
// If all the maths worked out, we arrived at the original stack address
console.assert(dataPtr == oldStackPtr, `AudioWorklet stack missmatch (audio data finishes at ${dataPtr} instead of ${oldStackPtr})`);

// Sanity checks. If these trip the most likely cause, beyond unforeseen
// stack shenanigans, is that the 'render quantum size' changed.
if (numOutputs) {
// First that the output view addresses match the stack positions.
k = dataPtr - bytesPerChannel;
for (i = 0; i < outputViewsNeeded; i++) {
console.assert(k == this.outputViews[i].byteOffset, 'AudioWorklet internal error in addresses of the output array views');
k -= bytesPerChannel;
}
// And that the views' size match the passed in output buffers
for (i of outputList) {
for (j of i) {
console.assert(j.byteLength == bytesPerChannel, `AudioWorklet unexpected output buffer size (expected ${bytesPerChannel} got ${j.byteLength})`);
}
}
}
#endif

// 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)
// The preallocated 'outputViews' already have the correct offsets and
// sizes into the stack (recall from the ctor that they run backwards).
k = outputViewsNeeded - 1;
for (i of outputList) {
for (j of i) {
for (k = 0; k < this.samplesPerChannel; ++k) {
j[k] = HEAPF32[outputDataPtr++];
}
j.set(this.outputViews[k--]);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/library_webaudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ let LibraryWebAudio = {

let audioWorkletCreationFailed = () => {
#if WEBAUDIO_DEBUG
console.error(`emscripten_start_wasm_audio_worklet_thread_async() addModule() failed!`);
// Note about Cross-Origin here: a lack of Cross-Origin-Opener-Policy and
// Cross-Origin-Embedder-Policy headers to the client request will result
// in the worklet file failing to load.
console.error(`emscripten_start_wasm_audio_worklet_thread_async() addModule() failed! Are the Cross-Origin headers being set?`);
#endif
{{{ makeDynCall('viip', 'callback') }}}(contextHandle, 0/*EM_FALSE*/, userData);
};
Expand Down
Loading