use crate::{audio::Levels, engine::Engine};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
use std::time::Instant;
pub const FRAME_INTERVAL: std::time::Duration = std::time::Duration::from_millis(33);
const PHASE_PERIOD: f32 = std::f32::consts::TAU * 10.0;
pub struct Wave {
phase: f32,
rms: f32,
bass: f32,
pulse: f32,
peak: f32,
active: bool,
playback: (u64, u64),
updated: Instant,
}
impl Default for Wave {
fn default() -> Self {
Self {
phase: 0.0,
rms: 0.0,
bass: 0.0,
pulse: 0.0,
peak: 0.0,
active: false,
playback: (0, 0),
updated: Instant::now(),
}
}
}
fn smooth(value: &mut f32, target: f32, dt: f32, attack: f32, release: f32) {
let time = if target > *value { attack } else { release };
*value += (target - *value) * (1.0 - (-dt / time).exp());
}
impl Wave {
pub fn update(&mut self, engine: &Engine) {
let now = Instant::now();
let dt = now.duration_since(self.updated).as_secs_f32().min(0.1);
self.updated = now;
if self.playback != engine.playback_id() {
self.playback = engine.playback_id();
self.rms = 0.0;
self.bass = 0.0;
self.pulse = 0.0;
self.peak = 0.0;
}
self.active = engine.track().is_some() && !engine.paused;
if engine.track().is_none() {
self.rms = 0.0;
self.bass = 0.0;
self.pulse = 0.0;
self.peak = 0.0;
}
if self.active {
self.step(engine.levels(), dt);
}
}
fn step(&mut self, levels: Levels, dt: f32) {
smooth(&mut self.rms, levels.rms.sqrt(), dt, 0.018, 0.09);
smooth(&mut self.bass, levels.bass.sqrt(), dt, 0.014, 0.11);
smooth(&mut self.pulse, levels.pulse, dt, 0.006, 0.055);
smooth(&mut self.peak, levels.peak.sqrt(), dt, 0.007, 0.065);
self.phase = (self.phase
+ dt * (1.8 + self.rms * 1.2 + self.bass * 1.4 + self.pulse * 5.5))
% PHASE_PERIOD;
}
}
fn dot(layer: &mut [u8], width: usize, x: usize, y: usize) {
const BITS: [[u8; 2]; 4] = [[1, 8], [2, 16], [4, 32], [64, 128]];
layer[(y / 4) * width + x / 2] |= BITS[y % 4][x % 2];
}
impl Widget for &Wave {
fn render(self, area: Rect, buffer: &mut Buffer) {
if area.is_empty() {
return;
}
let inner = area;
let width = usize::from(inner.width);
let height = usize::from(inner.height);
let pixels = (width * 2, height * 4);
let center = (pixels.1 - 1) as f32 / 2.0;
let amplitude = (self.rms * 0.50 + self.bass * 0.20 + self.pulse * 0.35).min(0.94);
let detail = ((self.peak - self.rms * 0.8).max(0.0) * 0.55 + self.pulse * 0.45).min(1.0);
let mut layers: [Vec<u8>; 3] = std::array::from_fn(|_| vec![0; width * height]);
for (index, layer) in layers.iter_mut().enumerate() {
let mut previous = center as usize;
for x in 0..pixels.0 {
let position = x as f32 / (pixels.0 - 1) as f32;
let envelope = (std::f32::consts::PI * position).sin().max(0.0).sqrt();
let phase = self.phase - index as f32 * 0.5;
let angle = position * std::f32::consts::TAU;
let signal = (angle * 2.4 - phase).sin() * 0.56
+ (angle * 4.8 + phase * 1.3).sin() * 0.22
+ (angle * 1.2 - phase * 0.7).sin() * 0.12
+ (angle * 9.6 - phase * 2.1).sin() * detail * 0.14
+ (angle * 14.4 + phase * 2.7).sin() * self.pulse * 0.10;
let scale = [1.0, 0.78, 0.55][index];
let y = (center + signal * center * amplitude * envelope * scale)
.round()
.clamp(0.0, (pixels.1 - 1) as f32) as usize;
for point in previous.min(y)..=previous.max(y) {
dot(layer, width, x, point);
}
previous = y;
}
}
let pulse = self.pulse;
let brightness = 0.55 + self.peak * 0.45;
let colors = if self.active {
[
Color::Rgb(
((65.0 + pulse * 180.0) * brightness) as u8,
((230.0 - pulse * 65.0) * brightness) as u8,
(240.0 * brightness) as u8,
),
Color::Rgb(130, 95, 210),
Color::Rgb(35, 65, 90),
]
} else {
[Color::Cyan, Color::DarkGray, Color::DarkGray]
};
for y in 0..height {
for x in 0..width {
let index = y * width + x;
let mask = layers[0][index] | layers[1][index] | layers[2][index];
if mask == 0 {
continue;
}
let color = (0..3).find(|&layer| layers[layer][index] != 0).unwrap();
buffer[(inner.x + x as u16, inner.y + y as u16)]
.set_char(char::from_u32(0x2800 + u32::from(mask)).unwrap())
.set_fg(colors[color]);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hits_reach_full_strength_in_one_frame_and_release_between_fast_beats() {
let mut wave = Wave::default();
wave.step(
Levels {
pulse: 1.0,
..Levels::default()
},
1.0 / 30.0,
);
assert!(wave.pulse > 0.9, "A hit was blurred across multiple frames");
for _ in 0..4 {
wave.step(Levels::default(), 1.0 / 30.0);
}
assert!(wave.pulse < 0.15, "Fast consecutive hits blend together");
}
#[test]
fn phase_boundaries_keep_the_wave_shape_continuous() {
let area = Rect::new(0, 0, 80, 12);
for (turns, pulse) in [(1.0, 0.2), (10.0, 0.2), (10.0, 1.0)] {
let mut wave = Wave {
phase: std::f32::consts::TAU * turns - 0.001,
rms: 0.6,
bass: 0.4,
pulse,
peak: 0.5,
active: true,
..Wave::default()
};
let mut before = Buffer::empty(area);
(&wave).render(area, &mut before);
wave.step(
Levels {
rms: 0.36,
bass: 0.16,
pulse,
peak: 0.25,
},
0.0005,
);
let mut after = Buffer::empty(area);
(&wave).render(area, &mut after);
let changed = before
.content
.iter()
.zip(&after.content)
.filter(|(a, b)| a.symbol() != b.symbol())
.count();
assert!(
changed <= 20,
"Wave jumped across {turns} turns: {changed} cells changed"
);
}
}
}