Skip to main content

rust_synth/tui/
params.rs

1//! Parameter sliders for the currently-selected track.
2
3use ratatui::layout::{Constraint, Direction, Layout, Rect};
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::widgets::{Block, Borders, Gauge};
6use ratatui::Frame;
7
8use super::app::{AppState, Focus};
9use crate::audio::engine::EngineHandle;
10use crate::audio::preset::{lfo_target_name, LFO_TARGETS};
11
12pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle, app: &AppState) {
13    let tracks = engine.tracks.lock();
14    let Some(track) = tracks.get(app.selected_track) else {
15        return;
16    };
17    let s = track.params.snapshot();
18
19    let focus_style = if app.focus == Focus::Params {
20        Style::default().fg(Color::Yellow)
21    } else {
22        Style::default().fg(Color::Gray)
23    };
24    let outer = Block::default()
25        .borders(Borders::ALL)
26        .title(format!(
27            " params · {} · {} {} ",
28            track.name,
29            track.kind.label(),
30            if app.focus == Focus::Params { "◀" } else { " " }
31        ))
32        .border_style(focus_style);
33    let inner = outer.inner(area);
34    f.render_widget(outer, area);
35
36    let rows = Layout::default()
37        .direction(Direction::Vertical)
38        .constraints([Constraint::Length(1); 13])
39        .split(inner);
40
41    let lfo_target_idx = (s.lfo_target.round() as u32) % LFO_TARGETS;
42    let items: [(&str, f32, String); 13] = [
43        ("gain    ", s.gain,                              format!("{:>4.2}", s.gain)),
44        ("cutoff  ", norm_log(s.cutoff, 40.0, 12000.0),   format!("{:>5.0} Hz", s.cutoff)),
45        ("resonance", (s.resonance / 0.70).min(1.0),      format!("{:>4.2}", s.resonance)),
46        ("detune  ", (s.detune + 50.0) / 100.0,           format!("{:>+3.0} ct", s.detune)),
47        ("freq    ", norm_log(s.freq, 20.0, 880.0),       format!("{:>5.1} Hz", s.freq)),
48        ("reverb  ", s.reverb_mix,                        format!("{:>4.2}", s.reverb_mix)),
49        ("supermass", s.supermass,                        format!("{:>4.2}", s.supermass)),
50        ("pulse   ", s.pulse_depth,                       format!("{:>4.2}", s.pulse_depth)),
51        ("lfo rate", norm_log(s.lfo_rate, 0.01, 20.0),    format!("{:>5.2} Hz", s.lfo_rate)),
52        ("lfo depth", s.lfo_depth,                        format!("{:>4.2}", s.lfo_depth)),
53        ("lfo tgt ", (lfo_target_idx as f32) / (LFO_TARGETS - 1) as f32,
54                                                          lfo_target_name(lfo_target_idx).to_string()),
55        ("character", s.character,                        format!("{:>4.2}", s.character)),
56        ("arp     ", s.arp,                               format!("{:>4.2}", s.arp)),
57    ];
58
59    for (i, ((name, v, label), row)) in items.iter().zip(rows.iter()).enumerate() {
60        let selected = i == app.selected_param && app.focus == Focus::Params;
61        let style = if selected {
62            Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
63        } else {
64            Style::default().fg(Color::Gray)
65        };
66        let arrow = if selected { "▶ " } else { "  " };
67        let g = Gauge::default()
68            .block(Block::default())
69            .gauge_style(style)
70            .ratio(v.clamp(0.0, 1.0) as f64)
71            .label(format!("{arrow}{name}  {label}"));
72        f.render_widget(g, *row);
73    }
74}
75
76fn norm_log(v: f32, lo: f32, hi: f32) -> f32 {
77    let v = v.max(lo);
78    ((v.ln() - lo.ln()) / (hi.ln() - lo.ln())).clamp(0.0, 1.0)
79}