From 79544f64c3e8aa56769378e8c985bc8df3b90115 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Tue, 23 Jun 2026 15:49:30 +0200 Subject: [PATCH] perf: decouple audio capture from render loop with dedicated thread Move PCM decoding into a producer thread that feeds an mpsc channel, letting the render loop drain samples non-blockingly each frame. Add a frame-aligned sleep via event::poll to replace the implicit CPU yield that the old blocking read provided. Also replace powf(0.5) with sqrt(). Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 131 ++++++++++++++++++++++------------------------------ 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9c18bf5..54816e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection}; use std::collections::VecDeque; use std::io::{stdout, Read, Write}; use std::fs::File; +use std::sync::mpsc; +use std::thread; use std::time::{Duration, Instant}; use byteorder::{LittleEndian, ReadBytesExt}; use std::process::{Command, Stdio}; @@ -159,22 +161,15 @@ fn main() { let fft = Radix4::new(fft_size, FftDirection::Forward); - let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)]; let mut complex_buffer = vec![Complex { re: 0.0f32, im: 0.0f32 }; fft_size]; let hann_window: Vec = (0..fft_size) .map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos())) .collect(); - enum AudioSource { - StdIn(std::io::StdinLock<'static>), - File(File), - PipeWire(std::process::ChildStdout), - } - let _ = out.flush(); - let mut audio_source = if cli.pipewire { + let reader: Box = if cli.pipewire { let process = Command::new("pw-record") .args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"]) .stdout(Stdio::piped()) @@ -182,14 +177,51 @@ fn main() { .expect("failed to start pw-record"); let pipe = process.stdout.expect("failed to capture pw-record stdout"); let _ = out.flush(); - AudioSource::PipeWire(pipe) + Box::new(pipe) } else if let Some(filename) = &cli.input { - let file = File::open(filename).expect("failed to open input file"); - AudioSource::File(file) + Box::new(File::open(filename).expect("failed to open input file")) } else { - AudioSource::StdIn(std::io::stdin().lock()) + Box::new(std::io::stdin()) }; + let (tx, rx) = mpsc::channel::>(); + let _audio_thread = thread::spawn(move || { + let mut reader = reader; + let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)]; + loop { + match reader.read(&mut input_buffer) { + Ok(0) => break, + Ok(n) => { + let mut samples = Vec::with_capacity(n / (num_channels * (bit_depth / 8))); + let mut cursor = std::io::Cursor::new(&input_buffer[..n]); + while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) { + let mut frame_sum = 0.0f32; + let mut valid_channels = 0u32; + for _ in 0..num_channels { + match cursor.read_i16::() { + Ok(sample) => { + frame_sum += sample as f32; + valid_channels += 1; + } + Err(_) => break, + } + } + if valid_channels > 0 { + samples.push((frame_sum / valid_channels as f32) / 32768.0); + } + } + if !samples.is_empty() && tx.send(samples).is_err() { + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(1)); + } + Err(_) => break, + } + } + }); + let max_width: u16 = 1000; let max_height: u16 = 1000; @@ -226,7 +258,8 @@ fn main() { let mut max_magnitude: f32 = 1.0; loop { - if event::poll(Duration::ZERO).unwrap_or(false) { + let time_to_next_frame = interval.saturating_sub(last_frame_time.elapsed()); + if event::poll(time_to_next_frame).unwrap_or(false) { match event::read() { Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break, Ok(Event::Key(KeyEvent { @@ -272,69 +305,17 @@ fn main() { } all_samples.clear(); - let mut buffer_is_empty = false; - loop { - let read_result = match &mut audio_source { - AudioSource::StdIn(input) => input.read(&mut input_buffer), - AudioSource::File(file) => file.read(&mut input_buffer), - AudioSource::PipeWire(pipe) => pipe.read(&mut input_buffer), - }; - - match read_result { - Ok(0) => { - buffer_is_empty = true; - break; - } - Ok(n) => { - let mut cursor = std::io::Cursor::new(&input_buffer[..n]); - while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) { - let mut frame_sum = 0.0f32; - let mut valid_channels = 0u32; - for _ in 0..num_channels { - match cursor.read_i16::() { - Ok(sample) => { - frame_sum += sample as f32; - valid_channels += 1; - } - Err(_) => break, - } - } - if valid_channels > 0 { - all_samples.push((frame_sum / valid_channels as f32) / 32768.0); - } - } - if all_samples.len() >= fft_size * 2 { - break; - } - } - Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - buffer_is_empty = true; - break; - } - Err(e) => { - let error_msg = format!("Error: {}", e); - for (i, c) in error_msg.chars().enumerate() { - if i < sc { - next_buffer[0][i] = Cell { character: c, filled: true }; - } - } - view_diff.clear(); - for y in 0..sr { - for x in 0..sc { - if current_buffer[y][x] != next_buffer[y][x] { - view_diff.add_change(y, x, next_buffer[y][x].character); - } - } - } - view_diff.apply(); - std::mem::swap(&mut current_buffer, &mut next_buffer); - let _ = out.flush(); - break; - } + match rx.try_recv() { + Ok(batch) => all_samples.extend_from_slice(&batch), + Err(_) => break, } } + // Latency guard: if the audio thread got ahead, drop old samples + if all_samples.len() > fft_size * 2 { + let excess = all_samples.len() - fft_size * 2; + all_samples.drain(..excess); + } if !all_samples.is_empty() { if all_samples.len() > fft_size { @@ -364,7 +345,7 @@ fn main() { if mapping_idx < fft_size / 2 { let magnitude = complex_buffer[mapping_idx].norm(); let freq_boost = 1.0 + (mapping_idx as f32 / (fft_size / 2) as f32) * 4.0; - let scaled = (magnitude * 80.0 * freq_boost).powf(0.5); + let scaled = (magnitude * 80.0 * freq_boost).sqrt(); freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor }; } } @@ -397,7 +378,7 @@ fn main() { std::mem::swap(&mut current_buffer, &mut next_buffer); last_frame_time = Instant::now(); let _ = out.flush(); - } else if buffer_is_empty { + } else { for i in 0..sc { last_bars[i] *= drop_factor; render_bar(&mut next_buffer, i, sr, (last_bars[i] / max_magnitude) * sr as f32);