Have all linear input be controllable by an LFO

This commit is contained in:
2026-03-26 10:55:37 +00:00
parent f26ecda58c
commit d47cae5a95
5 changed files with 68 additions and 19 deletions

53
www/bootstrap.js vendored
View File

@@ -231,9 +231,9 @@ class PatchRouter {
return dst?.kind === "out" && c.dst_jack === "audio_in";
});
// Mark VCAs that have an ADSR patched into their cv_in
// Mark VCAs that have an ADSR patched into any of their CV param inputs
for (const c of patch.cables) {
if (c.dst_jack !== "cv_in") continue;
if (!c.dst_jack.startsWith("cv_")) continue;
const srcMod = patch.modules.find(m => m.id === c.src);
const dstInfo = this.nodes.get(c.dst);
if (srcMod?.kind === "adsr" && dstInfo?.kind === "vca") {
@@ -312,28 +312,49 @@ class PatchRouter {
const dst = this.nodes.get(cable.dst);
if (!src?.node || !dst?.node) return; // ADSR has node:null — skip
// ── Audio signal cables ────────────────────────────────────────────
// ── Audio signal ───────────────────────────────────────────────────
if (cable.dst_jack === "audio_in") {
try { src.node.connect(dst.node); } catch(_) {}
return;
}
// ── CV modulation cables ───────────────────────────────────────────
if (cable.dst_jack === "cv_in") {
if (src.kind === "lfo") {
if (dst.kind === "svf") {
// LFO modulates filter cutoff frequency
try { src.node.connect(dst.node.frequency); } catch(_) {}
} else if (dst.kind === "vco") {
// LFO modulates VCO pitch (vibrato)
try { src.node.connect(dst.node.frequency); } catch(_) {}
}
// LFO → VCA: not wired (VCA cv_in is for ADSR envelope only)
}
// ADSR → VCA: handled by parameter automation in noteOn/Off, not a node connection
// ── Per-parameter CV modulation: cv_{param_id} ────────────────────
if (cable.dst_jack.startsWith("cv_")) {
// ADSR → VCA: handled by parameter automation in noteOn/Off (not a node connection)
if (src.kind === "adsr") return;
const ap = this._getAudioParam(dst, cable.dst_jack.slice(3));
if (ap) try { src.node.connect(ap); } catch(_) {}
}
}
// Returns the Web Audio AudioParam that corresponds to a param id on a node,
// or null if no mapping exists.
_getAudioParam(nodeInfo, paramId) {
switch (nodeInfo.kind) {
case "vco":
if (paramId === "freq_hz") return nodeInfo.node.frequency;
break;
case "svf":
if (paramId === "cutoff_hz") return nodeInfo.node.frequency;
if (paramId === "resonance") return nodeInfo.node.Q;
break;
case "vca":
if (paramId === "gain") return nodeInfo.node.gain;
break;
case "lfo":
if (paramId === "rate_hz") return nodeInfo.oscNode.frequency;
if (paramId === "depth") return nodeInfo.node.gain;
break;
case "out":
if (paramId === "level") return nodeInfo.node.gain;
break;
case "adsr":
// ADSR has no Web Audio node; envelope is driven via parameter automation
break;
}
return null;
}
_midiHz(note) {
return 440 * Math.pow(2, (note - 69) / 12);
}