From 89247d92dbce4109640032ca50e40f6053da6a7f Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 18 Nov 2025 13:52:22 -0600 Subject: [PATCH] Revert to old stop format for compatibility --- packages/soundfonts/fontloader.mjs | 9 ++- packages/superdough/helpers.mjs | 14 ++--- packages/superdough/sampler.mjs | 5 +- packages/superdough/superdough.mjs | 47 +++++----------- packages/superdough/synth.mjs | 90 ++++++++++++++++++++---------- packages/superdough/wavetable.mjs | 20 +++++-- packages/superdough/zzfx.mjs | 9 +-- 7 files changed, 109 insertions(+), 85 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 441d294b0..1c6178264 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -144,7 +144,7 @@ export function registerSoundfonts() { Object.entries(gm).forEach(([name, fonts]) => { registerSound( name, - async (time, value) => { + async (time, value, onended) => { const [attack, decay, sustain, release] = getADSRValues([ value.attack, value.decay, @@ -166,10 +166,13 @@ export function registerSoundfonts() { let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); // pitch envelope getPitchEnvelope(bufferSource.detune, value, time, holdEnd); - const cleanup = () => { + bufferSource.onended = () => { cleanupNodes([bufferSource, vibratoOscillator, node]); + onended(); }; - return { node, cleanup }; + const envEnd = holdEnd + release + 0.01; + bufferSource.stop(envEnd); + return { node, stop: bufferSource.stop }; }, { type: 'soundfont', prebake: true, fonts }, ); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index fbc1e0b7a..0339d53f4 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -518,15 +518,13 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { return Number(freq); }; -export const cleanupNode = (node) => { - if (node == null) { - return; - } - node.disconnect(); +export const cleanupNode = (node, time) => { + if (node == null) return; + node.disconnect?.(); node.parameters?.get('end')?.setValueAtTime(0, 0); - node.stop?.(); + node.stop?.(time); }; -export const cleanupNodes = (nodes) => { - nodes.map(cleanupNodes); +export const cleanupNodes = (nodes, time) => { + nodes.map((n) => cleanupNode(n, time)); }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 1d4fcedec..1a7bc5ad7 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -319,10 +319,11 @@ 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); - const cleanup = () => { + const stop = () => { cleanupNodes([bufferSource, vibratoOscillator, node]); + onended(); }; - const handle = { node: out, cleanup }; + const handle = { node: out, stop }; // cut groups if (cut !== undefined) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d271d2c84..60c6c05f9 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -10,6 +10,8 @@ import './vowel.mjs'; import { _mod, clamp, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { + cleanupNode, + cleanupNodes, createFilter, gainNode, getCompressor, @@ -152,7 +154,7 @@ export const getAudioDevices = async () => { let defaultDefaultValues = { s: 'triangle', - gain: 1, + gain: 0.8, postgain: 1, density: '.03', channels: [1, 2], @@ -169,6 +171,7 @@ let defaultDefaultValues = { velocity: 1, fft: 8, tremolodepth: 1, + tremolophase: 0, drive: 0.69, }; @@ -484,15 +487,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // get source AudioNode - let sourceNode, cleanup; + let sourceNode, stop; if (source) { sourceNode = source(t, value, hapDuration, cps); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, value, () => void 0, cps); + const soundHandle = await onTrigger(t, value, () => {}, cps); if (soundHandle) { sourceNode = soundHandle.node; - cleanup = soundHandle.cleanup; + stop = soundHandle.stop; activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC } } else { @@ -509,7 +512,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({ input: sourceNode, output: sourceNode, cleanup }); + chain.push({ input: sourceNode, output: sourceNode, stop }); FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well for (const fx of FX) { let { @@ -684,18 +687,8 @@ 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); - 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 }); + chain.push(phaserFX); } // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { @@ -707,13 +700,8 @@ 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); - const cleanup = () => { - dryDelay.disconnect(); - delayNode.disconnect(); - dry.disconnect(); - wetDelay.disconnect(); - }; - chain.push({ input: dry, output: sum, cleanup }); + const stop = () => cleanupNodes([dryDelay, delayNode, dry, wetDelay]); + chain.push({ input: dry, output: sum, stop }); } // reverb if (fx.room > 0) { @@ -743,13 +731,8 @@ 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); - const cleanup = () => { - dryReverb.disconnect(); - reverbNode.disconnect(); - dry.disconnect(); - wetReverb.disconnect(); - }; - chain.push({ input: dry, output: sum, cleanup }); + const stop = () => cleanupNodes([dryReverb, reverbNode, dry, wetReverb]); + chain.push({ input: dry, output: sum, stop }); } } @@ -818,9 +801,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ac, () => { audioNodes.forEach((n) => { - const node = n?.output ?? n; - node?.disconnect(); - n?.cleanup?.(); + cleanupNode(n?.output ?? n, endWithRelease + 0.01); }); activeSoundSources.delete(chainID); }, diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index b52efd87e..fc9c5517b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -13,6 +13,7 @@ import { getVibratoOscillator, getWorklet, noises, + webAudioTimeout, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; @@ -39,24 +40,27 @@ export function registerSynthSounds() { [...waveforms].forEach((s) => { registerSound( s, - (t, value) => { + (t, value, onended) => { const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], 'linear', [0.001, 0.05, 0.6, 0.01], ); - const { node: o } = getOscillator(s, t, value); + const { node: o, stop } = getOscillator(s, t, value); // turn down const g = gainNode(0.3); - const cleanup = () => { + o.onended = () => { cleanupNodes([o, g]); + onended(); }; const node = o.connect(g).connect(gainNode(1)); const holdEnd = t + value.duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + const envEnd = holdEnd + release + 0.01; + stop(envEnd); return { node, - cleanup, + stop, }; }, { type: 'synth', prebake: true }, @@ -65,7 +69,7 @@ export function registerSynthSounds() { registerSound( 'sbd', - (t, value) => { + (t, value, onended) => { const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; const ctx = getAudioContext(); const attackhold = 0.02; @@ -95,8 +99,9 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); - const cleanup = () => { + o.onended = () => { cleanupNodes([o, g, sat, noise.node, noiseGain]); + onended(); }; const node = o.connect(sat).connect(g).connect(mix); @@ -112,9 +117,11 @@ export function registerSynthSounds() { mix.gain.setValueAtTime(mixGain, end - 0.01); mix.gain.linearRampToValueAtTime(0, end); + o.stop(end); + return { node, - cleanup, + stop: o.stop, }; }, { type: 'synth', prebake: true }, @@ -122,7 +129,7 @@ export function registerSynthSounds() { registerSound( 'supersaw', - (begin, value) => { + (begin, value, onended) => { const ac = getAudioContext(); let { duration, n, unison = 5, spread = 0.6, detune } = value; detune = detune ?? n ?? 0.18; @@ -162,12 +169,18 @@ export function registerSynthSounds() { getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - const cleanup = () => { - cleanupNodes([o, fm, vibratoOscillator]); - }; + const timeoutNode = webAudioTimeout( + ac, + () => { + cleanupNodes([o, fm, vibratoOscillator]); + onended(); + }, + begin, + end, + ); return { node, - cleanup, + stop: timeoutNode.stop, }; }, { prebake: true, type: 'synth' }, @@ -175,7 +188,7 @@ export function registerSynthSounds() { registerSound( 'bytebeat', - (begin, value) => { + (begin, value, onended) => { const defaultBeats = [ '(t%255 >= t/255%255)*255', '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', @@ -223,14 +236,18 @@ export function registerSynthSounds() { o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency }); const node = o.connect(gainNode(1)); getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - - const cleanup = () => { - cleanupNodes([node, o]); - }; - + const timeoutNode = webAudioTimeout( + ac, + () => { + cleanupNodes([node, o]); + onended(); + }, + begin, + end, + ); return { node, - cleanup, + stop: timeoutNode.stop, }; }, { prebake: true, type: 'synth' }, @@ -238,7 +255,7 @@ export function registerSynthSounds() { registerSound( 'pulse', - (begin, value) => { + (begin, value, onended) => { const ac = getAudioContext(); let { pwrate, pwsweep } = value; if (pwsweep == null) { @@ -288,12 +305,18 @@ export function registerSynthSounds() { lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); lfo.connect(o.parameters.get('pulsewidth')); } - const cleanup = () => { - cleanupNodes([o, lfo, fm, vibratoOscillator]); - }; + const timeoutNode = webAudioTimeout( + ac, + () => { + cleanupNodes([node, o, lfo, fm, vibratoOscillator]); + onended(); + }, + begin, + end, + ); return { node, - cleanup, + stop: timeoutNode.stop, }; }, { prebake: true, type: 'synth' }, @@ -302,7 +325,7 @@ export function registerSynthSounds() { [...noises].forEach((s) => { registerSound( s, - (t, value) => { + (t, value, onended) => { const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], 'linear', @@ -314,7 +337,7 @@ export function registerSynthSounds() { let { density } = value; sound = getNoiseOscillator(s, t, density); - let { node: o } = sound; + let { node: o, stop } = sound; // turn down const g = gainNode(0.3); @@ -325,12 +348,15 @@ 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 cleanup = () => { + o.onended = () => { cleanupNodes([node, o, g]); + onended(); }; + const envEnd = holdEnd + release + 0.01; + stop(envEnd); return { node, - cleanup, + stop, }; }, { type: 'synth', prebake: true }, @@ -397,8 +423,14 @@ export function getOscillator(s, t, value) { if (noise) { noiseMix = getNoiseMix(o, noise, t); } - + const stop = (time) => { + fmModulator.stop(time); + vibratoOscillator?.stop(time); + noiseMix?.stop(time); + o.stop(time); + }; return { node: noiseMix?.node || o, + stop, }; } diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c91813aad..b6e73bd41 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -10,6 +10,7 @@ import { getPitchEnvelope, getVibratoOscillator, getWorklet, + webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -165,8 +166,8 @@ const _processTables = (json, baseUrl, frameLen, options = {}) => { export function registerWaveTable(key, tables, params) { registerSound( key, - (t, hapValue, cps) => { - return onTriggerSynth(t, hapValue, tables, cps, params?.frameLen ?? 2048); + (t, hapValue, onended, cps) => { + return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048); }, { type: 'wavetable', @@ -206,7 +207,7 @@ export const tables = async (url, frameLen, json, options = {}) => { }); }; -export async function onTriggerSynth(t, value, tables, cps, frameLen) { +export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const { s, n = 0, duration, clip } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); @@ -314,8 +315,15 @@ export async function onTriggerSynth(t, value, tables, cps, frameLen) { getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); const handle = { node, source }; - handle.cleanup = () => { - cleanupNodes([source, vibratoOscillator, fm, node, wtPosModulators, wtWarpModulators]); - }; + const timeoutNode = webAudioTimeout( + ac, + () => { + cleanupNodes([source, vibratoOscillator, fm, node, wtPosModulators, wtWarpModulators]); + onended(); + }, + t, + envEnd, + ); + handle.stop = timeoutNode.stop; return handle; } diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index c7043ca80..edb69b5da 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -73,9 +73,6 @@ export const getZZFX = (value, t) => { const source = getAudioContext().createBufferSource(); source.buffer = buffer; source.start(t); - const cleanup = () => { - cleanupNode(source); - }; return { node: source, }; @@ -85,8 +82,12 @@ export function registerZZFXSounds() { ['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => { registerSound( wave, - (t, value) => { + (t, value, onended) => { const { node: o } = getZZFX({ s: wave, ...value }, t); + o.onended = () => { + cleanupNode(o); + onended(); + }; return { node: o, stop: () => {},