Merge pull request '[perf] release unused AudioBufferSourceNode + releaseAudioNode' (#1805) from jeromew/strudel:fix-perf11 into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1805
Reviewed-by: Aria <glossing@noreply.codeberg.org>
This commit is contained in:
Aria 2025-12-03 17:35:03 +01:00
commit eb18648a2a
7 changed files with 91 additions and 48 deletions

View file

@ -6,6 +6,7 @@ import {
getADSRValues, getADSRValues,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
onceEnded,
} from '@strudel/webaudio'; } from '@strudel/webaudio';
import gm from './gm.mjs'; import gm from './gm.mjs';
@ -170,12 +171,12 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (releaseTime) => {}; const stop = (releaseTime) => {};
bufferSource.onended = () => { onceEnded(bufferSource, () => {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
onended(); onended();
}; });
return { node, stop }; return { node, stop };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },

View file

@ -349,20 +349,11 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.connect(zeroGain); constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime` // Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => { onceEnded(constantNode, () => {
// Ensure garbage collection releaseAudioNode(zeroGain);
try { releaseAudioNode(constantNode);
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete(); onComplete();
}; });
constantNode.start(startTime); constantNode.start(startTime);
constantNode.stop(stopTime); constantNode.stop(stopTime);
return constantNode; return constantNode;
@ -554,10 +545,54 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
return Number(freq); return Number(freq);
}; };
export const destroyAudioWorkletNode = (node) => { // This helper should be used instead of the `node.onended = callback` pattern
if (node == null) { // It adds a mechanism to help minimize gc retention
return; export const onceEnded = (node, callback) => {
} let onended = callback;
node.disconnect(); node.onended = function cleanup() {
node.parameters.get('end')?.setValueAtTime(0, 0); onended && onended();
onended = null;
this.onended = null;
};
};
export const releaseAudioNode = (node) => {
if (node == null) return;
// 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();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
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);
}
}; };

View file

@ -1,7 +1,14 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import {
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> const bufferCache = {}; // string: Promise<ArrayBuffer>
@ -286,17 +293,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, 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) { if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error'); logger(`[sampler] could not load "${s}:${n}"`, 'error');
return; 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 // vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
@ -319,13 +328,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
node.connect(out); node.connect(out);
bufferSource.onended = function () { onceEnded(bufferSource, function () {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
out.disconnect(); out.disconnect();
onended(); onended();
}; });
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (endTime) => { const stop = (endTime) => {

View file

@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { import {
applyFM, applyFM,
destroyAudioWorkletNode,
gainNode, gainNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
@ -13,6 +12,7 @@ import {
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
noises, noises,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@ -192,8 +192,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
@ -270,8 +269,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
}, },
begin, begin,
@ -344,9 +342,8 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
destroyAudioWorkletNode(lfo); releaseAudioNode(lfo);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();

View file

@ -3,13 +3,13 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { import {
applyFM, applyFM,
applyParameterModulators, applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
getParamADSR, getParamADSR,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@ -319,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout( const timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(source); releaseAudioNode(source);
vibratoOscillator?.stop(); vibratoOscillator?.stop();
fm?.stop(); fm?.stop();
node.disconnect();
wtPosModulators?.disconnect(); wtPosModulators?.disconnect();
wtWarpModulators?.disconnect(); wtWarpModulators?.disconnect();
onended(); onended();

View file

@ -508,13 +508,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
]; ];
} }
process(_input, outputs, params) { process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) { if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' }); // should terminate
return false; return false;
} }
if (currentTime <= params.begin[0]) {
// keep alive
return true;
}
const output = outputs[0]; const output = outputs[0];
const voices = params.voices[0]; // k-rate const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {

View file

@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound } from './superdough.mjs'; import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { buildSamples } from './zzfx_fork.mjs'; import { buildSamples } from './zzfx_fork.mjs';
import { onceEnded, releaseAudioNode } from './helpers.mjs';
export const getZZFX = (value, t) => { export const getZZFX = (value, t) => {
let { let {
@ -83,10 +84,10 @@ export function registerZZFXSounds() {
wave, wave,
(t, value, onended) => { (t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t); const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => { onceEnded(o, () => {
o.disconnect(); releaseAudioNode(o);
onended(); onended();
}; });
return { return {
node: o, node: o,
stop: () => {}, stop: () => {},