rodio_tap 0.2.0

Tap rodio audio sources and analyze them in real-time.
Documentation
use rodio::{Decoder, DeviceSinkBuilder, Player};
use rodio_tap::{TapReader, Visualizer, VisualizerConfig};
use std::error::Error;
use std::fs::File;
use std::io::{self, BufReader, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

const BAR_WIDTH: usize = 40;

fn main() -> Result<(), Box<dyn Error>> {
    let wav_paths = parse_cli_paths()?;

    let mut sink_handle = DeviceSinkBuilder::open_default_sink()?;
    sink_handle.log_on_drop(false);
    let player = Player::connect_new(sink_handle.mixer());

    let (queue_in, queue_out) = rodio::queue::queue(false);
    let (tap_reader, tap_adapter) = TapReader::<2>::new(queue_out);
    player.append(tap_adapter);

    for wav_path in &wav_paths {
        let file = File::open(wav_path)?;
        let decoder = Decoder::new(BufReader::new(file))?;
        queue_in.append(decoder);
    }

    let _terminal = TerminalGuard::new()?;

    let tap_for_reader = Arc::clone(&tap_reader);
    thread::spawn(move || {
        let config = VisualizerConfig {
            period: Duration::from_millis(33),
            ..Default::default()
        };
        let frequency_bins = config.frequency_bins();

        Visualizer::<2>::run_with_frame_reader(
            move || Some(Arc::clone(&tap_for_reader)),
            config,
            move |channels, sample_rate_hz| {
                let _ = render(&frequency_bins, channels, sample_rate_hz);
            },
        );
    });

    player.play();
    while !player.empty() {
        thread::sleep(Duration::from_millis(80));
    }
    thread::sleep(Duration::from_millis(100));

    Ok(())
}

fn render(
    frequency_bins: &[rodio_tap::FrequencyBin],
    channels: &[rodio_tap::ChannelSpectrum],
    sample_rate_hz: u32,
) -> io::Result<()> {
    let Some(channel) = channels.first() else {
        return Ok(());
    };

    let mut out = io::stdout().lock();
    write!(out, "\x1B[H")?;
    writeln!(
        out,
        "WAV visualizer (simple API)  |  sr: {} Hz  |  peak: {:.3}  |  rms: {:.3}{}",
        sample_rate_hz,
        channel.peak,
        channel.rms,
        if channels.len() > 1 {
            "  |  showing ch0"
        } else {
            ""
        }
    )?;

    for (idx, &magnitude) in channel.bins.iter().enumerate() {
        let bars = magnitude.round().clamp(0.0, BAR_WIDTH as f32) as usize;
        let bar = "#".repeat(bars);
        let freq = frequency_bins.get(idx);
        writeln!(
            out,
            "{:>5.0} - {:>5.0} Hz | {:<width$}",
            freq.map(|f| f.hz_lo).unwrap_or(0.0),
            freq.map(|f| f.hz_hi).unwrap_or(0.0),
            bar,
            width = BAR_WIDTH
        )?;
    }

    out.flush()
}

struct TerminalGuard;

impl TerminalGuard {
    fn new() -> io::Result<Self> {
        let mut out = io::stdout().lock();
        write!(out, "\x1B[2J\x1B[H\x1B[?25l")?;
        out.flush()?;
        Ok(Self)
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = writeln!(io::stdout(), "\x1B[?25h");
    }
}

fn parse_cli_paths() -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let mut args = std::env::args_os();
    let program = args.next().unwrap_or_default();
    let paths: Vec<PathBuf> = args.map(PathBuf::from).collect();

    if paths.is_empty() {
        return Err(format!(
            "Usage: {:?} <path/to/file1.wav> [path/to/file2.wav ...]",
            program
        )
        .into());
    }

    for path in &paths {
        if !path.exists() {
            return Err(format!("Input file does not exist: {}", path.display()).into());
        }
    }

    Ok(paths)
}