From 68b1cb87ca32ebcc9080a25abe5cf2019ffad24d Mon Sep 17 00:00:00 2001 From: jeromew Date: Mon, 1 Dec 2025 15:29:19 +0000 Subject: [PATCH] [perf] release unused AudioBufferSourceNode + releaseAudioNode --- packages/superdough/helpers.mjs | 31 +++++++++++++++++++++++++++---- packages/superdough/sampler.mjs | 16 +++++++++------- packages/superdough/synth.mjs | 13 +++++-------- packages/superdough/wavetable.mjs | 5 ++--- packages/superdough/worklets.mjs | 9 +++++---- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index ed2c0c44..3e9b8113 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -554,10 +554,33 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { return Number(freq); }; -export const destroyAudioWorkletNode = (node) => { - if (node == null) { - return; +export const releaseAudioNode = (node) => { + // check we received an AudioNode + if (!(node instanceof AudioNode)) { + throw new Error('releaseAudioNode can only release an AudioNode'); } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); + + // make sure all AudioScheduledSourceNode is in a stopped state + // https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode + if (node instanceof AudioScheduledSourceNode) { + try { + node.stop(); + } catch (e) { + if (e instanceof DOMException && e.name === 'InvalidStateError') { + node.start(node.context.currentTime + 5); // will never happen + node.stop(); + } + } + } + + // https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing + // An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]] + // 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) { + node.parameters.get('end')?.setValueAtTime(0, 0); + } }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index aaa1a8e4..e066a260 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,7 +1,7 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; -import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; +import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator, releaseAudioNode } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -286,17 +286,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); - // asny stuff above took too long? - if (ac.currentTime > t) { - logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); - // console.warn('sample still loading:', s, n); - return; - } if (!bufferSource) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } + // async stuff above took too long? + if (ac.currentTime > t) { + logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight'); + // AudioBufferSourceNode will never be used. discard it + releaseAudioNode(bufferSource); + return; + } + // vibrato let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3b27bf6c..d50aebae 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, - destroyAudioWorkletNode, gainNode, getADSRValues, getFrequencyFromValue, @@ -13,6 +12,7 @@ import { getVibratoOscillator, getWorklet, noises, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -192,8 +192,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -270,8 +269,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); }, begin, @@ -344,9 +342,8 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - destroyAudioWorkletNode(lfo); - envGain.disconnect(); + releaseAudioNode(o); + lfo && releaseAudioNode(lfo); onended(); fm?.stop(); vibratoOscillator?.stop(); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 3659edcd..e9d81041 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -3,13 +3,13 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { applyFM, applyParameterModulators, - destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, getParamADSR, getPitchEnvelope, getVibratoOscillator, getWorklet, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -319,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(source); + releaseAudioNode(source); vibratoOscillator?.stop(); fm?.stop(); - node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); onended(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index bf633a63..2f75453b 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -508,13 +508,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(_input, outputs, params) { - if (currentTime <= params.begin[0]) { - return true; - } if (currentTime >= params.end[0]) { - // this.port.postMessage({ type: 'onended' }); + // should terminate return false; } + if (currentTime <= params.begin[0]) { + // keep alive + return true; + } const output = outputs[0]; const voices = params.voices[0]; // k-rate for (let i = 0; i < output[0].length; i++) {