diff --git a/Cargo.lock b/Cargo.lock index 2d8255e..0ce48ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,6 +704,7 @@ name = "oscilloscope-video-gen" version = "1.0.0" dependencies = [ "anyhow", + "bytemuck", "clap", "hound", "image", diff --git a/Cargo.toml b/Cargo.toml index 945581c..18cbc9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ anyhow = "1.0" # FFT processing for spectrometer rustfft = "6.1" +# Efficient byte casting +bytemuck = "1.24" + [profile.release] opt-level = 3 lto = true diff --git a/src/audio.rs b/src/audio.rs index e2d5948..463d76b 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -3,6 +3,7 @@ //! Handles reading and decoding WAV files into normalized sample data. use anyhow::{anyhow, Context, Result}; +use bytemuck; use std::path::Path; /// Normalized audio sample data. @@ -49,12 +50,11 @@ impl AudioData { let mut left_channel = Vec::with_capacity(total_samples); let mut right_channel = Vec::with_capacity(total_samples); - for i in 0..total_samples { - let offset = i * 2 * num_channels; - - let left_val = i16::from_le_bytes([pcm_data[offset], pcm_data[offset + 1]]); - let right_val = i16::from_le_bytes([pcm_data[offset + 2], pcm_data[offset + 3]]); - + // Convert PCM data to f32 samples efficiently + let samples: &[i16] = bytemuck::cast_slice(&pcm_data); + for chunk in samples.chunks_exact(num_channels) { + let left_val = chunk[0]; + let right_val = chunk[1]; left_channel.push(left_val as f32 / 32768.0); right_channel.push(right_val as f32 / 32768.0); } diff --git a/src/render.rs b/src/render.rs index ceba562..d16791a 100644 --- a/src/render.rs +++ b/src/render.rs @@ -78,12 +78,7 @@ pub fn draw_line( fn draw_graticule( buffer: &mut ImageBuffer, Vec>, primary_color: image::Rgb, - show_grid: bool, ) { - if !show_grid { - return; - } - let (width, height) = buffer.dimensions(); for x in 0..width { @@ -108,7 +103,7 @@ pub fn parse_rgb_hex(hex: &str) -> Result> { } /// Compute frequency spectrum from audio samples using FFT. -fn compute_spectrum(audio_data: &AudioData, start_sample: usize, window_size: usize) -> Vec { +fn compute_spectrum(audio_data: &AudioData, start_sample: usize) -> Vec { // Use a larger FFT size for better frequency resolution, especially in the bass let fft_size = 2048; let mut planner = FftPlanner::new(); @@ -159,19 +154,23 @@ fn draw_spectrometer( color: image::Rgb, sample_rate: u32, ) { - let spacing = 1; - let bar_width = (width - (num_bars as u32 - 1) * spacing) / num_bars as u32; + // Constants for logarithmic frequency mapping + const MIN_FREQ: f32 = 20.0; + const MAX_FREQ: f32 = 20000.0; + const FREQ_BOOST_FACTOR: f32 = 5.0; + const DYNAMIC_RANGE_SCALE: f32 = 20.0; + const NOISE_FLOOR: f32 = 0.05; + const BAR_SPACING: u32 = 1; + + let bar_width = (width - (num_bars as u32 - 1) * BAR_SPACING) / num_bars as u32; let bar_width = bar_width.max(1); - // Logarithmic mapping parameters - let min_freq = 20.0f32; - let max_freq = 20000.0f32; let nyquist = sample_rate as f32 / 2.0; for i in 0..num_bars { // Calculate frequency range for this bar (logarithmic) - let f_start = min_freq * (max_freq / min_freq).powf(i as f32 / num_bars as f32); - let f_end = min_freq * (max_freq / min_freq).powf((i + 1) as f32 / num_bars as f32); + let f_start = MIN_FREQ * (MAX_FREQ / MIN_FREQ).powf(i as f32 / num_bars as f32); + let f_end = MIN_FREQ * (MAX_FREQ / MIN_FREQ).powf((i + 1) as f32 / num_bars as f32); // Map frequencies to FFT bin indices let bin_start = (f_start / nyquist * spectrum.len() as f32) as usize; @@ -185,18 +184,17 @@ fn draw_spectrometer( } // Apply frequency-dependent boost (higher frequencies are naturally quieter) - // Boost highs by adding a linear factor based on frequency - let freq_factor = 1.0 + (f_start / max_freq) * 5.0; + let freq_factor = 1.0 + (f_start / MAX_FREQ) * FREQ_BOOST_FACTOR; let mut val = magnitude * freq_factor; // Dynamic range compression/scaling - val = (val * 20.0).sqrt().min(1.0); - + val = (val * DYNAMIC_RANGE_SCALE).sqrt().min(1.0); + // Noise floor - if val < 0.05 { val = 0.0; } + if val < NOISE_FLOOR { val = 0.0; } let bar_height = (val * height as f32) as u32; - let x = x_offset + i as u32 * (bar_width + spacing); + let x = x_offset + i as u32 * (bar_width + BAR_SPACING); for y in 0..bar_height { let pixel_y = y_offset + height - 1 - y; @@ -226,7 +224,7 @@ pub fn draw_frame( } if options.show_grid { - draw_graticule(&mut buffer, options.left_color, true); + draw_graticule(&mut buffer, options.left_color); } let end_sample = std::cmp::min(start_sample + samples_per_frame, audio_data.left_channel.len()); @@ -362,8 +360,7 @@ pub fn draw_frame( let spec_x_offset = half_width; let spec_y_offset = half_height; - let window_size = 1024.min(samples_per_frame); - let spectrum = compute_spectrum(audio_data, start_sample, window_size); + let spectrum = compute_spectrum(audio_data, start_sample); draw_spectrometer( &mut buffer, @@ -386,8 +383,7 @@ pub fn draw_frame( } } RenderMode::Spectrometer => { - let window_size = 1024.min(samples_per_frame); - let spectrum = compute_spectrum(audio_data, start_sample, window_size); + let spectrum = compute_spectrum(audio_data, start_sample); draw_spectrometer( &mut buffer,