Working version with cleanup and such

This commit is contained in:
Aria
2025-09-22 21:56:47 -05:00
parent 99e4855545
commit bb0ea3b4ad
6 changed files with 112 additions and 140 deletions
+2 -10
View File
@@ -220,16 +220,8 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
// Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
constantNode.disconnect();
zeroGain.disconnect();
onComplete();
};
constantNode.start(startTime);
+3 -10
View File
@@ -340,19 +340,12 @@ 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);
bufferSource.onended = function () {
const cleanup = () => {
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
out.disconnect();
onended();
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime) => {
bufferSource.stop(endTime);
};
const handle = { node: out, bufferSource, stop };
}
const handle = { node: out, cleanup };
// cut groups
if (cut !== undefined) {
+51 -53
View File
@@ -390,19 +390,21 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 2; //num of filters in series
// filters
const numStages = 8; // num of filters in series
let fOffset = 0;
const filterChain = [];
const fMin = centerFrequency * 0.5, fMax = centerFrequency;
const ratio = Math.pow(fMax / fMin, 1 / numStages);
for (let i = 0; i < numStages; i++) {
const filter = ac.createBiquadFilter();
filter.type = 'notch';
filter.type = 'allpass';
filter.gain.value = 1;
filter.frequency.value = centerFrequency + fOffset;
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
filter.frequency.value = fMin * Math.pow(ratio, i + 0.5);
filter.Q.value = 2 - clamp(depth * 2, 0, 1.9);
lfoGain.connect(filter.detune);
fOffset += 282;
fOffset += 4;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
@@ -560,26 +562,6 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
let counter = 0;
const livingNodes = {};
function disconnectNodeWhenQuiet(ac, node, dependentNodes) {
const meter = new AudioWorkletNode(ac, 'meter-processor');
const thisCounter = counter;
livingNodes[thisCounter] = dependentNodes;
counter += 1;
node.connect(meter);
let done = () => {
meter.disconnect();
meter.port.onmessage = null;
node.disconnect();
dependentNodes.forEach((n) => n?.disconnect());
delete livingNodes[thisCounter];
};
meter.port.onmessage = (e) => {
if (e.data?.quiet) done();
};
}
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
@@ -673,8 +655,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
let audioNodes = [];
let earlyNodes = [];
let finalDryNode;
if (['-', '~', '_'].includes(s)) {
return;
@@ -685,7 +665,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// get source AudioNode
let sourceNode;
let sourceNode, cleanup;
if (source) {
sourceNode = source(t, value, hapDuration, cps);
} else if (getSound(s)) {
@@ -694,6 +674,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (soundHandle) {
sourceNode = soundHandle.node;
cleanup = soundHandle.cleanup;
activeSoundSources.set(chainID, soundHandle);
}
} else {
@@ -710,7 +691,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(sourceNode);
chain.push({ input: sourceNode, output: sourceNode, cleanup });
FX = FX ? FX : [value]; // default to using the top-level values if FX is not specified
for (const fx of FX) {
let {
@@ -869,15 +850,23 @@ 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);
chain.push(phaserFX);
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 });
}
if (fx.workletSrc !== undefined) {
const workletNode = getWorklet(ac, 'generic-processor', {});
earlyNodes = [...chain];
chain.push(workletNode);
let workletSrc = fx.workletSrc;
workletSrc = workletSrc
const workletSrc = fx.workletSrc
.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i])
.replaceAll('sFreq', getFrequencyFromValue(value))
.replaceAll('sGate', `cc('strudel-gate-${chainID}')`);
@@ -885,9 +874,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const { src, ugens, registers } = compileKabel(workletSrc);
workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, end });
}
let dry = chain[chain.length - 1];
// delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const dry = gainNode(1);
delayfeedback = clamp(delayfeedback, 0, 0.98);
const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
const wetDelay = gainNode(delay);
@@ -895,11 +884,14 @@ 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);
earlyNodes = [...chain];
chain.push(sum);
audioNodes.push(dryDelay, delayNode, wetDelay);
const cleanup = () => {
dryDelay.disconnect();
delayNode.disconnect();
dry.disconnect();
wetDelay.disconnect();
}
chain.push({ input: dry, output: sum, cleanup });
}
dry = chain[chain.length - 1];
// reverb
if (fx.room > 0) {
let roomIR;
@@ -913,6 +905,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, fx.ir, 0);
}
const dry = gainNode(1);
const reverbNode = ac.createReverb(
fx.roomsize,
fx.roomfade,
@@ -927,13 +920,16 @@ 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);
earlyNodes = [...chain];
chain.push(sum);
audioNodes.push(dryReverb, reverbNode, wetReverb);
const cleanup = () => {
dryReverb.disconnect();
reverbNode.disconnect();
dry.disconnect();
wetReverb.disconnect();
}
chain.push({ input: dry, output: sum, cleanup });
}
}
const useFXRelease = FXrelease !== undefined && FXrelease > release;
if (useFXRelease) {
if (FXrelease !== undefined && FXrelease > release) {
const releaseNode = gainNode(1);
releaseNode.gain.setValueAtTime(1, end + release);
releaseNode.gain.linearRampToValueAtTime(0, endWithRelease);
@@ -982,24 +978,26 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
finalDryNode = dryGain;
} else {
connectToOrbit(post, orbit);
finalDryNode = post;
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
chain.slice(1).reduce((last, current) => {
const lastC = last.output ?? last;
const currentC = current.input ?? current;
lastC.connect(currentC);
return current.output ?? current;
}, chain[0]);
audioNodes.push(...chain);
// earlyNodes = [];
earlyNodes = earlyNodes.length > 0 && !useFXRelease ? earlyNodes : audioNodes;
audioNodes = audioNodes.filter((n) => !earlyNodes.includes(n));
webAudioTimeout(
ac,
() => {
// audioNodes.forEach((n) => n?.disconnect());
// earlyNodes.forEach((n) => n?.disconnect());
disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes);
audioNodes.forEach((n) => {
const node = n?.output ?? n;
node?.disconnect();
n?.cleanup?.();
});
activeSoundSources.delete(chainID);
},
0,
+55 -13
View File
@@ -13,6 +13,14 @@ import {
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
@@ -51,6 +59,11 @@ export function registerSynthSounds() {
const { duration } = value;
const cleanup = () => {
o.disconnect();
g.disconnect();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
@@ -58,6 +71,7 @@ export function registerSynthSounds() {
const envEnd = holdEnd + release + 0.01;
return {
node,
cleanup,
};
},
{ type: 'synth', prebake: true },
@@ -96,6 +110,14 @@ export function registerSynthSounds() {
const mix = gainNode(mixGain);
const cleanup = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
@@ -111,6 +133,7 @@ export function registerSynthSounds() {
return {
node,
cleanup,
};
},
{ type: 'synth', prebake: true },
@@ -154,13 +177,18 @@ export function registerSynthSounds() {
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
const node = o.connect(gainNode(1));
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
const cleanup = () => {
destroyAudioWorkletNode(o);
fm?.stop();
vibratoOscillator?.stop();
}
return {
node: envGain,
node,
cleanup,
};
},
{ prebake: true, type: 'synth' },
@@ -216,13 +244,17 @@ export function registerSynthSounds() {
o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency });
let envGain = gainNode(1);
envGain = o.connect(envGain);
const node = o.connect(gainNode(1));
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
const cleanup = () => {
destroyAudioWorkletNode(o);
}
return {
node: envGain,
node,
cleanup,
};
},
{ prebake: true, type: 'synth' },
@@ -272,18 +304,24 @@ export function registerSynthSounds() {
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
const node = o.connect(gainNode(1));
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let lfo;
if (pwsweep != 0) {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
const cleanup = () => {
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
fm?.stop();
vibratoOscillator?.stop();
}
return {
node: envGain,
node,
cleanup,
};
},
{ prebake: true, type: 'synth' },
@@ -315,9 +353,13 @@ 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 envEnd = holdEnd + release + 0.01;
const cleanup = () => {
g.disconnect();
o.disconnect();
}
return {
node,
cleanup,
};
},
{ type: 'synth', prebake: true },
-49
View File
@@ -910,54 +910,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
registerProcessor('byte-beat-processor', ByteBeatProcessor);
class MeterProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.threshold = 1e-2;
this.holdMs = 40;
this.belowSince = null;
this.initialized = currentTime * 1000;
this.finished = false;
}
process(inputs) {
if (this.finished) return true;
const now = currentTime * 1000;
const chs = inputs[0] || [];
const N = chs[0]?.length || 0;
if (!N) {
if (now - this.initialized >= this.holdMs) {
this.port.postMessage({ quiet: true });
this.finished = true;
}
return true;
}
let sum = 0,
count = 0;
for (let c = 0; c < chs.length; c++) {
const x = chs[c];
for (let i = 0; i < N; i++) {
const v = x[i];
sum += v * v;
}
count += N;
}
const rms = Math.sqrt(sum / Math.max(1, count));
if (rms < this.threshold) {
if (this.belowSince == null) this.belowSince = now;
if (now - this.belowSince >= this.holdMs) {
this.port.postMessage({ quiet: true });
this.finished = true;
}
} else {
this.belowSince = null;
}
return true;
}
}
registerProcessor('meter-processor', MeterProcessor);
class GenericProcessor extends AudioWorkletProcessor {
constructor() {
super();
@@ -971,7 +923,6 @@ class GenericProcessor extends AudioWorkletProcessor {
let {
src,
schema: { ugens, registers },
gate,
start,
end,
} = event.data;
+1 -5
View File
@@ -80,12 +80,8 @@ export function registerZZFXSounds() {
['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
(t, value) => {
const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => {
o.disconnect();
onended();
};
return {
node: o,
stop: () => {},