1use ratatui::layout::Rect;
12use ratatui::style::{Color, Modifier, Style};
13use ratatui::symbols::Marker;
14use ratatui::widgets::canvas::{Canvas, Context, Line};
15use ratatui::widgets::{Block, Borders};
16use ratatui::Frame;
17
18use super::app::AppState;
19use crate::audio::engine::EngineHandle;
20use crate::audio::preset::PresetKind;
21use crate::audio::track::TrackSnapshot;
22
23const POINTS: usize = 240;
25const CYCLES: f64 = 2.0;
27
28pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle, app: &AppState) {
29 let tracks = engine.tracks.lock();
30 let Some(track) = tracks.get(app.selected_track) else {
31 return;
32 };
33 let s = track.params.snapshot();
34 let kind = track.kind;
35 let name = track.name.clone();
36 drop(tracks);
37
38 let (color, subtitle) = describe(kind, &s);
39 let title = format!(" waveshape · {} · {} ", name, subtitle);
40
41 let canvas = Canvas::default()
42 .block(
43 Block::default()
44 .borders(Borders::ALL)
45 .title(title)
46 .title_style(Style::default().add_modifier(Modifier::BOLD)),
47 )
48 .marker(Marker::Braille)
49 .x_bounds([0.0, CYCLES])
50 .y_bounds([-1.15, 1.15])
51 .paint(move |ctx| {
52 ctx.draw(&Line {
54 x1: 0.0,
55 y1: 0.0,
56 x2: CYCLES,
57 y2: 0.0,
58 color: Color::Rgb(40, 40, 48),
59 });
60 for k in 1..CYCLES as u32 {
62 ctx.draw(&Line {
63 x1: k as f64,
64 y1: -1.1,
65 x2: k as f64,
66 y2: 1.1,
67 color: Color::Rgb(30, 30, 38),
68 });
69 }
70 draw_waveshape(ctx, kind, &s, color);
71 });
72
73 f.render_widget(canvas, area);
74}
75
76fn describe(kind: PresetKind, s: &TrackSnapshot) -> (Color, String) {
77 match kind {
78 PresetKind::PadZimmer => (
79 Color::Cyan,
80 "4 detuned partials [1, 1.501, 2.013, 3.007]".to_string(),
81 ),
82 PresetKind::DroneSub => (
83 Color::Magenta,
84 format!("sub sine + brown @ ≤{} Hz", s.cutoff.min(300.0) as u32),
85 ),
86 PresetKind::Shimmer => (
87 Color::LightYellow,
88 "3 high partials [×2, ×3, ×4.007]".to_string(),
89 ),
90 PresetKind::Heartbeat => (
91 Color::Red,
92 "kick body · pitch-swept sine".to_string(),
93 ),
94 PresetKind::BassPulse => (
95 Color::Green,
96 "sine stack [×½, ×1, ×2]".to_string(),
97 ),
98 PresetKind::Bell => (
99 Color::LightBlue,
100 format!("FM · mod 2.76, depth {:.2}", s.resonance.min(0.65)),
101 ),
102 PresetKind::SuperSaw => (
103 Color::LightGreen,
104 format!("7-saw unison · spread {:.0} ct", s.detune.abs()),
105 ),
106 PresetKind::PluckSaw => (
107 Color::Yellow,
108 format!("2-saw · detune {:+.0} ct", s.detune),
109 ),
110 }
111}
112
113fn draw_waveshape(ctx: &mut Context, kind: PresetKind, s: &TrackSnapshot, color: Color) {
114 let mut prev: Option<(f64, f64)> = None;
115 for i in 0..POINTS {
116 let x = i as f64 / (POINTS - 1) as f64 * CYCLES;
117 let y = sample(kind, s, x).clamp(-1.1, 1.1);
119 if let Some((px, py)) = prev {
120 ctx.draw(&Line {
121 x1: px,
122 y1: py,
123 x2: x,
124 y2: y,
125 color,
126 });
127 }
128 prev = Some((x, y));
129 }
130}
131
132fn sample(kind: PresetKind, s: &TrackSnapshot, phase: f64) -> f64 {
133 let tau = std::f64::consts::TAU;
134 let p = tau * phase;
135 match kind {
136 PresetKind::PadZimmer => {
137 let det = s.detune as f64 * 0.000578;
138 0.30 * (p * 1.000).sin()
139 + 0.20 * (p * 1.501 * (1.0 + det)).sin()
140 + 0.14 * (p * 2.013 * (1.0 + det)).sin()
141 + 0.08 * (p * 3.007).sin()
142 }
143 PresetKind::DroneSub => {
144 0.60 * (p * 0.5).sin() + 0.15 * (p * 1.0).sin() + 0.08 * (p * 2.03).sin()
148 }
149 PresetKind::Shimmer => {
150 0.40 * (p * 2.000).sin() + 0.30 * (p * 3.000).sin() + 0.20 * (p * 4.007).sin()
151 }
152 PresetKind::Heartbeat => {
153 let pitch = 0.7 + 1.5 * (-phase * 5.0).exp();
156 let env = (-phase * 2.5).exp();
157 (p * pitch).sin() * env
158 }
159 PresetKind::BassPulse => {
160 0.55 * (p * 1.0).sin() + 0.22 * (p * 2.0).sin() + 0.35 * (p * 0.5).sin()
161 }
162 PresetKind::Bell => {
163 let depth = s.resonance.min(0.65) as f64;
166 let modulator = (p * 2.76).sin() * (depth * 3.5);
167 (p + modulator).sin()
168 }
169 PresetKind::SuperSaw => {
170 const OFFS: [f64; 7] = [-1.0, -0.66, -0.33, 0.0, 0.33, 0.66, 1.0];
171 let width = (s.detune.abs() as f64).max(1.0);
172 let mut sum = 0.0;
173 for off in OFFS {
174 let ratio = 2.0_f64.powf(off * width / 1200.0);
175 let x = phase * ratio;
176 sum += 2.0 * (x - (x + 0.5).floor());
178 }
179 sum / OFFS.len() as f64
180 }
181 PresetKind::PluckSaw => {
182 let cents_b = s.detune as f64 * 0.5;
183 let ratio_b = 2.0_f64.powf(cents_b / 1200.0);
184 let sa = 2.0 * (phase - (phase + 0.5).floor());
185 let xb = phase * ratio_b;
186 let sb = 2.0 * (xb - (xb + 0.5).floor());
187 0.5 * sa + 0.5 * sb
188 }
189 }
190}