//! Filled waveform display using braille characters. use crate::scope::Orientation; use crate::theme; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::widgets::Widget; use std::cell::RefCell; thread_local! { static PATTERNS: RefCell> = const { RefCell::new(Vec::new()) }; } /// Filled waveform renderer using braille dot plotting. pub struct Waveform<'a> { data: &'a [f32], orientation: Orientation, color: Option, gain: f32, } impl<'a> Waveform<'a> { pub fn new(data: &'a [f32]) -> Self { Self { data, orientation: Orientation::Horizontal, color: None, gain: 1.0, } } pub fn orientation(mut self, o: Orientation) -> Self { self.orientation = o; self } pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self } pub fn gain(mut self, g: f32) -> Self { self.gain = g; self } } impl Widget for Waveform<'_> { fn render(self, area: Rect, buf: &mut Buffer) { if area.width == 0 || area.height == 0 || self.data.is_empty() { return; } let color = self.color.unwrap_or_else(|| theme::get().meter.low); match self.orientation { Orientation::Horizontal => { render_horizontal(self.data, area, buf, color, self.gain) } Orientation::Vertical => render_vertical(self.data, area, buf, color, self.gain), } } } fn braille_bit(dot_x: usize, dot_y: usize) -> u8 { match (dot_x, dot_y) { (0, 0) => 0x01, (0, 1) => 0x02, (0, 2) => 0x04, (0, 3) => 0x40, (1, 0) => 0x08, (1, 1) => 0x10, (1, 2) => 0x20, (1, 3) => 0x80, _ => unreachable!(), } } fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) { let width = area.width as usize; let height = area.height as usize; let fine_width = width * 2; let fine_height = height * 4; let len = data.len(); PATTERNS.with(|p| { let mut patterns = p.borrow_mut(); patterns.clear(); patterns.resize(width * height, 0); for fine_x in 0..fine_width { let start = fine_x * len / fine_width; let end = ((fine_x + 1) * len / fine_width).max(start + 1).min(len); let slice = &data[start..end]; let mut min_s = f32::MAX; let mut max_s = f32::MIN; for &s in slice { let s = (s * gain).clamp(-1.0, 1.0); if s < min_s { min_s = s; } if s > max_s { max_s = s; } } let fy_top = ((1.0 - max_s) * 0.5 * (fine_height - 1) as f32).round() as usize; let fy_bot = ((1.0 - min_s) * 0.5 * (fine_height - 1) as f32).round() as usize; let fy_top = fy_top.min(fine_height - 1); let fy_bot = fy_bot.min(fine_height - 1); let char_x = fine_x / 2; let dot_x = fine_x % 2; for fy in fy_top..=fy_bot { let char_y = fy / 4; let dot_y = fy % 4; patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y); } } for cy in 0..height { for cx in 0..width { let pattern = patterns[cy * width + cx]; if pattern != 0 { let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); buf[(area.x + cx as u16, area.y + cy as u16)] .set_char(ch) .set_fg(color); } } } }); } fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) { let width = area.width as usize; let height = area.height as usize; let fine_width = width * 2; let fine_height = height * 4; let len = data.len(); PATTERNS.with(|p| { let mut patterns = p.borrow_mut(); patterns.clear(); patterns.resize(width * height, 0); for fine_y in 0..fine_height { let start = fine_y * len / fine_height; let end = ((fine_y + 1) * len / fine_height).max(start + 1).min(len); let slice = &data[start..end]; let mut min_s = f32::MAX; let mut max_s = f32::MIN; for &s in slice { let s = (s * gain).clamp(-1.0, 1.0); if s < min_s { min_s = s; } if s > max_s { max_s = s; } } let fx_left = ((min_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize; let fx_right = ((max_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize; let fx_left = fx_left.min(fine_width - 1); let fx_right = fx_right.min(fine_width - 1); let char_y = fine_y / 4; let dot_y = fine_y % 4; for fx in fx_left..=fx_right { let char_x = fx / 2; let dot_x = fx % 2; patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y); } } for cy in 0..height { for cx in 0..width { let pattern = patterns[cy * width + cx]; if pattern != 0 { let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); buf[(area.x + cx as u16, area.y + cy as u16)] .set_char(ch) .set_fg(color); } } } }); }