Big refactor; start envelope at current value

This commit is contained in:
Aria 2025-08-20 13:22:37 -05:00
parent e25eab11ad
commit d8f0d70908
2 changed files with 130 additions and 109 deletions

View file

@ -1439,8 +1439,6 @@ export const { lfoSkew } = registerControl('lfoSkew');
export const { lfoCurve } = registerControl('lfoCurve'); export const { lfoCurve } = registerControl('lfoCurve');
export const { lfoSynced } = registerControl('lfoSynced'); export const { lfoSynced } = registerControl('lfoSynced');
export const { envNum } = registerControl('envNum');
export const { envTarget } = registerControl('envTarget'); export const { envTarget } = registerControl('envTarget');
export const { envParam } = registerControl('envParam'); export const { envParam } = registerControl('envParam');
export const { envAttack } = registerControl('envAttack'); export const { envAttack } = registerControl('envAttack');

View file

@ -532,15 +532,100 @@ function _getNodeParams(node) {
return Array.from(params); 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, countKeys) {
const num = ["num", "target", "parameter"] // 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) {
const keys = Object.keys(nodes);
errorLogger(
new Error(`Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(", ")}`),
'superdough'
);
return [];
}
const audioParams = [];
targetNodes.forEach((targetNode) => {
const targetParam = _getNodeParam(targetNode, paramName);
if (!targetParam) {
const available = _getNodeParams(targetNode);
errorLogger(
new Error(
`Could not connect to parameter '${paramName}' on '${targetName}'. Available parameters: ${available.join(", ")}`
),
'superdough'
);
return;
}
audioParams.push(targetParam);
});
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 = {}; let lfos = {};
function _connectLFO(params) { function _connectLFO(params) {
const { const {
frequency = 1, frequency = 1,
synced = 0, synced = 0,
cps = 0.5, cps = 0.5,
lfoNum = 1, // default to LFO 1 num = 1, // default to LFO 1
lfoTarget, target,
lfoParam, param,
begin,
...filteredParams ...filteredParams
} = params; } = params;
filteredParams['frequency'] = synced ? frequency / cps : frequency; filteredParams['frequency'] = synced ? frequency / cps : frequency;
@ -548,61 +633,22 @@ function _connectLFO(params) {
if (lfoNode == null) { if (lfoNode == null) {
const ac = getAudioContext(); const ac = getAudioContext();
lfoNode = getLfo(ac, filteredParams); lfoNode = getLfo(ac, filteredParams);
lfos[lfoNum] = lfoNode; lfos[num] = lfoNode;
} }
lfoNode.disconnect(); try {
const targetNodes = nodes[lfoTarget]; lfoNode.disconnect();
if (targetNodes === undefined) { } catch {
const keys = Object.keys(nodes); // pass
errorLogger(new Error(`Could not connect to target ${lfoTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough');
return;
}
targetNodes.forEach((targetNode) => {
const targetParam = _getNodeParam(targetNode, lfoParam);
if (targetParam === undefined) {
const parameters = _getNodeParams(targetNode);
errorLogger(new Error(`Could not connect to parameter ${lfoParam} on node ${lfoTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough');
}
lfoNode.connect(targetParam);
});
const time = filteredParams.begin;
for (const [name, value] of Object.entries(filteredParams)) {
if (value == null) continue;
const p = lfoNode.parameters?.get(name);
if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time);
else p.cancelScheduledValues(time);
p.setValueAtTime(value, time);
}
}
function connectLFOs(params) {
// We break down params specifying multiple LFOs into a set of parameters for
// a single LFO
const numLFOs = [
[params.lfoNum].flat().length,
[params.lfoTarget].flat().length,
[params.lfoParam].flat().length,
].reduce((a, v) => Math.max(a, v)); // Number of LFOs is the max as implied by these values
for (let i = 0; i < numLFOs; i++) {
let singleParams = {};
for (const k in params) {
const v = params[k];
const flatV = [v].flat();
if (flatV.length !== numLFOs && flatV.length !== 1) {
errorLogger(new Error(`Could not setup LFOs. We derived ${numLFOs} as the intended number of LFOs, but ${k}: ${flatV} does not have matching length nor length 1`));
return;
}
singleParams[k] = flatV[i] ?? flatV[0];
}
_connectLFO(singleParams);
} }
const targets = _getTargetParams(target, param);
targets.forEach((target) => lfoNode.connect(target));
_setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin);
} }
function _connectEnvelope(params) { function _connectEnvelope(params) {
const { const {
envNum = 1, // default to envelope 1 target,
envTarget, param,
envParam,
envDepth, envDepth,
begin, begin,
end, end,
@ -613,54 +659,33 @@ function _connectEnvelope(params) {
curve, curve,
...filteredParams ...filteredParams
} = params; } = params;
const targetNodes = nodes[envTarget]; const targets = _getTargetParams(target, param);
if (targetNodes === undefined) { const [att, dec, sus, rel] = getADSRValues(
const keys = Object.keys(nodes); [
errorLogger(new Error(`Could not connect to target ${envTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); attack,
return; decay,
} sustain,
targetNodes.forEach((targetNode) => { release,
const targetParam = _getNodeParam(targetNode, envParam); ],
if (targetParam === undefined) { curve,
const parameters = _getNodeParams(targetNode); [0.005, 0.14, 0, 0.1]
errorLogger(new Error(`Could not connect to parameter ${envParam} on node ${envTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough'); );
} targets.forEach((targetParam) => {
const [att, dec, sus, rel] = getADSRValues( const currentValue = targetParam.value;
[ const min = currentValue;
attack, const max = currentValue + envDepth;
decay,
sustain,
release,
],
curve,
[0.005, 0.14, 0, 0.1]
);
const min = 0;
const max = envDepth;
getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve); getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve);
}); });
} }
function connectEnvelopes(params) { function connectModulators(params, modulatorType) {
// We break down params specifying multiple envelopes into a set of parameters for // We break down params specifying multiple modulators into a set of parameters for
// a single envelopes // a single one
const numEnvelopes = [ const individualParams = _splitParams(params);
[params.envNum].flat().length, if (modulatorType === "lfo") {
[params.envTarget].flat().length, individualParams.forEach(_connectLFO);
[params.envParam].flat().length, } else if (modulatorType === "envelope") {
].reduce((a, v) => Math.max(a, v)); // Number of envelopes is the max as implied by these values individualParams.forEach(_connectEnvelope);
for (let i = 0; i < numEnvelopes; i++) {
let singleParams = {};
for (const k in params) {
const v = params[k];
const flatV = [v].flat();
if (flatV.length !== numEnvelopes && flatV.length !== 1) {
errorLogger(new Error(`Could not setup envelopes. We derived ${numEnvelopes} as the intended number of envelopes, but ${k}: ${flatV} does not have matching length nor length 1`));
return;
}
singleParams[k] = flatV[i] ?? flatV[0];
}
_connectEnvelope(singleParams);
} }
} }
@ -792,7 +817,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
lfoSkew, lfoSkew,
lfoCurve, lfoCurve,
lfoSynced, lfoSynced,
envNum,
envTarget, envTarget,
envParam, envParam,
envAttack, envAttack,
@ -1090,10 +1114,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// finally, now that `nodes` is populated, set up LFOs and envelopes // finally, now that `nodes` is populated, set up LFOs and envelopes
if (lfoTarget !== undefined && lfoParam !== undefined) { if (lfoTarget !== undefined && lfoParam !== undefined) {
connectLFOs({ connectModulators({
lfoNum, num: lfoNum,
lfoTarget, target: lfoTarget,
lfoParam, param: lfoParam,
frequency: lfoRate, frequency: lfoRate,
depth: lfoDepth, depth: lfoDepth,
dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 dcoffset: lfoDCOffset ?? 0, // override default value of 0.5
@ -1104,13 +1128,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
begin: t, begin: t,
synced: lfoSynced, synced: lfoSynced,
cps: cps, cps: cps,
}); }, "lfo");
} }
if (envTarget !== undefined && envParam !== undefined) { if (envTarget !== undefined && envParam !== undefined) {
connectEnvelopes({ connectModulators({
envNum, target: envTarget,
envTarget, param: envParam,
envParam,
envDepth, envDepth,
attack: envAttack, attack: envAttack,
decay: envDecay, decay: envDecay,
@ -1119,7 +1142,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
curve: envCurve, curve: envCurve,
begin: t, begin: t,
end: endWithRelease, end: endWithRelease,
}); }, "envelope");
} }
}; };