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
+99 -158
View File
@@ -376,8 +376,7 @@ export function resetGlobalEffects() {
controller?.reset();
analysers = {};
analysersData = {};
lfos = {};
nodes = {};
idToNodes = {};
}
function _getNodeParam(node, name) {
@@ -409,66 +408,21 @@ function _getNodeParams(node) {
return Array.from(params);
}
/**
* Split parameters (which might be arrays -- implying multiple-parameter modulation -- or a single number) into independent
* 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];
function _getTargetParams(nodes, target) {
const targetNodes = nodes[target];
if (!targetNodes) {
const keys = Object.keys(nodes);
errorLogger(
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',
);
return [];
}
const audioParams = [];
targetNodes.forEach((targetNode) => {
const paramName = guessParamName(blah);
const targetParam = _getNodeParam(targetNode, paramName);
if (!targetParam) {
const available = _getNodeParams(targetNode);
@@ -485,64 +439,57 @@ function _getTargetParams(targetName, paramName) {
return audioParams;
}
function _setWorkletParamsAtTime(audioParams, params, time) {
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) {
function connectLFO(idx, params, nodeTracker) {
const {
frequency = 1,
synced = 0,
cps = 0.5,
num = 1, // default to LFO 1
rate = 1,
sync,
cps,
target,
param,
begin,
...filteredParams
} = params;
filteredParams['frequency'] = synced ? frequency / cps : frequency;
let lfoNode = lfos[num];
if (lfoNode == null) {
const ac = getAudioContext();
lfoNode = getLfo(ac, filteredParams);
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;
filteredParams['frequency'] = sync !== undefined ? sync / cps : rate;
const ac = getAudioContext();
lfoNode = getLfo(ac, filteredParams);
nodeTracker[`lfo${idx}`] = [lfoNode];
_getTargetParams(target).forEach(lfoNode.connect);
}
function _connectEnvelope(params) {
const { target, param, envDepth, begin, end, attack, decay, sustain, release, curve, ...filteredParams } = params;
const targets = _getTargetParams(target, param);
const [att, dec, sus, rel] = getADSRValues([attack, decay, sustain, release], curve, [0.005, 0.14, 0, 0.1]);
targets.forEach((targetParam) => {
const currentValue = targetParam.value;
const min = currentValue;
const max = currentValue + envDepth;
getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve);
});
function connectEnvelope(idx, params, nodeTracker) {
const { target, ...filteredParams } = params;
const ac = getAudioContext();
envNode = getEnvelope(ac, filteredParams);
nodeTracker[`env${idx}`] = [envNode];
_getTargetParams(nodeTracker, target).forEach(envNode.connect);
}
function connectModulators(params, modulatorType, nodeTracker) {
// We break down params specifying multiple modulators into a set of parameters for
// a single one
const individualParams = _splitParams(params);
if (modulatorType === 'lfo') {
individualParams.map((p) => _connectLFO(p, nodeTracker));
} else if (modulatorType === 'envelope') {
individualParams.forEach(_connectEnvelope);
}
return [];
function connectSendModulator(params, signal, nodeTracker, pendingConnections) {
const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
dc.start(t);
const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 });
offset.start(t);
const raw = dc.connect(gainNode(1));
const modulator = post
.connect(raw)
.connect(gainNode((params.depth ?? 1) / 0.3))
.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();
@@ -552,9 +499,10 @@ function mapChannelNumbers(channels) {
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) => {
const nodes = {};
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
@@ -644,24 +592,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee,
compressorAttack,
compressorRelease,
lfoNum,
lfoTarget,
lfoParam,
lfoRate,
lfoDepth,
lfoDCOffset,
lfoShape,
lfoSkew,
lfoCurve,
lfoSynced,
envTarget,
envParam,
envAttack,
envDecay,
envSustain,
envRelease,
envCurve,
envDepth,
} = value;
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]);
audioNodes = audioNodes.concat(chain);
// finally, now that `nodes` is populated, set up LFOs and envelopes
if (lfoTarget !== undefined && lfoParam !== undefined) {
connectModulators(
{
num: lfoNum ?? 1,
target: lfoTarget,
param: lfoParam,
frequency: lfoRate,
depth: lfoDepth,
dcoffset: lfoDCOffset ?? 0, // override default value of 0.5
shape: lfoShape,
skew: lfoSkew,
curve: lfoCurve,
persistent: 1,
begin: t,
synced: lfoSynced,
cps: cps,
},
'lfo',
nodes,
);
// finally, now that `nodes` is populated, set up modulators
if (value.lfo) {
for (const [params, idx] of Object.entries(value.lfo)) {
connectLFO(
idx,
{
...params,
cps,
begin: t,
end: endWithRelease,
},
'lfo',
nodes,
);
}
}
if (envTarget !== undefined && envParam !== undefined) {
connectModulators(
{
target: envTarget,
param: envParam,
envDepth,
attack: envAttack,
decay: envDecay,
sustain: envSustain,
release: envRelease,
curve: envCurve,
begin: t,
end: endWithRelease,
},
'envelope',
);
if (value.env) {
for (const [params, idx] of Object.entries(value.env)) {
connectEnvelope(
idx,
{
...params,
begin: t,
end: endWithRelease,
},
'envelope',
nodes,
);
}
}
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) => {
+1 -3
View File
@@ -123,7 +123,6 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 },
{ 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) {
const begin = parameters['begin'][0];
const end = parameters['end'][0];
const persistent = parameters['persistent'][0];
if (persistent < 0.5 && currentTime >= end) {
if (currentTime >= end) {
return false;
}
if (currentTime <= begin) {