From bb0ea3b4add4a2c0f580eb56ea34ca128924e5d6 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 22 Sep 2025 21:56:47 -0500 Subject: [PATCH] Working version with cleanup and such --- packages/superdough/helpers.mjs | 12 +--- packages/superdough/sampler.mjs | 13 +--- packages/superdough/superdough.mjs | 104 ++++++++++++++--------------- packages/superdough/synth.mjs | 68 +++++++++++++++---- packages/superdough/worklets.mjs | 49 -------------- packages/superdough/zzfx.mjs | 6 +- 6 files changed, 112 insertions(+), 140 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 69e7e8560..de44f8d1c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -220,16 +220,8 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { // Schedule the `onComplete` callback to occur at `stopTime` constantNode.onended = () => { // Ensure garbage collection - try { - zeroGain.disconnect(); - } catch { - // pass - } - try { - constantNode.disconnect(); - } catch { - // pass - } + constantNode.disconnect(); + zeroGain.disconnect(); onComplete(); }; constantNode.start(startTime); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..0da3d9fdd 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -340,19 +340,12 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); - bufferSource.onended = function () { + const cleanup = () => { bufferSource.disconnect(); vibratoOscillator?.stop(); node.disconnect(); - out.disconnect(); - onended(); - }; - let envEnd = holdEnd + release + 0.01; - bufferSource.stop(envEnd); - const stop = (endTime) => { - bufferSource.stop(endTime); - }; - const handle = { node: out, bufferSource, stop }; + } + const handle = { node: out, cleanup }; // cut groups if (cut !== undefined) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 43e27a072..e44e9af21 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -390,19 +390,21 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 const ac = getAudioContext(); const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); - //filters - const numStages = 2; //num of filters in series + // filters + const numStages = 8; // num of filters in series let fOffset = 0; const filterChain = []; + const fMin = centerFrequency * 0.5, fMax = centerFrequency; + const ratio = Math.pow(fMax / fMin, 1 / numStages); for (let i = 0; i < numStages; i++) { const filter = ac.createBiquadFilter(); - filter.type = 'notch'; + filter.type = 'allpass'; filter.gain.value = 1; - filter.frequency.value = centerFrequency + fOffset; - filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9); + filter.frequency.value = fMin * Math.pow(ratio, i + 0.5); + filter.Q.value = 2 - clamp(depth * 2, 0, 1.9); lfoGain.connect(filter.detune); - fOffset += 282; + fOffset += 4; if (i > 0) { filterChain[i - 1].connect(filter); } @@ -560,26 +562,6 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -let counter = 0; -const livingNodes = {}; -function disconnectNodeWhenQuiet(ac, node, dependentNodes) { - const meter = new AudioWorkletNode(ac, 'meter-processor'); - const thisCounter = counter; - livingNodes[thisCounter] = dependentNodes; - counter += 1; - node.connect(meter); - let done = () => { - meter.disconnect(); - meter.port.onmessage = null; - node.disconnect(); - dependentNodes.forEach((n) => n?.disconnect()); - delete livingNodes[thisCounter]; - }; - meter.port.onmessage = (e) => { - if (e.data?.quiet) done(); - }; -} - export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); @@ -673,8 +655,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } let audioNodes = []; - let earlyNodes = []; - let finalDryNode; if (['-', '~', '_'].includes(s)) { return; @@ -685,7 +665,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // get source AudioNode - let sourceNode; + let sourceNode, cleanup; if (source) { sourceNode = source(t, value, hapDuration, cps); } else if (getSound(s)) { @@ -694,6 +674,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (soundHandle) { sourceNode = soundHandle.node; + cleanup = soundHandle.cleanup; activeSoundSources.set(chainID, soundHandle); } } else { @@ -710,7 +691,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } const chain = []; // audio nodes that will be connected to each other sequentially - chain.push(sourceNode); + chain.push({ input: sourceNode, output: sourceNode, cleanup }); FX = FX ? FX : [value]; // default to using the top-level values if FX is not specified for (const fx of FX) { let { @@ -869,15 +850,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // phaser if (fx.phaserrate !== undefined && phaserdepth > 0) { + const dry = gainNode(1); const phaserFX = getPhaser(t, endWithRelease, fx.phaserrate, phaserdepth, fx.phasercenter, fx.phasersweep); - chain.push(phaserFX); + const fb = gainNode(fx.phaserFeedback ?? 0.4); + const mix = gainNode(0.5); + dry.connect(mix); + dry.connect(phaserFX).connect(mix); + mix.connect(fb).connect(phaserFX); + cleanup = () => { + phaserFX.disconnect(); + dry.disconnect(); + } + chain.push({ input: dry, output: mix, cleanup }); } if (fx.workletSrc !== undefined) { const workletNode = getWorklet(ac, 'generic-processor', {}); - earlyNodes = [...chain]; chain.push(workletNode); - let workletSrc = fx.workletSrc; - workletSrc = workletSrc + const workletSrc = fx.workletSrc .replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i]) .replaceAll('sFreq', getFrequencyFromValue(value)) .replaceAll('sGate', `cc('strudel-gate-${chainID}')`); @@ -885,9 +874,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const { src, ugens, registers } = compileKabel(workletSrc); workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, end }); } - let dry = chain[chain.length - 1]; // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { + const dry = gainNode(1); delayfeedback = clamp(delayfeedback, 0, 0.98); const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback); const wetDelay = gainNode(delay); @@ -895,11 +884,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); dry.connect(dryDelay).connect(sum); dry.connect(delayNode).connect(wetDelay).connect(sum); - earlyNodes = [...chain]; - chain.push(sum); - audioNodes.push(dryDelay, delayNode, wetDelay); + const cleanup = () => { + dryDelay.disconnect(); + delayNode.disconnect(); + dry.disconnect(); + wetDelay.disconnect(); + } + chain.push({ input: dry, output: sum, cleanup }); } - dry = chain[chain.length - 1]; // reverb if (fx.room > 0) { let roomIR; @@ -913,6 +905,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } roomIR = await loadBuffer(url, ac, fx.ir, 0); } + const dry = gainNode(1); const reverbNode = ac.createReverb( fx.roomsize, fx.roomfade, @@ -927,13 +920,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); dry.connect(dryReverb).connect(sum); dry.connect(reverbNode).connect(wetReverb).connect(sum); - earlyNodes = [...chain]; - chain.push(sum); - audioNodes.push(dryReverb, reverbNode, wetReverb); + const cleanup = () => { + dryReverb.disconnect(); + reverbNode.disconnect(); + dry.disconnect(); + wetReverb.disconnect(); + } + chain.push({ input: dry, output: sum, cleanup }); } } - const useFXRelease = FXrelease !== undefined && FXrelease > release; - if (useFXRelease) { + if (FXrelease !== undefined && FXrelease > release) { const releaseNode = gainNode(1); releaseNode.gain.setValueAtTime(1, end + release); releaseNode.gain.linearRampToValueAtTime(0, endWithRelease); @@ -982,24 +978,26 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const dryGain = new GainNode(ac, { gain: dry }); chain.push(dryGain); connectToOrbit(dryGain, orbit); - finalDryNode = dryGain; } else { connectToOrbit(post, orbit); - finalDryNode = post; } // connect chain elements together - chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); + chain.slice(1).reduce((last, current) => { + const lastC = last.output ?? last; + const currentC = current.input ?? current; + lastC.connect(currentC); + return current.output ?? current; + }, chain[0]); audioNodes.push(...chain); - // earlyNodes = []; - earlyNodes = earlyNodes.length > 0 && !useFXRelease ? earlyNodes : audioNodes; - audioNodes = audioNodes.filter((n) => !earlyNodes.includes(n)); webAudioTimeout( ac, () => { - // audioNodes.forEach((n) => n?.disconnect()); - // earlyNodes.forEach((n) => n?.disconnect()); - disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes); + audioNodes.forEach((n) => { + const node = n?.output ?? n; + node?.disconnect(); + n?.cleanup?.(); + }); activeSoundSources.delete(chainID); }, 0, diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 48fa8a9ef..07be7cbc2 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -13,6 +13,14 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; +function destroyAudioWorkletNode(node) { + if (node == null) { + return; + } + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +} + const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveformAliases = [ ['tri', 'triangle'], @@ -51,6 +59,11 @@ export function registerSynthSounds() { const { duration } = value; + const cleanup = () => { + o.disconnect(); + g.disconnect(); + }; + const envGain = gainNode(1); let node = o.connect(g).connect(envGain); const holdEnd = t + duration; @@ -58,6 +71,7 @@ export function registerSynthSounds() { const envEnd = holdEnd + release + 0.01; return { node, + cleanup, }; }, { type: 'synth', prebake: true }, @@ -96,6 +110,14 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); + const cleanup = () => { + o.disconnect(); + g.disconnect(); + sat.disconnect(); + noise.node.disconnect(); + noiseGain.disconnect(); + }; + const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); @@ -111,6 +133,7 @@ export function registerSynthSounds() { return { node, + cleanup, }; }, { type: 'synth', prebake: true }, @@ -154,13 +177,18 @@ export function registerSynthSounds() { getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin); - let envGain = gainNode(1); - envGain = o.connect(envGain); + const node = o.connect(gainNode(1)); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + const cleanup = () => { + destroyAudioWorkletNode(o); + fm?.stop(); + vibratoOscillator?.stop(); + } return { - node: envGain, + node, + cleanup, }; }, { prebake: true, type: 'synth' }, @@ -216,13 +244,17 @@ export function registerSynthSounds() { o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency }); - let envGain = gainNode(1); - envGain = o.connect(envGain); + const node = o.connect(gainNode(1)); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + const cleanup = () => { + destroyAudioWorkletNode(o); + } return { - node: envGain, + node, + cleanup, }; }, { prebake: true, type: 'synth' }, @@ -272,18 +304,24 @@ export function registerSynthSounds() { getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin); - let envGain = gainNode(1); - envGain = o.connect(envGain); + const node = o.connect(gainNode(1)); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let lfo; if (pwsweep != 0) { lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); lfo.connect(o.parameters.get('pulsewidth')); } + const cleanup = () => { + destroyAudioWorkletNode(o); + destroyAudioWorkletNode(lfo); + fm?.stop(); + vibratoOscillator?.stop(); + } return { - node: envGain, + node, + cleanup, }; }, { prebake: true, type: 'synth' }, @@ -315,9 +353,13 @@ export function registerSynthSounds() { let node = o.connect(g).connect(envGain); const holdEnd = t + duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); - const envEnd = holdEnd + release + 0.01; + const cleanup = () => { + g.disconnect(); + o.disconnect(); + } return { node, + cleanup, }; }, { type: 'synth', prebake: true }, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 08b78f805..c99fe2645 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -910,54 +910,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor { registerProcessor('byte-beat-processor', ByteBeatProcessor); -class MeterProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.threshold = 1e-2; - this.holdMs = 40; - this.belowSince = null; - this.initialized = currentTime * 1000; - this.finished = false; - } - process(inputs) { - if (this.finished) return true; - const now = currentTime * 1000; - const chs = inputs[0] || []; - const N = chs[0]?.length || 0; - if (!N) { - if (now - this.initialized >= this.holdMs) { - this.port.postMessage({ quiet: true }); - this.finished = true; - } - return true; - } - - let sum = 0, - count = 0; - for (let c = 0; c < chs.length; c++) { - const x = chs[c]; - for (let i = 0; i < N; i++) { - const v = x[i]; - sum += v * v; - } - count += N; - } - const rms = Math.sqrt(sum / Math.max(1, count)); - - if (rms < this.threshold) { - if (this.belowSince == null) this.belowSince = now; - if (now - this.belowSince >= this.holdMs) { - this.port.postMessage({ quiet: true }); - this.finished = true; - } - } else { - this.belowSince = null; - } - return true; - } -} -registerProcessor('meter-processor', MeterProcessor); - class GenericProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -971,7 +923,6 @@ class GenericProcessor extends AudioWorkletProcessor { let { src, schema: { ugens, registers }, - gate, start, end, } = event.data; diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index a6af82609..60870fd54 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -80,12 +80,8 @@ export function registerZZFXSounds() { ['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => { registerSound( wave, - (t, value, onended) => { + (t, value) => { const { node: o } = getZZFX({ s: wave, ...value }, t); - o.onended = () => { - o.disconnect(); - onended(); - }; return { node: o, stop: () => {},