Use nodes prior to GC

This commit is contained in:
Aria
2025-11-21 15:17:15 -06:00
parent 2518b2aed2
commit ffb618dc97
5 changed files with 124 additions and 81 deletions
+43
View File
@@ -36,6 +36,27 @@ export function getWorklet(ac, processor, params, config) {
return node;
}
// Pool of inactive (weakly referenced) voices to be reused if possible
const voicePools = {};
// Cycles through the pool attempting to find an inactive voice that hasn't been GC'd
export const claimVoice = (key) => {
voicePools[key] ??= [];
const pool = voicePools[key];
let node;
while (pool.length && !node) {
const ref = pool.pop();
node = ref?.deref?.();
}
return node;
};
// Releases a voice from use and adds to the inactive pool
export const releaseVoice = (key, node) => {
voicePools[key] ??= [];
voicePools[key].push(new WeakRef(node));
};
export const getParamADSR = (
param,
attack,
@@ -537,3 +558,25 @@ export const destroyAudioWorkletNode = (node) => {
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
};
export const setParams = (node, params, t) => {
const ac = getAudioContext();
t ??= ac.currentTime;
const workletParams = node && node.parameters && typeof node.parameters.get === 'function';
for (const [key, value] of Object.entries(params)) {
if (value == null) continue;
let param;
if (workletParams) {
param = node.parameters.get(key);
}
if (param == null && node[key] instanceof AudioParam) {
param = node[key];
}
if (param instanceof AudioParam) {
param.cancelScheduledValues(t);
param.setValueAtTime(value, t);
} else {
node[key] = value;
}
}
};
+9 -8
View File
@@ -478,15 +478,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const endWithRelease = end + release;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next();
const source = ch.value[1].deref();
const chainID = ch.value[0];
// oldest audio nodes will be stopped if maximum polyphony is exceeded
while (activeSoundSources.size >= maxPolyphony) {
const { value: entry } = activeSoundSources.entries().next();
if (!entry) break;
const [oldChainID, ref] = entry;
const handle = ref?.deref();
const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
source?.stop?.(endTime);
activeSoundSources.delete(chainID);
handle?.node?.gain?.linearRampToValueAtTime?.(0, endTime);
handle?.stop?.(endTime);
activeSoundSources.delete(oldChainID);
}
let audioNodes = [];
+28 -36
View File
@@ -3,6 +3,7 @@ import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
applyFM,
claimVoice,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
@@ -12,6 +13,8 @@ import {
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
setParams,
releaseVoice,
noises,
webAudioTimeout,
} from './helpers.mjs';
@@ -69,9 +72,7 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (endTime) => {
stop(endTime);
},
stop,
};
},
{ type: 'synth', prebake: true },
@@ -138,9 +139,7 @@ export function registerSynthSounds() {
return {
node,
stop: (endTime) => {
o.stop(endTime);
},
stop: o.stop,
};
},
{ type: 'synth', prebake: true },
@@ -164,22 +163,22 @@ export function registerSynthSounds() {
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
let o = claimVoice('supersaw');
const params = {
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
};
if (o == null) {
o = getWorklet(ac, 'supersaw-oscillator', params, {
outputChannelCount: [2],
},
);
});
} else {
setParams(o, params);
}
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
@@ -189,10 +188,11 @@ export function registerSynthSounds() {
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
releaseVoice('supersaw', o);
envGain.disconnect();
onended();
fm?.stop();
@@ -204,9 +204,7 @@ export function registerSynthSounds() {
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
stop: timeoutNode.stop,
};
},
{ prebake: true, type: 'synth' },
@@ -267,7 +265,7 @@ export function registerSynthSounds() {
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
@@ -280,9 +278,7 @@ export function registerSynthSounds() {
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
stop: timeoutNode.stop,
};
},
{ prebake: true, type: 'synth' },
@@ -341,7 +337,7 @@ export function registerSynthSounds() {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
let timeoutNode = webAudioTimeout(
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
@@ -357,9 +353,7 @@ export function registerSynthSounds() {
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
stop: timeoutNode.stop,
};
},
{ prebake: true, type: 'synth' },
@@ -402,9 +396,7 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (endTime) => {
stop(endTime);
},
stop,
};
},
{ type: 'synth', prebake: true },
+27 -22
View File
@@ -3,14 +3,17 @@ import { getCommonSampleInfo } from './util.mjs';
import {
applyFM,
applyParameterModulators,
destroyAudioWorkletNode,
claimVoice,
getADSRValues,
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
setParams,
releaseVoice,
webAudioTimeout,
destroyAudioWorkletNode,
} from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -218,32 +221,35 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen);
const voiceKey = `wavetable:${payload.key}`;
let holdEnd = t + duration;
if (clip !== undefined) {
holdEnd = Math.min(t + clip * duration, holdEnd);
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
let source = claimVoice(voiceKey);
const params = {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
};
if (source == null) {
source = getWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] });
} else {
setParams(source, params);
}
source.port.postMessage({ type: 'table', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
releaseVoice(voiceKey, source);
return;
}
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
@@ -318,19 +324,18 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
fm?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
destroyAudioWorkletNode(source);
releaseVoice(voiceKey, source);
onended();
},
t,
envEnd,
);
handle.stop = (time) => {
timeoutNode.stop(time);
};
handle.stop = timeoutNode.stop;
return handle;
}
+17 -15
View File
@@ -508,14 +508,15 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
const output = outputs[0];
const begin = params.begin[0];
const end = params.end[0];
if (currentTime <= begin || currentTime >= end) {
output[0].fill(0);
output[1].fill(0);
return true;
}
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
const output = outputs[0];
const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
@@ -1317,18 +1318,19 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return level;
}
_zeroOutputs(outL, outR) {
outL.fill(0);
outR.fill(0);
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true;
}
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
if (!this.tables) {
outL.fill(0);
if (outR !== outL) outR.set(outL);
const begin = parameters.begin[0];
const end = parameters.end[0];
if (!this.tables || currentTime <= begin || currentTime >= end) {
this._zeroOutputs();
return true;
}
const voices = parameters.voices[0]; // k-rate