Dynamic changes to LFO
This commit is contained in:
@@ -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;
|
||||
|
||||
54
www/bootstrap.js
vendored
54
www/bootstrap.js
vendored
@@ -360,6 +360,20 @@ class PatchRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Waveform sample (mirrors Rust waveform_sample) ───────────────────────────
|
||||
|
||||
function waveformSample(waveIdx, phase) {
|
||||
const TAU = 2 * Math.PI;
|
||||
switch (waveIdx) {
|
||||
case 0: return Math.sin(phase * TAU); // Sine
|
||||
case 1: return 2 * phase - 1; // Saw
|
||||
case 2: return phase < 0.5 ? 1 : -1; // Square
|
||||
case 3: return 4 * Math.abs(phase - Math.floor(phase + 0.5)) - 1; // Triangle
|
||||
case 4: return phase < 0.25 ? 1 : -1; // Pulse
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Computer keyboard map (standard DAW layout) ───────────────────────────────
|
||||
|
||||
const KEY_NOTE = {
|
||||
@@ -401,6 +415,15 @@ async function bootstrap() {
|
||||
const ro = new ResizeObserver(() => allCanvases.forEach(fitCanvas));
|
||||
allCanvases.forEach(c => ro.observe(c));
|
||||
|
||||
// ── Component registry: param min/max for CV display scaling ─────────
|
||||
// { kind → { paramId → { min, max } } }
|
||||
const paramRanges = new Map(
|
||||
JSON.parse(patchbay.available_components()).map(c => [
|
||||
c.kind,
|
||||
new Map(c.params.map(p => [p.id, { min: p.min, max: p.max }]))
|
||||
])
|
||||
);
|
||||
|
||||
// ── Default patch bay layout ──────────────────────────────────────────
|
||||
const cw = pbCanvas.width || pbCanvas.clientWidth || 800;
|
||||
patchbay.add_module("vco", cw * 0.08, 80);
|
||||
@@ -500,6 +523,37 @@ async function bootstrap() {
|
||||
const outLabel = router.isOutputPatched() ? "patched" : "unpatched";
|
||||
status.textContent = router.isOutputPatched() ? "Running · output patched" : "Running · output unpatched";
|
||||
|
||||
// ── Push live CV modulation offsets to the patchbay for display ──
|
||||
// This makes param bars and waveform previews reflect CV-modulated
|
||||
// values even when the knob hasn't moved.
|
||||
{
|
||||
const patch = JSON.parse(patchbay.get_patch_json());
|
||||
patchbay.clear_cv_mods();
|
||||
for (const cable of patch.cables) {
|
||||
if (!cable.dst_jack.startsWith("cv_")) continue;
|
||||
const srcMod = patch.modules.find(m => m.id === cable.src);
|
||||
if (!srcMod || srcMod.kind !== "lfo") continue;
|
||||
|
||||
const paramId = cable.dst_jack.slice(3); // "cv_rate_hz" → "rate_hz"
|
||||
const dstMod = patch.modules.find(m => m.id === cable.dst);
|
||||
if (!dstMod) continue;
|
||||
|
||||
const range = paramRanges.get(dstMod.kind)?.get(paramId);
|
||||
if (!range) continue;
|
||||
|
||||
// LFO normalised output: -depth..+depth
|
||||
const rate = srcMod.params.rate_hz ?? 2;
|
||||
const depth = srcMod.params.depth ?? 0.5;
|
||||
const waveIdx = Math.round(srcMod.params.waveform ?? 0);
|
||||
const phase = ((now / 1000) * rate) % 1.0;
|
||||
const lfoNorm = waveformSample(waveIdx, phase) * depth;
|
||||
|
||||
// Scale to ±½ of the param's full range for display
|
||||
const offset = lfoNorm * (range.max - range.min) * 0.5;
|
||||
patchbay.set_cv_mod(dstMod.id, paramId, offset);
|
||||
}
|
||||
}
|
||||
|
||||
oscilloscope.draw();
|
||||
spectrum.draw();
|
||||
patchbay.draw(now);
|
||||
|
||||
Reference in New Issue
Block a user