diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 29fa48b3fc76f..552dd0c19c669 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -28,11 +28,43 @@ function createWasmAudioWorkletProcessor(audioParams) { 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 - // 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.samplesPerChannel; + this.bytesPerChannel = this.samplesPerChannel * {{{ getNativeTypeSize('float') }}}; + + // Create up-front as many typed views for marshalling the output data as + // may be required (with an arbitrary maximum of 16, 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(((wwParams.stackSize - /*minimum alloc*/ 16) / this.bytesPerChannel) | 0, /*sensible limit*/ 16); +#if ASSERTIONS + console.assert(this.maxBuffers > 0, `AudioWorklet needs more stack allocating (at least ${this.bytesPerChannel})`); +#endif + // These are still alloc'd to take advantage of the overflow checks, etc. + var oldStackPtr = stackSave(); + var viewDataIdx = {{{ getHeapOffset('stackAlloc(this.maxBuffers * this.bytesPerChannel)', 'float') }}}; +#if WEBAUDIO_DEBUG + console.log(`AudioWorklet creating ${this.maxBuffers} buffer one-time views (for a stack size of ${wwParams.stackSize} at address 0x${(viewDataIdx * 4).toString(16)})`); +#endif + this.outputViews = []; + for (var n = this.maxBuffers; n > 0; n--) { + // 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() { @@ -53,26 +85,43 @@ function createWasmAudioWorkletProcessor(audioParams) { var entry; // reused list entry or index var subentry; // reused channel or other array in each list entry or index - // Calculate how much stack space is needed. - var bytesPerChannel = this.samplesPerChannel * {{{ getNativeTypeSize('float') }}}; + // Calculate the required stack and output buffer views var stackMemoryNeeded = (numInputs + numOutputs) * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; + for (entry of inputList) { + stackMemoryNeeded += entry.length * this.bytesPerChannel; + } + var outputViewsNeeded = 0; + for (entry of outputList) { + outputViewsNeeded += entry.length; + } + stackMemoryNeeded += outputViewsNeeded * this.bytesPerChannel; var numParams = 0; - for (entry of inputList) stackMemoryNeeded += entry.length * bytesPerChannel; - for (entry of outputList) stackMemoryNeeded += entry.length * bytesPerChannel; for (entry in parameters) { stackMemoryNeeded += parameters[entry].byteLength + {{{ C_STRUCTS.AudioParamFrame.__size__ }}}; ++numParams; } +#if ASSERTIONS + 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. var oldStackPtr = stackSave(); - var inputsPtr = stackAlloc(stackMemoryNeeded); + // Allocate the necessary stack space. All pointer variables are always in + // bytes; 'dataPtr' is the start of the data section, advancing as space + // for structs and data is taken; 'structPtr' is reused as the working + // start to each struct record. + // Ordinarily 'dataPtr' would be 16-byte aligned, from the internal + // _emscripten_stack_alloc(), as were the output views, and so to ensure + // the views fall on the correct addresses (and we finish at the stacktop) + // bytes are added and the start advanced. + var alignedNeededStack = (stackMemoryNeeded + 15) & ~15; + var dataPtr = stackAlloc(alignedNeededStack); - // Copy input audio descriptor structs and data to Wasm ('structPtr' is - // reused as the working start to each struct record, 'dataPtr' start of - // the data section, usually after all structs). + // Copy input audio descriptor structs and data to Wasm (recall, structs + // first, audio data after). 'inputsPtr' is the start of the C callback's + // input AudioSampleFrame. + var /*const*/ inputsPtr = dataPtr; var structPtr = inputsPtr; - var dataPtr = inputsPtr + numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; + dataPtr += numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; for (entry of inputList) { // Write the AudioSampleFrame struct instance {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'entry.length', 'u32') }}}; @@ -82,26 +131,13 @@ function createWasmAudioWorkletProcessor(audioParams) { // Marshal the input audio sample data for each audio channel of this input for (subentry of entry) { HEAPF32.set(subentry, {{{ getHeapOffset('dataPtr', 'float') }}}); - dataPtr += bytesPerChannel; + dataPtr += this.bytesPerChannel; } } - // Copy output audio descriptor structs to Wasm - var outputsPtr = dataPtr; - structPtr = outputsPtr; - var outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}); - for (entry of outputList) { - // Write the AudioSampleFrame struct instance - {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'entry.length', 'u32') }}}; - {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}}; - {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}}; - structPtr += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; - // Reserve space for the output data - dataPtr += bytesPerChannel * entry.length; - } - - // Copy parameters descriptor structs and data to Wasm - var paramsPtr = dataPtr; + // Copy parameters descriptor structs and data to Wasm. 'paramsPtr' is the + // start of the C callback's input AudioParamFrame. + var /*const*/ paramsPtr = dataPtr; structPtr = paramsPtr; dataPtr += numParams * {{{ C_STRUCTS.AudioParamFrame.__size__ }}}; for (entry = 0; subentry = parameters[entry++];) { @@ -114,20 +150,60 @@ function createWasmAudioWorkletProcessor(audioParams) { dataPtr += subentry.length * {{{ getNativeTypeSize('float') }}}; } + // TODO: why does Chrome wasm64 (Chrome has weird rules for params) need + // the manual alignment here? An off-by-one somewhere? outputsPtr is + // getting clobbered otherwise, is the struct correctly aligned? Do more + // stack allocs instead? Probably needs alignments between struct writes? + dataPtr += alignedNeededStack - stackMemoryNeeded; + + // Copy output audio descriptor structs to Wasm. 'outputsPtr' is the start + // of the C callback's output AudioSampleFrame. + // Note: dataPtr after the struct offsets should now be 16-byte aligned. + var /*const*/ outputsPtr = dataPtr; + structPtr = outputsPtr; + dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; + for (entry of outputList) { + // Write the AudioSampleFrame struct instance + {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.numberOfChannels, 'entry.length', 'u32') }}}; + {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.samplesPerChannel, 'this.samplesPerChannel', 'u32') }}}; + {{{ makeSetValue('structPtr', C_STRUCTS.AudioSampleFrame.data, 'dataPtr', '*') }}}; + structPtr += {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}; + // Advance the output pointer to the next output (matching the pre-allocated views) + dataPtr += this.bytesPerChannel * entry.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 after + // construction (which shouldn't be possible). + if (numOutputs) { + // First that the output view addresses match the stack positions + dataPtr -= this.bytesPerChannel; + for (entry = 0; entry < outputViewsNeeded; entry++) { + console.assert(dataPtr == this.outputViews[entry].byteOffset, 'AudioWorklet internal error in addresses of the output array views'); + dataPtr -= this.bytesPerChannel; + } + // And that the views' size match the passed in output buffers + for (entry of outputList) { + for (subentry of entry) { + console.assert(subentry.byteLength == this.bytesPerChannel, `AudioWorklet unexpected output buffer size (expected ${this.bytesPerChannel} got ${subentry.byteLength})`); + } + } + } +#endif + // Call out to Wasm callback to perform audio processing var didProduceAudio = this.callback(numInputs, inputsPtr, numOutputs, outputsPtr, numParams, paramsPtr, this.userData); if (didProduceAudio) { // 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) - outputDataPtr = {{{ getHeapOffset('outputDataPtr', 'float') }}}; + // The preallocated 'outputViews' already have the correct offsets and + // sizes into the stack (recall from the ctor that they run backwards). for (entry of outputList) { for (subentry of entry) { - // repurposing structPtr for now - for (structPtr = 0; structPtr < this.samplesPerChannel; ++structPtr) { - subentry[structPtr] = HEAPF32[outputDataPtr++]; - } + subentry.set(this.outputViews[--outputViewsNeeded]); } } } diff --git a/test/code_size/audio_worklet_wasm.js b/test/code_size/audio_worklet_wasm.js index 6792f50a603dd..96048c30f62df 100644 --- a/test/code_size/audio_worklet_wasm.js +++ b/test/code_size/audio_worklet_wasm.js @@ -1,18 +1,18 @@ -var l = globalThis.Module || "undefined" != typeof Module ? Module : {}, n = "em-ww" == globalThis.name, q = "undefined" !== typeof AudioWorkletGlobalScope, t, v, J, K, E, F, A, X, H, C, B, Y, Z; +var k = globalThis.Module || "undefined" != typeof Module ? Module : {}, p = "em-ww" == globalThis.name, q = "undefined" !== typeof AudioWorkletGlobalScope, t, v, J, K, F, D, A, X, E, C, B, Y, Z; -q && (n = !0); +q && (p = !0); function u(a) { t = a; - v = a.I; - y(); - l ||= {}; - l.wasm = a.C; + v = a.L; + x(); + k ||= {}; + k.wasm = a.H; z(); - a.C = a.J = 0; + a.H = a.M = 0; } -n && !q && (onmessage = a => { +p && !q && (onmessage = a => { onmessage = null; u(a.data); }); @@ -23,43 +23,48 @@ if (q) { constructor(d) { super(); d = d.processorOptions; - this.u = A.get(d.u); - this.v = d.v; - this.s = d.s; + this.v = A.get(d.v); + this.A = d.A; + this.u = d.u; + this.s = 4 * this.u; + this.F = Math.min((t.D - 16) / this.s | 0, 16); + d = B(); + var f = C(this.F * this.s) >> 2; + this.G = []; + for (var g = this.F; 0 < g; g--) this.G.unshift(D.subarray(f, f += this.u)); + E(d); } static get parameterDescriptors() { return c; } - process(d, g, h) { - var p = d.length, w = g.length, k, r, x = 4 * this.s, f = 12 * (p + w), D = 0; - for (k of d) f += k.length * x; - for (k of g) f += k.length * x; - for (k in h) f += h[k].byteLength + 8, ++D; - var V = B(), G = C(f); - f = G; - var m = G + 12 * p; - for (k of d) { - E[f >> 2] = k.length; - E[f + 4 >> 2] = this.s; - E[f + 8 >> 2] = m; - f += 12; - for (r of k) F.set(r, m >> 2), m += x; + process(d, f, g) { + var n = d.length, w = f.length, h, r, y = 12 * (n + w); + for (h of d) y += h.length * this.s; + var G = 0; + for (h of f) G += h.length; + y += G * this.s; + var H = 0; + for (h in g) y += g[h].byteLength + 8, ++H; + var W = B(), O = y + 15 & -16, l = C(O), P = l, m = P; + l += 12 * n; + for (h of d) { + F[m >> 2] = h.length; + F[m + 4 >> 2] = this.u; + F[m + 8 >> 2] = l; + m += 12; + for (r of h) D.set(r, l >> 2), l += this.s; } - var O = m; - f = O; - d = m += 12 * w; - for (k of g) E[f >> 2] = k.length, E[f + 4 >> 2] = this.s, E[f + 8 >> 2] = m, f += 12, - m += x * k.length; - f = x = m; - m += 8 * D; - for (k = 0; r = h[k++]; ) E[f >> 2] = r.length, E[f + 4 >> 2] = m, f += 8, F.set(r, m >> 2), - m += 4 * r.length; - if (h = this.u(p, G, w, O, D, x, this.v)) { - d >>= 2; - for (k of g) for (r of k) for (f = 0; f < this.s; ++f) r[f] = F[d++]; - } - H(V); - return !!h; + m = d = l; + l += 8 * H; + for (h = 0; r = g[h++]; ) F[m >> 2] = r.length, F[m + 4 >> 2] = l, m += 8, D.set(r, l >> 2), + l += 4 * r.length; + m = g = l += O - y; + l += 12 * w; + for (h of f) F[m >> 2] = h.length, F[m + 4 >> 2] = this.u, F[m + 8 >> 2] = l, m += 12, + l += this.s * h.length; + if (n = this.v(n, P, w, g, H, d, this.A)) for (h of f) for (r of h) r.set(this.G[--G]); + E(W); + return !!n; } } return e; @@ -72,10 +77,10 @@ if (q) { I = this.port; I.onmessage = async e => { e = e.data; - e._wpn ? (registerProcessor(e._wpn, a(e.D)), I.postMessage({ - _wsc: e.u, - A: [ e.F, 1, e.v ] - })) : e._wsc && A.get(e._wsc)(...e.A); + e._wpn ? (registerProcessor(e._wpn, a(e.I)), I.postMessage({ + _wsc: e.v, + B: [ e.J, 1, e.A ] + })) : e._wsc && A.get(e._wsc)(...e.B); }; } process() {} @@ -83,19 +88,19 @@ if (q) { registerProcessor("em-bootstrap", b); } -function y() { +function x() { var a = v.buffer; J = new Uint8Array(a); K = new Int32Array(a); - E = new Uint32Array(a); - F = new Float32Array(a); + F = new Uint32Array(a); + D = new Float32Array(a); } -n || (v = l.mem || new WebAssembly.Memory({ +p || (v = k.mem || new WebAssembly.Memory({ initial: 256, maximum: 256, shared: !0 -}), y()); +}), x()); var L = [], M = a => { a = a.data; @@ -103,106 +108,106 @@ var L = [], M = a => { b && A.get(b)(...a.x); }, N = a => { L.push(a); -}, P = a => H(a), Q = () => B(), S = (a, b, c, e) => { - b = R[b]; - R[a].connect(b.destination || b, c, e); -}, R = {}, T = 0, U = "undefined" != typeof TextDecoder ? new TextDecoder : void 0, W = (a = 0) => { +}, Q = a => E(a), R = () => B(), aa = (a, b, c, e) => { + b = S[b]; + S[a].connect(b.destination || b, c, e); +}, S = {}, T = 0, U = "undefined" != typeof TextDecoder ? new TextDecoder : void 0, V = (a = 0) => { for (var b = J, c = a, e = c + void 0; b[c] && !(c >= e); ) ++c; if (16 < c - a && b.buffer && U) return U.decode(b.slice(a, c)); for (e = ""; a < c; ) { var d = b[a++]; if (d & 128) { - var g = b[a++] & 63; - if (192 == (d & 224)) e += String.fromCharCode((d & 31) << 6 | g); else { - var h = b[a++] & 63; - d = 224 == (d & 240) ? (d & 15) << 12 | g << 6 | h : (d & 7) << 18 | g << 12 | h << 6 | b[a++] & 63; + var f = b[a++] & 63; + if (192 == (d & 224)) e += String.fromCharCode((d & 31) << 6 | f); else { + var g = b[a++] & 63; + d = 224 == (d & 240) ? (d & 15) << 12 | f << 6 | g : (d & 7) << 18 | f << 12 | g << 6 | b[a++] & 63; 65536 > d ? e += String.fromCharCode(d) : (d -= 65536, e += String.fromCharCode(55296 | d >> 10, 56320 | d & 1023)); } } else e += String.fromCharCode(d); } return e; -}, aa = a => { +}, ba = a => { var b = window.AudioContext || window.webkitAudioContext; if (a) { - var c = E[a >> 2]; + var c = F[a >> 2]; a = { - latencyHint: (c ? W(c) : "") || void 0, - sampleRate: E[a + 4 >> 2] || void 0 + latencyHint: (c ? V(c) : "") || void 0, + sampleRate: F[a + 4 >> 2] || void 0 }; } else a = void 0; - if (c = b) b = new b(a), R[++T] = b, c = T; + if (c = b) b = new b(a), S[++T] = b, c = T; return c; -}, ba = (a, b, c, e, d) => { - var g = c ? K[c + 4 >> 2] : 0; +}, ca = (a, b, c, e, d) => { + var f = c ? K[c + 4 >> 2] : 0; if (c) { - var h = K[c >> 2]; - c = E[c + 8 >> 2]; - var p = g; + var g = K[c >> 2]; + c = F[c + 8 >> 2]; + var n = f; if (c) { c >>= 2; - for (var w = []; p--; ) w.push(E[c++]); + for (var w = []; n--; ) w.push(F[c++]); c = w; } else c = void 0; e = { - numberOfInputs: h, - numberOfOutputs: g, + numberOfInputs: g, + numberOfOutputs: f, outputChannelCount: c, processorOptions: { - u: e, - v: d, - s: 128 + v: e, + A: d, + u: 128 } }; } else e = void 0; - a = new AudioWorkletNode(R[a], b ? W(b) : "", e); - R[++T] = a; + a = new AudioWorkletNode(S[a], b ? V(b) : "", e); + S[++T] = a; return T; -}, ca = (a, b, c, e) => { - var d = [], g = (g = E[b >> 2]) ? W(g) : "", h = K[b + 4 >> 2]; - b = E[b + 8 >> 2]; - for (var p = 0; h--; ) d.push({ - name: p++, - defaultValue: F[b >> 2], - minValue: F[b + 4 >> 2], - maxValue: F[b + 8 >> 2], +}, da = (a, b, c, e) => { + var d = [], f = (f = F[b >> 2]) ? V(f) : "", g = K[b + 4 >> 2]; + b = F[b + 8 >> 2]; + for (var n = 0; g--; ) d.push({ + name: n++, + defaultValue: D[b >> 2], + minValue: D[b + 4 >> 2], + maxValue: D[b + 8 >> 2], automationRate: (K[b + 12 >> 2] ? "k" : "a") + "-rate" }), b += 16; - R[a].audioWorklet.B.port.postMessage({ - _wpn: g, - D: d, - F: a, - u: c, - v: e + S[a].audioWorklet.C.port.postMessage({ + _wpn: f, + I: d, + J: a, + v: c, + A: e }); -}, da = () => !1, ea = 1, fa = a => { +}, ea = () => !1, fa = 1, ha = a => { a = a.data; var b = a._wsc; - b && A.get(b)(...a.A); -}, ha = a => C(a), ia = (a, b, c, e, d) => { - var g = R[a], h = g.audioWorklet, p = () => { + b && A.get(b)(...a.B); +}, ia = a => C(a), ja = (a, b, c, e, d) => { + var f = S[a], g = f.audioWorklet, n = () => { A.get(e)(a, 0, d); }; - if (!h) return p(); - h.addModule(l.js).then((() => { - h.B = new AudioWorkletNode(g, "em-bootstrap", { + if (!g) return n(); + g.addModule(k.js).then((() => { + g.C = new AudioWorkletNode(f, "em-bootstrap", { processorOptions: { - K: ea++, - C: l.wasm, - I: v, - G: b, - H: c + N: fa++, + H: k.wasm, + L: v, + K: b, + D: c } }); - h.B.port.onmessage = fa; + g.C.port.onmessage = ha; A.get(e)(a, 1, d); - })).catch(p); + })).catch(n); }; -function ja(a) { +function ka(a) { let b = document.createElement("button"); b.innerHTML = "Toggle playback"; document.body.appendChild(b); - a = R[a]; + a = S[a]; b.onclick = () => { "running" != a.state ? a.resume() : a.suspend(); }; @@ -210,33 +215,33 @@ function ja(a) { function z() { Z = { - f: ja, - g: S, - d: aa, - h: ba, - e: ca, - b: da, - c: ia, + f: ka, + g: aa, + d: ba, + h: ca, + e: da, + b: ea, + c: ja, a: v }; - WebAssembly.instantiate(l.wasm, { + WebAssembly.instantiate(k.wasm, { a: Z }).then((a => { a = (a.instance || a).exports; X = a.j; - H = a.l; + E = a.l; C = a.m; B = a.n; Y = a.o; A = a.k; - l.stackSave = Q; - l.stackAlloc = ha; - l.stackRestore = P; - l.wasmTable = A; - n ? (Y(t.G, t.H), "undefined" === typeof AudioWorkletGlobalScope && (removeEventListener("message", N), + k.stackSave = R; + k.stackAlloc = ia; + k.stackRestore = Q; + k.wasmTable = A; + p ? (Y(t.K, t.D), "undefined" === typeof AudioWorkletGlobalScope && (removeEventListener("message", N), L = L.forEach(M), addEventListener("message", M))) : a.i(); - n || X(); + p || X(); })); } -n || z(); \ No newline at end of file +p || z(); \ No newline at end of file diff --git a/test/code_size/test_minimal_runtime_code_size_audio_worklet.json b/test/code_size/test_minimal_runtime_code_size_audio_worklet.json index cf4f0e7fce368..ed6a08c3cd13e 100644 --- a/test/code_size/test_minimal_runtime_code_size_audio_worklet.json +++ b/test/code_size/test_minimal_runtime_code_size_audio_worklet.json @@ -1,10 +1,10 @@ { "a.html": 519, "a.html.gz": 357, - "a.js": 3882, - "a.js.gz": 2038, + "a.js": 4062, + "a.js.gz": 2128, "a.wasm": 1288, "a.wasm.gz": 860, - "total": 5689, - "total_gz": 3255 + "total": 5869, + "total_gz": 3345 } diff --git a/test/webaudio/audioworklet_params_mixing.c b/test/webaudio/audioworklet_params_mixing.c index 7b2f56a80cae0..21f8661cd0817 100644 --- a/test/webaudio/audioworklet_params_mixing.c +++ b/test/webaudio/audioworklet_params_mixing.c @@ -33,7 +33,7 @@ bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, Audi } // Interestingly, params varies per browser. Chrome won't have a length > 1 // unless the value changes, and FF has all 128 entries even for a k-rate - // parameter. The only given is that two params are incoming: + // parameter. The only given for this test is that two params are incoming: assert(numParams == 2); assert(params[0].length == 1 || params[0].length == outSamplesPerChannel); assert(params[1].length == 1 || params[1].length == outSamplesPerChannel);