Another major cleanup 90% of first pass

This commit is contained in:
Aria 2025-11-19 17:10:05 -06:00
parent 7ccdefe1c6
commit 442f487973
2 changed files with 100 additions and 161 deletions

View file

@ -376,8 +376,7 @@ export function resetGlobalEffects() {
controller?.reset(); controller?.reset();
analysers = {}; analysers = {};
analysersData = {}; analysersData = {};
lfos = {}; idToNodes = {};
nodes = {};
} }
function _getNodeParam(node, name) { function _getNodeParam(node, name) {
@ -409,66 +408,21 @@ function _getNodeParams(node) {
return Array.from(params); return Array.from(params);
} }
/** function _getTargetParams(nodes, target) {
* Split parameters (which might be arrays -- implying multiple-parameter modulation -- or a single number) into independent const targetNodes = nodes[target];
* objects which only account for single-parameter modulation
*
* @param {Object} params - Dictionary of modulation parameters.
* @returns {Object[]} - Array of parameter objects, one per parameter modulation
*/
function _splitParams(params) {
const num = ['num', 'target', 'param'] // names used to indicate individual parameter modulations
.map((k) => [params[k] ?? 0].flat().length)
.reduce((a, v) => Math.max(a, v), 1);
const individualParams = [];
for (let i = 0; i < num; i++) {
const paramsI = {};
for (const k in params) {
const flatV = [params[k]].flat();
if (flatV.length !== num && flatV.length !== 1) {
errorLogger(
new Error(
`Could not set up modulations. We derived ${num} items, but ${k}: ${JSON.stringify(flatV)} has length ${flatV.length} (needs 1 or ${num}).`,
),
'superdough',
);
return [];
}
paramsI[k] = flatV[i] ?? flatV[0];
}
individualParams.push(paramsI);
}
return individualParams;
}
/**
* Given a node name and the name of a parameter on that node, attempt to retrieve
* all nodes corresponding to the name and their associated parameters
*
* Note that we say nodes, plural, because some nodes have multiple sub-nodes, like a
* 24db filter which is two filters in series
*
* @param {string} targetName - Name of the node to modulate parameters on (e.g. `lpf`, `source`, etc.)
* @param {string} paramName - Name of the parameter to modulate on that node
* @returns {AudioParam[]} - Array of audio parameter objects for modulation
*
*/
function _getTargetParams(targetName, paramName) {
const targetNodes = nodes[targetName];
if (!targetNodes) { if (!targetNodes) {
const keys = Object.keys(nodes); const keys = Object.keys(nodes);
errorLogger( errorLogger(
new Error( new Error(
`Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(', ')}`, `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`,
), ),
'superdough', 'superdough',
); );
return []; return [];
} }
const audioParams = []; const audioParams = [];
targetNodes.forEach((targetNode) => { targetNodes.forEach((targetNode) => {
const paramName = guessParamName(blah);
const targetParam = _getNodeParam(targetNode, paramName); const targetParam = _getNodeParam(targetNode, paramName);
if (!targetParam) { if (!targetParam) {
const available = _getNodeParams(targetNode); const available = _getNodeParams(targetNode);
@ -485,64 +439,57 @@ function _getTargetParams(targetName, paramName) {
return audioParams; return audioParams;
} }
function _setWorkletParamsAtTime(audioParams, params, time) { function connectLFO(idx, params, nodeTracker) {
for (const [name, value] of params) {
if (value == null) continue;
const p = audioParams.get(name);
if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time);
else p.cancelScheduledValues(time);
p.setValueAtTime(value, time);
}
}
let lfos = {};
function _connectLFO(params, nodeTracker) {
const { const {
frequency = 1, rate = 1,
synced = 0, sync,
cps = 0.5, cps,
num = 1, // default to LFO 1
target, target,
param,
begin,
...filteredParams ...filteredParams
} = params; } = params;
filteredParams['frequency'] = synced ? frequency / cps : frequency; filteredParams['frequency'] = sync !== undefined ? sync / cps : rate;
let lfoNode = lfos[num]; const ac = getAudioContext();
if (lfoNode == null) { lfoNode = getLfo(ac, filteredParams);
const ac = getAudioContext(); nodeTracker[`lfo${idx}`] = [lfoNode];
lfoNode = getLfo(ac, filteredParams); _getTargetParams(target).forEach(lfoNode.connect);
lfos[num] = lfoNode;
nodeTracker[`lfo${num}`] = [lfoNode];
}
const targets = _getTargetParams(target, param);
targets.forEach((target) => lfoNode.connect(target));
_setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin);
return lfoNode;
} }
function _connectEnvelope(params) { function connectEnvelope(idx, params, nodeTracker) {
const { target, param, envDepth, begin, end, attack, decay, sustain, release, curve, ...filteredParams } = params; const { target, ...filteredParams } = params;
const targets = _getTargetParams(target, param); const ac = getAudioContext();
const [att, dec, sus, rel] = getADSRValues([attack, decay, sustain, release], curve, [0.005, 0.14, 0, 0.1]); envNode = getEnvelope(ac, filteredParams);
targets.forEach((targetParam) => { nodeTracker[`env${idx}`] = [envNode];
const currentValue = targetParam.value; _getTargetParams(nodeTracker, target).forEach(envNode.connect);
const min = currentValue;
const max = currentValue + envDepth;
getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve);
});
} }
function connectModulators(params, modulatorType, nodeTracker) { function connectSendModulator(params, signal, nodeTracker, pendingConnections) {
// We break down params specifying multiple modulators into a set of parameters for const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
// a single one dc.start(t);
const individualParams = _splitParams(params); const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 });
if (modulatorType === 'lfo') { offset.start(t);
individualParams.map((p) => _connectLFO(p, nodeTracker)); const raw = dc.connect(gainNode(1));
} else if (modulatorType === 'envelope') { const modulator = post
individualParams.forEach(_connectEnvelope); .connect(raw)
} .connect(gainNode((params.depth ?? 1) / 0.3))
return []; .connect(gainNode(1));
offset.connect(modulator);
webAudioTimeout(
ac,
() => {
_getTargetParams(nodeTracker, params.target).forEach(signal.connect);;
},
0,
params.begin,
);
webAudioTimeout(
ac,
() => {
signal.disconnect();
delete pendingConnections[params.id][chainID];
},
0,
params.end + 0.05,
);
} }
let activeSoundSources = new Map(); let activeSoundSources = new Map();
@ -552,9 +499,10 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
} }
let nodes = {}; let idToNodes = {};
let pendingConnections = {};
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
const nodes = {};
// new: t is always expected to be the absolute target onset time // new: t is always expected to be the absolute target onset time
const ac = getAudioContext(); const ac = getAudioContext();
const audioController = getSuperdoughAudioController(); const audioController = getSuperdoughAudioController();
@ -644,24 +592,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee, compressorKnee,
compressorAttack, compressorAttack,
compressorRelease, compressorRelease,
lfoNum,
lfoTarget,
lfoParam,
lfoRate,
lfoDepth,
lfoDCOffset,
lfoShape,
lfoSkew,
lfoCurve,
lfoSynced,
envTarget,
envParam,
envAttack,
envDecay,
envSustain,
envRelease,
envCurve,
envDepth,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@ -991,45 +921,56 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
audioNodes = audioNodes.concat(chain); audioNodes = audioNodes.concat(chain);
// finally, now that `nodes` is populated, set up LFOs and envelopes // finally, now that `nodes` is populated, set up modulators
if (lfoTarget !== undefined && lfoParam !== undefined) { if (value.lfo) {
connectModulators( for (const [params, idx] of Object.entries(value.lfo)) {
{ connectLFO(
num: lfoNum ?? 1, idx,
target: lfoTarget, {
param: lfoParam, ...params,
frequency: lfoRate, cps,
depth: lfoDepth, begin: t,
dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 end: endWithRelease,
shape: lfoShape, },
skew: lfoSkew, 'lfo',
curve: lfoCurve, nodes,
persistent: 1, );
begin: t, }
synced: lfoSynced,
cps: cps,
},
'lfo',
nodes,
);
} }
if (envTarget !== undefined && envParam !== undefined) { if (value.env) {
connectModulators( for (const [params, idx] of Object.entries(value.env)) {
{ connectEnvelope(
target: envTarget, idx,
param: envParam, {
envDepth, ...params,
attack: envAttack, begin: t,
decay: envDecay, end: endWithRelease,
sustain: envSustain, },
release: envRelease, 'envelope',
curve: envCurve, nodes,
begin: t, );
end: endWithRelease, }
},
'envelope',
);
} }
if (value.id) {
idToNodes[value.id] = new WeakRef(nodes);
}
if (value.send) {
for (const p of value.env) {
const modNodes = idToNodes[p.id];
if (!modNodes) {
logger(
`[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`,
);
} else {
connectSendModulator(params, post);
pendingConnections[p.id] ??= {};
pendingConnections[p.id][chainID] = [modulator, p.target, p.param, endWithRelease];
}
});
}
if (applySends) {
}; };
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {

View file

@ -123,7 +123,6 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'dcoffset', defaultValue: 0 }, { name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 }, { name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1 }, { name: 'max', defaultValue: 1 },
{ name: 'persistent', defaultValue: 0, min: 0, max: 1 }, // whether to ignore end
]; ];
} }
@ -142,8 +141,7 @@ class LFOProcessor extends AudioWorkletProcessor {
process(_inputs, outputs, parameters) { process(_inputs, outputs, parameters) {
const begin = parameters['begin'][0]; const begin = parameters['begin'][0];
const end = parameters['end'][0]; const end = parameters['end'][0];
const persistent = parameters['persistent'][0]; if (currentTime >= end) {
if (persistent < 0.5 && currentTime >= end) {
return false; return false;
} }
if (currentTime <= begin) { if (currentTime <= begin) {