use crate::processing::FrameData;
use crate::visualizations::render::quantize_color;
use crate::visualizations::Visualization;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use std::collections::VecDeque;
pub struct Spectrogram {
history: VecDeque<Vec<f32>>,
beat_markers: VecDeque<bool>,
max_history: usize,
quant_step: u8,
}
impl Spectrogram {
pub fn new(max_history: usize) -> Self {
Self {
history: VecDeque::with_capacity(max_history),
beat_markers: VecDeque::with_capacity(max_history),
max_history,
quant_step: 16,
}
}
}
impl Visualization for Spectrogram {
fn name(&self) -> &str {
"spectrogram"
}
fn update(&mut self, frame: &FrameData) {
let mut recycled = if self.history.len() >= self.max_history {
self.beat_markers.pop_front();
self.history.pop_front().unwrap()
} else {
Vec::new()
};
recycled.resize(frame.spectrum.len(), 0.0);
recycled.copy_from_slice(&frame.spectrum);
self.history.push_back(recycled);
self.beat_markers.push_back(frame.beat.beat);
}
fn render(&mut self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.history.is_empty() {
return;
}
let cols = area.width as usize;
let rows = area.height as usize;
let total = self.history.len();
let visible_count = cols.min(total);
let skip = total - visible_count;
let x_offset = cols.saturating_sub(visible_count);
for (col_idx, (spectrum, &is_beat)) in self
.history
.iter()
.zip(self.beat_markers.iter())
.skip(skip)
.enumerate()
{
let x = area.x + (x_offset + col_idx) as u16;
if x >= area.x + area.width {
continue;
}
for row in 0..rows {
let band_idx = ((rows - 1 - row) * spectrum.len()) / rows.max(1);
let band_idx = band_idx.min(spectrum.len().saturating_sub(1));
let intensity = if spectrum.is_empty() {
0.0
} else {
spectrum[band_idx].clamp(0.0, 1.0)
};
let y = area.y + row as u16;
let display_intensity = if is_beat {
(intensity + 0.5).clamp(0.0, 1.0)
} else {
intensity
};
let color = quantize_color(magma_colormap(display_intensity), self.quant_step);
buf[(x, y)]
.set_char(intensity_char(display_intensity))
.set_fg(color);
}
}
}
fn set_quantization_step(&mut self, step: u8) {
self.quant_step = step;
}
fn apply_config(&mut self, config: &toml::Value) {
if let Some(len) = config.get("history_length").and_then(|v| v.as_integer()) {
self.max_history = len as usize;
}
}
fn save_config(&self) -> toml::Value {
let mut table = toml::value::Table::new();
table.insert(
"history_length".to_string(),
toml::Value::Integer(self.max_history as i64),
);
toml::Value::Table(table)
}
}
fn intensity_char(intensity: f32) -> char {
match (intensity * 4.0) as u8 {
0 => ' ',
1 => '\u{2591}', 2 => '\u{2592}', 3 => '\u{2593}', _ => '\u{2588}', }
}
fn magma_colormap(t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
let (r, g, b) = if t < 0.33 {
let s = t / 0.33;
((s * 120.0) as u8, 0u8, (s * 150.0) as u8)
} else if t < 0.66 {
let s = (t - 0.33) / 0.33;
(
120 + (s * 135.0) as u8,
(s * 80.0) as u8,
150 - (s * 150.0) as u8,
)
} else {
let s = (t - 0.66) / 0.34;
(255, 80 + (s * 175.0) as u8, (s * 80.0) as u8)
};
Color::Rgb(r, g, b)
}