Working version of LFOs

This commit is contained in:
Aria 2025-08-20 00:38:25 -05:00
parent f17a4d045f
commit 4a70ac5a99
3 changed files with 141 additions and 17 deletions

View file

@ -1430,6 +1430,11 @@ export const { panorient } = registerControl('panorient');
// ['portamento'], // ['portamento'],
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare // TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
export const { rate } = registerControl('rate'); export const { rate } = registerControl('rate');
export const { lfoTarget } = registerControl('lfoTarget');
export const { lfoParam } = registerControl('lfoParam');
export const { lfoNum } = registerControl('lfoNum');
export const { lfoBipolar } = registerControl('lfoBipolar');
export const { lfoShape } = registerControl('lfoShape');
// TODO: slide param for certain synths // TODO: slide param for certain synths
export const { slide } = registerControl('slide'); export const { slide } = registerControl('slide');
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare

View file

@ -510,6 +510,66 @@ export function resetGlobalEffects() {
orbits = {}; orbits = {};
analysers = {}; analysers = {};
analysersData = {}; analysersData = {};
lfos = {};
nodes = {};
}
function _getNodeParam(node, name) {
// Worklet case
if (node?.parameters) {
const p = node.parameters.get(name);
if (p instanceof AudioParam) {
return p;
}
}
// Built-in node case
const p = node?.[name];
if (p instanceof AudioParam) {
return p;
}
return undefined;
}
function _getNodeParams(node) {
const params = new Set();
// Worklet case
if (node?.parameters) {
node.parameters.forEach((_v, k) => params.add(k));
}
// Guesses based on common parameters
["gain", "frequency", "detune", "Q", "pan", "playbackRate", "delayTime"]
.forEach((k) => { if (node?.[k] instanceof AudioParam) params.add(k); });
return Array.from(params);
}
function connectLFO(lfoNum, target, param, frequency, depth, shape, bipolar, start, end) {
debugger;
let lfoNode = lfos[lfoNum];
const params = {
frequency,
depth,
}
if (lfoNode == null) {
const ac = getAudioContext();
const dcoffset = bipolar > 0.5 ? -0.5 : 0;
lfoNode = getLfo(ac, start, 1e9, { frequency, depth, shape, dcoffset});
lfos[lfoNum] = lfoNode;
}
lfoNode.disconnect();
const targetNodes = nodes[target];
targetNodes.forEach((targetNode) => {
if (targetNode === undefined) {
const keys = Object.keys(nodes);
errorLogger(new Error(`Could not connect to target ${target} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough');
return;
}
const targetParam = _getNodeParam(targetNode, param);
if (targetParam === undefined) {
const parameters = _getNodeParams(targetNode);
errorLogger(new Error(`Could not connect to parameter ${param} on node ${target}. Available parameters are ${parameters.join(", ")}`), 'superdough');
}
lfoNode.connect(targetParam);
});
} }
let activeSoundSources = new Map(); let activeSoundSources = new Map();
@ -519,6 +579,9 @@ 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 lfos = {};
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) => {
// 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();
@ -628,6 +691,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee, compressorKnee,
compressorAttack, compressorAttack,
compressorRelease, compressorRelease,
lfo,
lfoNum,
rate,
lfoTarget,
lfoParam,
lfoBipolar,
lfoShape,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@ -681,7 +751,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
let sourceNode; let sourceNode;
if (source) { if (source) {
sourceNode = source(t, value, hapDuration, cps); sourceNode = source(t, value, hapDuration, cps);
nodes['source'] = [sourceNode];
} else if (getSound(s)) { } else if (getSound(s)) {
debugger;
const { onTrigger } = getSound(s); const { onTrigger } = getSound(s);
const onEnded = () => { const onEnded = () => {
audioNodes.forEach((n) => n?.disconnect()); audioNodes.forEach((n) => n?.disconnect());
@ -692,6 +764,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, soundHandle); activeSoundSources.set(chainID, soundHandle);
nodes['source'] = [soundHandle.oscillator];
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@ -711,12 +784,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
// gain stage // gain stage
chain.push(gainNode(gain)); const initialGain = gainNode(gain);
nodes['gain'] = [initialGain];
chain.push(initialGain);
//filter //filter
const ftype = getFilterType(value.ftype); const ftype = getFilterType(value.ftype);
if (cutoff !== undefined) { if (cutoff !== undefined) {
let lp = () => const lp = () =>
createFilter( createFilter(
ac, ac,
'lowpass', 'lowpass',
@ -733,14 +808,18 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
ftype, ftype,
drive, drive,
); );
chain.push(lp()); const lp1 = lp();
nodes['lpf'] = [lp1];
chain.push(lp1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(lp()); const lp2 = lp();
nodes['lpf'].push(lp2);
chain.push(lp2);
} }
} }
if (hcutoff !== undefined) { if (hcutoff !== undefined) {
let hp = () => const hp = () =>
createFilter( createFilter(
ac, ac,
'highpass', 'highpass',
@ -755,31 +834,56 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
end, end,
fanchor, fanchor,
); );
chain.push(hp()); const hp1 = hp();
nodes['hpf'] = [hp1];
chain.push(hp1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(hp()); const hp2 = hp();
nodes['hpf'].push(hp1);
chain.push(hp2);
} }
} }
if (bandf !== undefined) { if (bandf !== undefined) {
let bp = () => let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
chain.push(bp()); const bp1 = bp();
nodes['bpf'] = [bp1];
chain.push(bp1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(bp()); const bp2 = bp();
nodes['bpf'].push(bp2);
chain.push(bp2);
} }
} }
if (vowel !== undefined) { if (vowel !== undefined) {
const vowelFilter = ac.createVowelFilter(vowel); const vowelFilter = ac.createVowelFilter(vowel);
nodes['vowel'] = vowelFilter;
chain.push(vowelFilter); chain.push(vowelFilter);
} }
// effects // effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); if (coarse !== undefined) {
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); const coarseNode = getWorklet(ac, 'coarse-processor', { coarse });
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); nodes['coarse'] = coarseNode;
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); chain.push(coarseNode);
}
if (crush !== undefined) {
const crushNode = getWorklet(ac, 'crush-processor', { crush });
nodes['crush'] = crushNode;
chain.push(crushNode);
}
if (shape !== undefined) {
const shapeNode = getWorklet(ac, 'shape-processor', { shape });
nodes['shape'] = shapeNode;
chain.push(shapeNode);
}
if (distort !== undefined) {
const distortNode = getWorklet(ac, 'distort-processor', { distort });
nodes['distort'] = distortNode;
chain.push(distortNode);
}
if (tremolosync != null) { if (tremolosync != null) {
tremolo = cps * tremolosync; tremolo = cps * tremolosync;
@ -808,10 +912,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(amGain); chain.push(amGain);
} }
compressorThreshold !== undefined && if (compressorThreshold !== undefined) {
chain.push( const compressorNode = getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease);
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease), nodes['compressor'] = compressorNode;
); chain.push(compressorNode);
}
// panning // panning
if (pan !== undefined) { if (pan !== undefined) {
@ -822,17 +927,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// phaser // phaser
if (phaser !== undefined && phaserdepth > 0) { if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
nodes['phaser'] = phaserFX;
chain.push(phaserFX); chain.push(phaserFX);
} }
// last gain // last gain
const post = new GainNode(ac, { gain: postgain }); const post = new GainNode(ac, { gain: postgain });
nodes['post'] = post;
chain.push(post); chain.push(post);
// delay // delay
let delaySend; let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
nodes['delay'] = delayNode;
delaySend = effectSend(post, delayNode, delay); delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend); audioNodes.push(delaySend);
} }
@ -851,6 +959,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
nodes['room'] = reverbNode;
reverbSend = effectSend(post, reverbNode, room); reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend); audioNodes.push(reverbSend);
} }
@ -874,6 +983,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// connect chain elements together // connect chain elements together
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
connectLFO(lfoNum, lfoTarget, lfoParam, rate, lfo, lfoBipolar, lfoShape, t, endWithRelease);
}; };
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {

View file

@ -87,6 +87,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
oscillator: o,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -156,6 +157,7 @@ export function registerSynthSounds() {
return { return {
node, node,
oscillator: o,
stop: (endTime) => { stop: (endTime) => {
o.stop(endTime); o.stop(endTime);
}, },
@ -222,6 +224,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -298,6 +301,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -375,6 +379,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -420,6 +425,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
oscillator: o,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -492,6 +498,7 @@ export function getOscillator(s, t, value) {
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
oscillator: o,
stop: (time) => { stop: (time) => {
fmModulator.stop(time); fmModulator.stop(time);
vibratoOscillator?.stop(time); vibratoOscillator?.stop(time);