use anyhow::{Context, Result};
use clap::Parser;
use ringbuf::traits::{Consumer, Split};
use ringbuf::HeapRb;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
mod audio;
mod beat;
mod config;
mod processing;
mod ui;
mod visualizations;
use audio::{AudioConfig, AudioTap};
use beat::BeatDetector;
use config::Config;
use processing::{FrameData, Processor, ProcessorConfig};
use ui::App;
use visualizations::aurora::Aurora;
use visualizations::life::Life;
use visualizations::lissajous::Lissajous;
use visualizations::milkdrop::Milkdrop;
use visualizations::plasma::Plasma;
use visualizations::radial::RadialSpectrum;
use visualizations::rain::Rain;
use visualizations::registry::VisualizationRegistry;
use visualizations::spectrogram::Spectrogram;
use visualizations::spectrum::SpectrumBars;
use visualizations::starfield::Starfield;
use visualizations::tunnel::Tunnel;
use visualizations::waveform::Waveform;
#[derive(Parser)]
#[command(name = "terminal-vibes", about = "Terminal-based music visualizer")]
struct Cli {
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
list_modes: bool,
}
fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
if cli.list_modes {
println!("Available visualization modes:");
println!(" spectrum - Frequency spectrum bars");
println!(" waveform - Oscilloscope waveform");
println!(" spectrogram - Scrolling frequency heatmap");
println!(" lissajous - Parametric curve spirograph");
println!(" tunnel - Concentric rings rushing forward");
println!(" radial - Circular spectrum starburst");
println!(" plasma - Sine interference color field");
println!(" aurora - Northern lights curtains");
println!(" starfield - 3D particle warp drive");
println!(" rain - Music-reactive falling streams");
println!(" life - Audio-reactive Game of Life");
return Ok(());
}
let config = Config::load(cli.config.as_ref()).context("Failed to load config")?;
let rb = HeapRb::<f32>::new(config.audio.buffer_size);
let (producer, mut consumer) = rb.split();
let audio_config = AudioConfig {
sample_rate: 44100.0,
channels: 2,
};
let _audio_tap =
AudioTap::new(producer, audio_config.clone()).context(if cfg!(target_os = "macos") {
"Failed to start audio capture. \
Make sure you're on macOS 15+ and have granted audio permissions."
} else if cfg!(target_os = "windows") {
"Failed to start audio capture. \
Make sure an audio output device is available."
} else {
"Failed to start audio capture. \
Make sure PulseAudio or PipeWire is running."
})?;
let mut registry = VisualizationRegistry::new();
registry.register(Box::new(SpectrumBars::new()));
registry.register(Box::new(Waveform::new()));
registry.register(Box::new(Spectrogram::new(200)));
registry.register(Box::new(Lissajous::new()));
registry.register(Box::new(Tunnel::new()));
registry.register(Box::new(RadialSpectrum::new()));
registry.register(Box::new(Plasma::new()));
registry.register(Box::new(Aurora::new()));
registry.register(Box::new(Starfield::new()));
registry.register(Box::new(Rain::new()));
registry.register(Box::new(Life::new()));
registry.register(Box::new(Milkdrop::new()));
let (frame_tx, frame_rx) = mpsc::sync_channel::<FrameData>(2);
let running = Arc::new(AtomicBool::new(true));
let running_processor = running.clone();
let fft_size = config.audio.fft_size;
let smoothing = config.audio.smoothing;
let beat_detection_config = config.beat_detection.clone();
let processor_handle = thread::spawn(move || {
let mut processor = Processor::new(ProcessorConfig {
fft_size,
smoothing,
num_bands: 128,
db_floor: -60.0,
});
let mut beat_detector = BeatDetector::new(128, beat_detection_config);
let mut accum = Vec::with_capacity(fft_size * 2);
let mut drain_buf = vec![0.0_f32; 4096];
let interval = Duration::from_millis(1000 / 60);
while running_processor.load(Ordering::Relaxed) {
let count = consumer.pop_slice(&mut drain_buf);
if count > 0 {
accum.extend_from_slice(&drain_buf[..count]);
}
while accum.len() >= fft_size {
let mut frame = processor.process(&accum[..fft_size]);
let (beat_data, tempo_data) = beat_detector.analyze(&frame.spectrum);
frame.beat = beat_data;
frame.tempo = tempo_data;
let _ = frame_tx.try_send(frame);
let keep = fft_size / 2;
let start = accum.len() - keep;
accum.drain(..start);
}
thread::sleep(interval);
}
});
let mut app = App::new(registry, config, running.clone());
app.run(frame_rx)?;
running.store(false, Ordering::Relaxed);
let _ = processor_handle.join();
Ok(())
}