terminal-vibes 1.6.6

Terminal-based music visualizer for system audio
Documentation
use crate::processing::FrameData;
use crate::visualizations::render::{HalfBlockCanvas, SIN_LUT};
use crate::visualizations::Visualization;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use std::f32::consts::PI;

pub struct Plasma {
    time: f32,
    /// Scaling factors modulated by spectrum bands
    k1: f32,
    k2: f32,
    k3: f32,
    k4: f32,
    rms: f32,
    hue_offset: f32,
    beat_envelope: f32,
    canvas: HalfBlockCanvas,
}

impl Default for Plasma {
    fn default() -> Self {
        Self::new()
    }
}

impl Plasma {
    pub fn new() -> Self {
        Self {
            time: 0.0,
            k1: 8.0,
            k2: 12.0,
            k3: 10.0,
            k4: 14.0,
            rms: 0.0,
            hue_offset: 0.0,
            beat_envelope: 0.0,
            canvas: HalfBlockCanvas::new(0, 0),
        }
    }
}

impl Visualization for Plasma {
    fn name(&self) -> &str {
        "plasma"
    }

    fn update(&mut self, frame: &FrameData) {
        self.rms = frame.rms;
        self.beat_envelope = frame.beat.envelope;

        let band_count = frame.spectrum.len();
        if band_count >= 4 {
            let quarter = band_count / 4;
            let bass = frame.spectrum[..quarter].iter().sum::<f32>() / quarter as f32;
            let low_mid = frame.spectrum[quarter..quarter * 2].iter().sum::<f32>() / quarter as f32;
            let high_mid =
                frame.spectrum[quarter * 2..quarter * 3].iter().sum::<f32>() / quarter as f32;
            let treble = frame.spectrum[quarter * 3..].iter().sum::<f32>() / quarter as f32;

            // Bass stretches, treble tightens
            self.k1 = 6.0 + bass * 12.0;
            self.k2 = 8.0 + low_mid * 10.0;
            self.k3 = 7.0 + high_mid * 14.0;
            self.k4 = 10.0 + treble * 8.0;
        }

        // Beat envelope warps frequency density — very visible distortion
        let beat_boost = 1.0 + self.beat_envelope * 0.8;
        self.k1 *= beat_boost;
        self.k2 *= beat_boost;
        self.k3 *= beat_boost;
        self.k4 *= beat_boost;

        // Beat drives hue rotation hard — colors shift visibly on beat
        self.hue_offset += 0.02 + frame.peak * 0.05 + self.beat_envelope * 0.08;
        self.time += 0.03 + self.rms * 0.05 + self.beat_envelope * 0.06;
    }

    fn set_quantization_step(&mut self, step: u8) {
        self.canvas.set_step(step);
    }

    fn heavy_rendering(&self) -> bool {
        true
    }

    fn render(&mut self, area: Rect, buf: &mut Buffer) {
        if area.width == 0 || area.height == 0 {
            return;
        }

        self.canvas.resize_or_clear(area.width, area.height);
        let pw = self.canvas.pixel_width();
        let ph = self.canvas.pixel_height();

        for py in 0..ph {
            let y = py as f32 / ph as f32;
            for px in 0..pw {
                let x = px as f32 / pw as f32;

                // Classic plasma: sum of sine functions
                let v1 = SIN_LUT.get(x * self.k1 + self.time);
                let v2 = SIN_LUT.get(y * self.k2 + self.time * 1.3);
                let v3 = SIN_LUT.get((x + y) * self.k3 + self.time * 0.7);
                let dist = ((x - 0.5).powi(2) + (y - 0.5).powi(2)).sqrt();
                let v4 = SIN_LUT.get(dist * self.k4 + self.time * 1.1);

                let v = (v1 + v2 + v3 + v4) / 4.0; // -1.0..1.0
                let t = (v + 1.0) / 2.0; // normalize to 0.0..1.0

                let color = plasma_color(t, self.hue_offset);
                self.canvas.set(px, py, color);
            }
        }

        self.canvas.render(&area, buf);
    }
}

/// Map a value (0..1) and hue offset to an RGB color via HSV-like rotation.
fn plasma_color(t: f32, hue_offset: f32) -> Color {
    let hue = (t + hue_offset) % 1.0;
    let r = (SIN_LUT.get(hue * 2.0 * PI) * 0.5 + 0.5) * 255.0;
    let g = (SIN_LUT.get(hue * 2.0 * PI + 2.094) * 0.5 + 0.5) * 255.0; // +120°
    let b = (SIN_LUT.get(hue * 2.0 * PI + 4.189) * 0.5 + 0.5) * 255.0; // +240°
    Color::Rgb(r as u8, g as u8, b as u8)
}