Have all linear input be controllable by an LFO
This commit is contained in:
@@ -36,7 +36,6 @@ impl Svf {
|
|||||||
label: "Filter",
|
label: "Filter",
|
||||||
jacks: &[
|
jacks: &[
|
||||||
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
||||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
|
||||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||||
],
|
],
|
||||||
params: &[
|
params: &[
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ impl Vco {
|
|||||||
kind: "vco",
|
kind: "vco",
|
||||||
label: "VCO",
|
label: "VCO",
|
||||||
jacks: &[
|
jacks: &[
|
||||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
|
||||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||||
],
|
],
|
||||||
params: &[
|
params: &[
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ impl Vca {
|
|||||||
label: "VCA",
|
label: "VCA",
|
||||||
jacks: &[
|
jacks: &[
|
||||||
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
||||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
|
||||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||||
],
|
],
|
||||||
params: &[
|
params: &[
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ const MOD_HEADER: f32 = 26.0;
|
|||||||
const JACK_ROW: f32 = 24.0;
|
const JACK_ROW: f32 = 24.0;
|
||||||
const PARAM_ROW: f32 = 20.0;
|
const PARAM_ROW: f32 = 20.0;
|
||||||
const JACK_R: f32 = 7.0;
|
const JACK_R: f32 = 7.0;
|
||||||
|
/// Smaller jack used for per-parameter CV inputs, rendered in the param area.
|
||||||
|
const CV_JACK_R: f32 = 4.5;
|
||||||
const PALETTE_H: f32 = 48.0;
|
const PALETTE_H: f32 = 48.0;
|
||||||
const PALETTE_BTN_W: f32 = 76.0;
|
const PALETTE_BTN_W: f32 = 76.0;
|
||||||
const PALETTE_PAD: f32 = 8.0;
|
const PALETTE_PAD: f32 = 8.0;
|
||||||
@@ -106,6 +108,18 @@ impl Module {
|
|||||||
out_idx += 1;
|
out_idx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Virtual per-parameter CV input jacks: id = "cv_{param_id}"
|
||||||
|
if let Some(param_id) = jack_id.strip_prefix("cv_") {
|
||||||
|
let n_in = desc.jacks.iter().filter(|j| j.direction == Direction::Input).count();
|
||||||
|
let n_out = desc.jacks.iter().filter(|j| j.direction == Direction::Output).count();
|
||||||
|
let params_top = self.y + MOD_HEADER + n_in.max(n_out) as f32 * JACK_ROW + 3.0;
|
||||||
|
for (pi, p) in desc.params.iter().enumerate() {
|
||||||
|
if p.id == param_id && p.labels.is_empty() {
|
||||||
|
let jy = params_top + 4.0 + pi as f32 * PARAM_ROW + PARAM_ROW * 0.5;
|
||||||
|
return Some((self.x, jy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +172,18 @@ impl Module {
|
|||||||
return Some((j.id.to_string(), j.direction, j.signal));
|
return Some((j.id.to_string(), j.direction, j.signal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Virtual per-parameter CV input jacks
|
||||||
|
let n_in = desc.jacks.iter().filter(|j| j.direction == Direction::Input).count();
|
||||||
|
let n_out = desc.jacks.iter().filter(|j| j.direction == Direction::Output).count();
|
||||||
|
let params_top = self.y + MOD_HEADER + n_in.max(n_out) as f32 * JACK_ROW + 3.0;
|
||||||
|
for (pi, p) in desc.params.iter().enumerate() {
|
||||||
|
if p.labels.is_empty() {
|
||||||
|
let jy = params_top + 4.0 + pi as f32 * PARAM_ROW + PARAM_ROW * 0.5;
|
||||||
|
if hypot(x - self.x, y - jy) <= CV_JACK_R * 2.5 {
|
||||||
|
return Some((format!("cv_{}", p.id), Direction::Input, SignalKind::Cv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -730,6 +756,11 @@ impl PatchBay {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ── Continuous slider ─────────────────────────────────────────
|
// ── Continuous slider ─────────────────────────────────────────
|
||||||
|
// Small CV input jack on the left edge of the param row
|
||||||
|
let jy = py + PARAM_ROW * 0.5;
|
||||||
|
draw_jack(ctx, m.x as f64, jy as f64, CV_JACK_R as f64,
|
||||||
|
signal_color(SignalKind::Cv), false);
|
||||||
|
|
||||||
let ratio = ((val - p.min) / (p.max - p.min)).clamp(0.0, 1.0);
|
let ratio = ((val - p.min) / (p.max - p.min)).clamp(0.0, 1.0);
|
||||||
let track_y = py + 13.0;
|
let track_y = py + 13.0;
|
||||||
|
|
||||||
|
|||||||
53
www/bootstrap.js
vendored
53
www/bootstrap.js
vendored
@@ -231,9 +231,9 @@ class PatchRouter {
|
|||||||
return dst?.kind === "out" && c.dst_jack === "audio_in";
|
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) {
|
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 srcMod = patch.modules.find(m => m.id === c.src);
|
||||||
const dstInfo = this.nodes.get(c.dst);
|
const dstInfo = this.nodes.get(c.dst);
|
||||||
if (srcMod?.kind === "adsr" && dstInfo?.kind === "vca") {
|
if (srcMod?.kind === "adsr" && dstInfo?.kind === "vca") {
|
||||||
@@ -312,28 +312,49 @@ class PatchRouter {
|
|||||||
const dst = this.nodes.get(cable.dst);
|
const dst = this.nodes.get(cable.dst);
|
||||||
if (!src?.node || !dst?.node) return; // ADSR has node:null — skip
|
if (!src?.node || !dst?.node) return; // ADSR has node:null — skip
|
||||||
|
|
||||||
// ── Audio signal cables ────────────────────────────────────────────
|
// ── Audio signal ───────────────────────────────────────────────────
|
||||||
if (cable.dst_jack === "audio_in") {
|
if (cable.dst_jack === "audio_in") {
|
||||||
try { src.node.connect(dst.node); } catch(_) {}
|
try { src.node.connect(dst.node); } catch(_) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CV modulation cables ───────────────────────────────────────────
|
// ── Per-parameter CV modulation: cv_{param_id} ────────────────────
|
||||||
if (cable.dst_jack === "cv_in") {
|
if (cable.dst_jack.startsWith("cv_")) {
|
||||||
if (src.kind === "lfo") {
|
// ADSR → VCA: handled by parameter automation in noteOn/Off (not a node connection)
|
||||||
if (dst.kind === "svf") {
|
if (src.kind === "adsr") return;
|
||||||
// LFO modulates filter cutoff frequency
|
const ap = this._getAudioParam(dst, cable.dst_jack.slice(3));
|
||||||
try { src.node.connect(dst.node.frequency); } catch(_) {}
|
if (ap) try { src.node.connect(ap); } 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
_midiHz(note) {
|
||||||
return 440 * Math.pow(2, (note - 69) / 12);
|
return 440 * Math.pow(2, (note - 69) / 12);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user