rust-synth 0.23.0

Terminal modular ambient synthesizer — FunDSP + Ratatui. Long cinematic pads, Euclidean drum sequencer, per-track LFO, Valhalla-Supermassive-style reverb, genetic evolution coupled to Conway's Game of Life, TOML presets, FLAC recording.
Documentation
//! Parameter sliders for the currently-selected track.

use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, Gauge};
use ratatui::Frame;

use super::app::{AppState, Focus};
use crate::audio::engine::EngineHandle;
use crate::audio::preset::{lfo_target_name, LFO_TARGETS};

pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle, app: &AppState) {
    let tracks = engine.tracks.lock();
    let Some(track) = tracks.get(app.selected_track) else {
        return;
    };
    let s = track.params.snapshot();

    let focus_style = if app.focus == Focus::Params {
        Style::default().fg(Color::Yellow)
    } else {
        Style::default().fg(Color::Gray)
    };
    let outer = Block::default()
        .borders(Borders::ALL)
        .title(format!(
            " params · {} · {} {} ",
            track.name,
            track.kind.label(),
            if app.focus == Focus::Params { "" } else { " " }
        ))
        .border_style(focus_style);
    let inner = outer.inner(area);
    f.render_widget(outer, area);

    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(1); 13])
        .split(inner);

    let lfo_target_idx = (s.lfo_target.round() as u32) % LFO_TARGETS;
    let items: [(&str, f32, String); 13] = [
        ("gain    ", s.gain,                              format!("{:>4.2}", s.gain)),
        ("cutoff  ", norm_log(s.cutoff, 40.0, 12000.0),   format!("{:>5.0} Hz", s.cutoff)),
        ("resonance", (s.resonance / 0.70).min(1.0),      format!("{:>4.2}", s.resonance)),
        ("detune  ", (s.detune + 50.0) / 100.0,           format!("{:>+3.0} ct", s.detune)),
        ("freq    ", norm_log(s.freq, 20.0, 880.0),       format!("{:>5.1} Hz", s.freq)),
        ("reverb  ", s.reverb_mix,                        format!("{:>4.2}", s.reverb_mix)),
        ("supermass", s.supermass,                        format!("{:>4.2}", s.supermass)),
        ("pulse   ", s.pulse_depth,                       format!("{:>4.2}", s.pulse_depth)),
        ("lfo rate", norm_log(s.lfo_rate, 0.01, 20.0),    format!("{:>5.2} Hz", s.lfo_rate)),
        ("lfo depth", s.lfo_depth,                        format!("{:>4.2}", s.lfo_depth)),
        ("lfo tgt ", (lfo_target_idx as f32) / (LFO_TARGETS - 1) as f32,
                                                          lfo_target_name(lfo_target_idx).to_string()),
        ("character", s.character,                        format!("{:>4.2}", s.character)),
        ("arp     ", s.arp,                               format!("{:>4.2}", s.arp)),
    ];

    for (i, ((name, v, label), row)) in items.iter().zip(rows.iter()).enumerate() {
        let selected = i == app.selected_param && app.focus == Focus::Params;
        let style = if selected {
            Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
        } else {
            Style::default().fg(Color::Gray)
        };
        let arrow = if selected { "" } else { "  " };
        let g = Gauge::default()
            .block(Block::default())
            .gauge_style(style)
            .ratio(v.clamp(0.0, 1.0) as f64)
            .label(format!("{arrow}{name}  {label}"));
        f.render_widget(g, *row);
    }
}

fn norm_log(v: f32, lo: f32, hi: f32) -> f32 {
    let v = v.max(lo);
    ((v.ln() - lo.ln()) / (hi.ln() - lo.ln())).clamp(0.0, 1.0)
}