Dynamic changes to LFO

This commit is contained in:
2026-03-26 15:47:04 +00:00
parent ecb61dad41
commit b8d3dc48ab
2 changed files with 92 additions and 4 deletions

View File

@@ -86,6 +86,10 @@ struct Module {
x: f32,
y: f32,
param_values: Vec<f32>,
/// Per-param CV modulation offset for display only (JS pushes live values
/// each frame via `set_cv_mod`). Added to `param_values` then clamped to
/// [min, max] before rendering. Reset to 0 each frame via `clear_cv_mods`.
cv_mod: Vec<f32>,
}
impl Module {
@@ -279,7 +283,8 @@ impl PatchBay {
let id = self.next_id;
self.next_id += 1;
let param_values = desc.params.iter().map(|p| p.default).collect();
self.modules.push(Module { id, kind: desc.kind, label: desc.label, x, y, param_values });
let cv_mod = vec![0.0_f32; desc.params.len()];
self.modules.push(Module { id, kind: desc.kind, label: desc.label, x, y, param_values, cv_mod });
self.patch_version += 1;
id
} else {
@@ -308,6 +313,28 @@ impl PatchBay {
self.params_version
}
/// Reset all CV display offsets to zero. Call once per frame before
/// pushing fresh values via `set_cv_mod`.
pub fn clear_cv_mods(&mut self) {
for m in &mut self.modules {
for v in &mut m.cv_mod { *v = 0.0; }
}
}
/// Push a live CV modulation offset (in the param's native unit) for
/// display purposes. The offset is added to the knob value and clamped
/// to [min, max] before rendering.
pub fn set_cv_mod(&mut self, module_id: u32, param_id: &str, offset: f32) {
if let Some(m) = self.modules.iter_mut().find(|m| m.id == module_id) {
let desc = REGISTRY.iter().find(|d| d.kind == m.kind).expect("unknown kind");
if let Some(pi) = desc.params.iter().position(|p| p.id == param_id) {
if pi < m.cv_mod.len() {
m.cv_mod[pi] = offset;
}
}
}
}
/// Update a parameter value (param_idx is the position in the descriptor's params slice).
pub fn set_param(&mut self, module_id: u32, param_idx: usize, value: f32) {
if let Some(m) = self.modules.iter_mut().find(|m| m.id == module_id) {
@@ -729,7 +756,9 @@ impl PatchBay {
for (pi, p) in desc.params.iter().enumerate() {
let is_active = active_param == Some(pi);
let py = params_top + 4.0 + pi as f32 * PARAM_ROW;
let val = m.param_values.get(pi).copied().unwrap_or(p.default);
let base = m.param_values.get(pi).copied().unwrap_or(p.default);
let mod_v = m.cv_mod.get(pi).copied().unwrap_or(0.0);
let val = (base + mod_v).clamp(p.min, p.max);
let tx = m.x + 8.0;
let tw = MOD_W - 16.0;
@@ -802,10 +831,15 @@ impl PatchBay {
// ── Mini oscilloscope preview (VCO / LFO) ────────────────────────────
if has_waveform_preview(desc) {
// Effective value = knob + CV modulation offset, clamped to param range.
let get_param = |id: &str| -> f32 {
desc.params.iter().position(|p| p.id == id)
.and_then(|pi| m.param_values.get(pi))
.copied()
.map(|pi| {
let base = m.param_values.get(pi).copied().unwrap_or(0.0);
let mod_v = m.cv_mod.get(pi).copied().unwrap_or(0.0);
let p = &desc.params[pi];
(base + mod_v).clamp(p.min, p.max)
})
.unwrap_or(0.0)
};
let wave_idx = get_param("waveform").round() as usize;