use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::widgets::Widget; const DB_MIN: f32 = -48.0; const DB_MAX: f32 = 3.0; const DB_RANGE: f32 = DB_MAX - DB_MIN; pub struct VuMeter { left: f32, right: f32, } impl VuMeter { pub fn new(left: f32, right: f32) -> Self { Self { left, right } } fn amplitude_to_db(amp: f32) -> f32 { if amp <= 0.0 { DB_MIN } else { (20.0 * amp.log10()).clamp(DB_MIN, DB_MAX) } } fn db_to_normalized(db: f32) -> f32 { (db - DB_MIN) / DB_RANGE } fn row_to_color(row_position: f32) -> Color { if row_position > 0.9 { Color::Red } else if row_position > 0.75 { Color::Yellow } else { Color::Green } } } impl Widget for VuMeter { fn render(self, area: Rect, buf: &mut Buffer) { if area.width < 3 || area.height == 0 { return; } let height = area.height as usize; let half_width = area.width / 2; let gap = 1u16; let left_db = Self::amplitude_to_db(self.left); let right_db = Self::amplitude_to_db(self.right); let left_norm = Self::db_to_normalized(left_db); let right_norm = Self::db_to_normalized(right_db); let left_rows = (left_norm * height as f32).round() as usize; let right_rows = (right_norm * height as f32).round() as usize; for row in 0..height { let y = area.y + area.height - 1 - row as u16; let row_position = (row as f32 + 0.5) / height as f32; let color = Self::row_to_color(row_position); for col in 0..half_width.saturating_sub(gap) { let x = area.x + col; if row < left_rows { buf[(x, y)].set_char(' ').set_bg(color); } } for col in 0..half_width.saturating_sub(gap) { let x = area.x + half_width + gap + col; if x < area.x + area.width && row < right_rows { buf[(x, y)].set_char(' ').set_bg(color); } } } } }