From 46291bf85ca0cf583568266f02f7370c6eb67e82 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 4 Dec 2025 13:17:20 +0000 Subject: [PATCH 1/5] Difuse the `onceEnded` mechanism --- packages/superdough/helpers.mjs | 4 ++-- packages/superdough/noise.mjs | 4 ++-- packages/superdough/synth.mjs | 13 +++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 0b696df9b..6fe1e5b72 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -324,10 +324,10 @@ export function getVibratoOscillator(param, value, t) { gain.gain.value = vibmod * 100; vibratoOscillator.connect(gain); gain.connect(param); - vibratoOscillator.onended = () => { + onceEnded(vibratoOscillator, () => { gain.disconnect(param); vibratoOscillator.disconnect(gain); - }; + }); vibratoOscillator.start(t); return vibratoOscillator; } diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 3e41515df..da60e87a1 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,4 +1,4 @@ -import { drywet } from './helpers.mjs'; +import { drywet, onceEnded } from './helpers.mjs'; import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; @@ -65,7 +65,7 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) { export function getNoiseMix(inputNode, wet, t) { const noiseOscillator = getNoiseOscillator('pink', t); const noiseMix = drywet(inputNode, noiseOscillator.node, wet); - noiseOscillator.node.onended = () => noiseMix.onended(); + onceEnded(noiseOscillator.node, () => noiseMix.onended()); return { node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index b02354465..a2d3aeca0 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,6 @@ import { clamp } from './util.mjs'; import { registerSound, soundMap } from './superdough.mjs'; +import { onceEnded } from './helpers.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, @@ -110,7 +111,7 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); - o.onended = () => { + onceEnded(o, () => { o.disconnect(); g.disconnect(); sat.disconnect(); @@ -118,7 +119,7 @@ export function registerSynthSounds() { noiseGain.disconnect(); mix.disconnect(); onended(); - }; + }); const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); @@ -384,11 +385,11 @@ export function registerSynthSounds() { const { duration } = value; - o.onended = () => { + onceEnded(o, () => { o.disconnect(); g.disconnect(); onended(); - }; + }); const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -489,11 +490,11 @@ export function getOscillator(s, t, value, onended) { noiseMix = getNoiseMix(o, noise, t); } - o.onended = () => { + onceEnded(o, () => { noiseMix || o.disconnect(); noiseMix?.node.disconnect(); onended(); - }; + }); o.start(t); return { From c7586e96ff8ff1bbeb7b8d6b965d4a8a6b8fce3b Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 4 Dec 2025 13:20:17 +0000 Subject: [PATCH 2/5] Add comment on `end` worklet param --- packages/superdough/helpers.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6fe1e5b72..f000db63e 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -613,6 +613,8 @@ export const releaseAudioNode = (node) => { // returns true and either its active source flag is true or // any AudioNode connected to one of its inputs is actively processing. if (node instanceof AudioWorkletNode) { + // while `end` is not native to the web audio API, it is common practice in superdough + // to use that param in the worklets to trigger returning false from the processor node.parameters.get('end')?.setValueAtTime(0, 0); } }; From f6f507dd291ccdaca4ca5b2d32dc456fd48edbe2 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 4 Dec 2025 15:26:59 +0000 Subject: [PATCH 3/5] propagate `releaseAudioNode` in packages --- packages/soundfonts/fontloader.mjs | 6 +++--- packages/superdough/helpers.mjs | 6 +++--- packages/superdough/sampler.mjs | 8 ++++---- packages/superdough/superdough.mjs | 4 ++-- packages/superdough/synth.mjs | 24 ++++++++++++------------ packages/superdough/wavetable.mjs | 6 +++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index ca9e45d51..95e210890 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -7,6 +7,7 @@ import { getPitchEnvelope, getVibratoOscillator, onceEnded, + releaseAudioNode, } from '@strudel/webaudio'; import gm from './gm.mjs'; @@ -172,9 +173,8 @@ export function registerSoundfonts() { bufferSource.stop(envEnd); const stop = (releaseTime) => {}; onceEnded(bufferSource, () => { - bufferSource.disconnect(); - vibratoOscillator?.stop(); - node.disconnect(); + releaseAudioNode(bufferSource); + releaseAudioNode(vibratoOscillator); onended(); }); return { node, stop }; diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index f000db63e..5202dbdfe 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -189,7 +189,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); } const lfo = getParamLfo(audioContext, param, start, end, lfoValues); - return { lfo, disconnect: () => lfo?.disconnect() }; + return lfo } export function createFilter(context, start, end, params, cps, cycle) { let { @@ -325,8 +325,8 @@ export function getVibratoOscillator(param, value, t) { vibratoOscillator.connect(gain); gain.connect(param); onceEnded(vibratoOscillator, () => { - gain.disconnect(param); - vibratoOscillator.disconnect(gain); + releaseAudioNode(gain); + releaseAudioNode(vibratoOscillator); }); vibratoOscillator.start(t); return vibratoOscillator; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 1e8c786bf..0e1e291ff 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -329,10 +329,10 @@ 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); onceEnded(bufferSource, function () { - bufferSource.disconnect(); - vibratoOscillator?.stop(); - node.disconnect(); - out.disconnect(); + releaseAudioNode(bufferSource); + releaseAudioNode(vibratoOscillator); + releaseAudioNode(node); + releaseAudioNode(out); onended(); }); let envEnd = holdEnd + release + 0.01; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6f610d514..bfcf3ab5c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend, releaseAudioNode } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -506,7 +506,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { - audioNodes.forEach((n) => n?.disconnect()); + audioNodes.forEach((n) => releaseAudioNode(n)); activeSoundSources.delete(chainID); }; const soundHandle = await onTrigger(t, value, onEnded, cps); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index a2d3aeca0..e82c889ac 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,5 @@ import { clamp } from './util.mjs'; import { registerSound, soundMap } from './superdough.mjs'; -import { onceEnded } from './helpers.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, @@ -13,6 +12,7 @@ import { getVibratoOscillator, getWorklet, noises, + onceEnded, releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; @@ -53,7 +53,7 @@ export function registerSynthSounds() { const g = gainNode(0.3); let sound = getOscillator(s, t, value, () => { - g.disconnect(); + releaseAudioNode(g); onended(); }); @@ -112,12 +112,12 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); onceEnded(o, () => { - o.disconnect(); - g.disconnect(); - sat.disconnect(); - noise.node.disconnect(); - noiseGain.disconnect(); - mix.disconnect(); + releaseAudioNode(o); + releaseAudioNode(g); + releaseAudioNode(sat); + releaseAudioNode(noise.node); + releaseAudioNode(noiseGain); + releaseAudioNode(mix); onended(); }); @@ -386,8 +386,8 @@ export function registerSynthSounds() { const { duration } = value; onceEnded(o, () => { - o.disconnect(); - g.disconnect(); + releaseAudioNode(o); + releaseAudioNode(g); onended(); }); @@ -491,8 +491,8 @@ export function getOscillator(s, t, value, onended) { } onceEnded(o, () => { - noiseMix || o.disconnect(); - noiseMix?.node.disconnect(); + noiseMix || releaseAudioNode(o); + releaseAudioNode(noiseMix?.node); onended(); }); o.start(t); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index e9d810414..4311c5cab 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -320,10 +320,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { ac, () => { releaseAudioNode(source); - vibratoOscillator?.stop(); + releaseAudioNode(vibratoOscillator); fm?.stop(); - wtPosModulators?.disconnect(); - wtWarpModulators?.disconnect(); + releaseAudioNode(wtPosModulators); + releaseAudioNode(wtWarpModulators); onended(); }, t, From 1225d0443748b04c1a84b077c71f88b5f66c7428 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 4 Dec 2025 15:29:21 +0000 Subject: [PATCH 4/5] codeformat --- packages/superdough/helpers.mjs | 2 +- packages/superdough/superdough.mjs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 5202dbdfe..a5f199fb1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -189,7 +189,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); } const lfo = getParamLfo(audioContext, param, start, end, lfoValues); - return lfo + return lfo; } export function createFilter(context, start, end, params, cps, cycle) { let { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bfcf3ab5c..98a875130 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,16 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend, releaseAudioNode } from './helpers.mjs'; +import { + createFilter, + gainNode, + getCompressor, + getDistortion, + getLfo, + getWorklet, + effectSend, + releaseAudioNode, +} from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; From 71d36b79edfdf85cb035764b418662210226d245 Mon Sep 17 00:00:00 2001 From: jeromew Date: Mon, 8 Dec 2025 17:31:34 +0000 Subject: [PATCH 5/5] Improve getOscillator/noiseMix release --- packages/superdough/helpers.mjs | 9 ++++++--- packages/superdough/noise.mjs | 7 +++++-- packages/superdough/synth.mjs | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a5f199fb1..5abc2bad1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -283,9 +283,12 @@ export function drywet(dry, wet, wetAmount = 0) { wet_gain.connect(mix); return { node: mix, - onended: () => { - dry_gain.disconnect(mix); - wet_gain.disconnect(mix); + teardown: () => { + releaseAudioNode(dry_gain); + releaseAudioNode(wet_gain); + // it is not the responsability of drywet + // to call `releaseAudioNode` on + // the 2 external args dry and wet dry.disconnect(dry_gain); wet.disconnect(wet_gain); }, diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index da60e87a1..52d9306a1 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,4 +1,4 @@ -import { drywet, onceEnded } from './helpers.mjs'; +import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs'; import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; @@ -65,9 +65,12 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) { export function getNoiseMix(inputNode, wet, t) { const noiseOscillator = getNoiseOscillator('pink', t); const noiseMix = drywet(inputNode, noiseOscillator.node, wet); - onceEnded(noiseOscillator.node, () => noiseMix.onended()); + onceEnded(noiseOscillator.node, () => { + releaseAudioNode(noiseOscillator.node); + }); return { node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), + teardown: noiseMix.teardown, }; } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e82c889ac..d9ee0d936 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -491,7 +491,8 @@ export function getOscillator(s, t, value, onended) { } onceEnded(o, () => { - noiseMix || releaseAudioNode(o); + noiseMix?.teardown(); + releaseAudioNode(o); releaseAudioNode(noiseMix?.node); onended(); });