use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::symbols::Marker;
use ratatui::widgets::canvas::{Canvas, Context, Line};
use ratatui::widgets::{Block, Borders};
use ratatui::Frame;
use super::app::AppState;
use crate::audio::engine::EngineHandle;
use crate::audio::preset::PresetKind;
use crate::audio::track::TrackSnapshot;
const POINTS: usize = 240;
const CYCLES: f64 = 2.0;
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 kind = track.kind;
let name = track.name.clone();
drop(tracks);
let (color, subtitle) = describe(kind, &s);
let title = format!(" waveshape · {} · {} ", name, subtitle);
let canvas = Canvas::default()
.block(
Block::default()
.borders(Borders::ALL)
.title(title)
.title_style(Style::default().add_modifier(Modifier::BOLD)),
)
.marker(Marker::Braille)
.x_bounds([0.0, CYCLES])
.y_bounds([-1.15, 1.15])
.paint(move |ctx| {
ctx.draw(&Line {
x1: 0.0,
y1: 0.0,
x2: CYCLES,
y2: 0.0,
color: Color::Rgb(40, 40, 48),
});
for k in 1..CYCLES as u32 {
ctx.draw(&Line {
x1: k as f64,
y1: -1.1,
x2: k as f64,
y2: 1.1,
color: Color::Rgb(30, 30, 38),
});
}
draw_waveshape(ctx, kind, &s, color);
});
f.render_widget(canvas, area);
}
fn describe(kind: PresetKind, s: &TrackSnapshot) -> (Color, String) {
let c = s.character as f64;
match kind {
PresetKind::PadZimmer => {
let r1 = 1.0 + lerp3(1.0, 0.501, 0.618, c);
let r2 = 2.0 + lerp3(0.0, 0.013, 0.414, c);
let r3 = 3.0 + lerp3(0.0, 0.007, 0.739, c);
(Color::Cyan, format!("partials [1, {r1:.3}, {r2:.3}, {r3:.3}]"))
}
PresetKind::DroneSub => (
Color::Magenta,
format!("sub sine + noise @ ≤{} Hz", s.cutoff.min(300.0) as u32),
),
PresetKind::Shimmer => {
let r1 = lerp3(2.0, 2.0, 2.1, c);
let r2 = lerp3(3.0, 3.0, 3.3, c);
let r3 = lerp3(4.0, 4.007, 4.8, c);
(Color::LightYellow, format!("partials [×{r1:.2}, ×{r2:.2}, ×{r3:.2}]"))
}
PresetKind::Heartbeat => {
let drop = lerp3(0.3, 1.5, 3.0, c);
(Color::Red, format!("pitch-swept kick · drop ×{drop:.2}"))
}
PresetKind::BassPulse => (
Color::Green,
"sine stack [×½, ×1, ×2]".to_string(),
),
PresetKind::Bell => {
let ratio = lerp3(1.41, 2.76, 4.18, c);
(Color::LightBlue, format!("FM ratio {ratio:.2} · depth {:.2}", s.resonance.min(0.65)))
}
PresetKind::SuperSaw => (
Color::LightGreen,
format!("7-saw unison · spread {:.0} ct", s.detune.abs()),
),
PresetKind::PluckSaw => (
Color::Yellow,
format!("2-saw · detune {:+.0} ct", s.detune),
),
}
}
fn lerp3(a: f64, b: f64, d: f64, c: f64) -> f64 {
let c = c.clamp(0.0, 1.0);
if c < 0.5 {
a + (b - a) * (c * 2.0)
} else {
b + (d - b) * ((c - 0.5) * 2.0)
}
}
fn draw_waveshape(ctx: &mut Context, kind: PresetKind, s: &TrackSnapshot, color: Color) {
let mut prev: Option<(f64, f64)> = None;
for i in 0..POINTS {
let x = i as f64 / (POINTS - 1) as f64 * CYCLES;
let y = sample(kind, s, x).clamp(-1.1, 1.1);
if let Some((px, py)) = prev {
ctx.draw(&Line {
x1: px,
y1: py,
x2: x,
y2: y,
color,
});
}
prev = Some((x, y));
}
}
fn sample(kind: PresetKind, s: &TrackSnapshot, phase: f64) -> f64 {
let tau = std::f64::consts::TAU;
let p = tau * phase;
let c = s.character as f64;
match kind {
PresetKind::PadZimmer => {
let det = s.detune as f64 * 0.000578;
let r1 = 1.0 + lerp3(1.0, 0.501, 0.618, c);
let r2 = 2.0 + lerp3(0.0, 0.013, 0.414, c);
let r3 = 3.0 + lerp3(0.0, 0.007, 0.739, c);
0.30 * (p * 1.000).sin()
+ 0.20 * (p * r1 * (1.0 + det)).sin()
+ 0.14 * (p * r2 * (1.0 + det)).sin()
+ 0.08 * (p * r3).sin()
}
PresetKind::DroneSub => {
0.60 * (p * 0.5).sin() + 0.15 * (p * 1.0).sin() + 0.08 * (p * 2.03).sin()
}
PresetKind::Shimmer => {
let r1 = lerp3(2.0, 2.0, 2.1, c);
let r2 = lerp3(3.0, 3.0, 3.3, c);
let r3 = lerp3(4.0, 4.007, 4.8, c);
0.40 * (p * r1).sin() + 0.30 * (p * r2).sin() + 0.20 * (p * r3).sin()
}
PresetKind::Heartbeat => {
let drop_scale = lerp3(0.3, 1.5, 3.0, c);
let pitch = 0.7 + drop_scale * (-phase * 5.0).exp();
let env = (-phase * 2.5).exp();
(p * pitch).sin() * env
}
PresetKind::BassPulse => {
0.55 * (p * 1.0).sin() + 0.22 * (p * 2.0).sin() + 0.35 * (p * 0.5).sin()
}
PresetKind::Bell => {
let depth = s.resonance.min(0.65) as f64;
let ratio = lerp3(1.41, 2.76, 4.18, c);
let modulator = (p * ratio).sin() * (depth * 3.5);
(p + modulator).sin()
}
PresetKind::SuperSaw => {
const OFFS: [f64; 7] = [-1.0, -0.66, -0.33, 0.0, 0.33, 0.66, 1.0];
let width = (s.detune.abs() as f64).max(1.0);
let mut sum = 0.0;
for off in OFFS {
let ratio = 2.0_f64.powf(off * width / 1200.0);
let x = phase * ratio;
sum += 2.0 * (x - (x + 0.5).floor());
}
sum / OFFS.len() as f64
}
PresetKind::PluckSaw => {
let cents_b = s.detune as f64 * 0.5;
let ratio_b = 2.0_f64.powf(cents_b / 1200.0);
let sa = 2.0 * (phase - (phase + 0.5).floor());
let xb = phase * ratio_b;
let sb = 2.0 * (xb - (xb + 0.5).floor());
0.5 * sa + 0.5 * sb
}
}
}