diff --git a/src/render.rs b/src/render.rs index 3c16cea..ceba562 100644 --- a/src/render.rs +++ b/src/render.rs @@ -109,17 +109,17 @@ 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 { - // Use shorter FFT for better temporal resolution - let fft_size = (window_size / 2).next_power_of_two().max(256); + // Use a larger FFT size for better frequency resolution, especially in the bass + let fft_size = 2048; let mut planner = FftPlanner::new(); let fft = planner.plan_fft_forward(fft_size); - // Collect audio samples for this window (sum L+R for better bass representation) + // Collect audio samples for this window let mut buffer: Vec> = (0..fft_size) .map(|i| { let sample_idx = start_sample + i; if sample_idx < audio_data.left_channel.len() { - // Sum channels instead of averaging for stronger bass representation + // Sum channels for mono analysis let sample = audio_data.left_channel[sample_idx] + audio_data.right_channel[sample_idx]; Complex::new(sample, 0.0) } else { @@ -128,34 +128,88 @@ fn compute_spectrum(audio_data: &AudioData, start_sample: usize, window_size: us }) .collect(); - // Apply Hann window to reduce spectral leakage + // Apply Hann window for (i, sample) in buffer.iter_mut().enumerate() { let window = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (fft_size - 1) as f32).cos()); sample.re *= window; sample.im *= window; } - // Apply FFT fft.process(&mut buffer); - // Compute magnitude spectrum (only positive frequencies, up to Nyquist) let nyquist_bin = fft_size / 2; - let mut spectrum: Vec = buffer[0..nyquist_bin] + // Normalize and skip DC + let spectrum: Vec = buffer[1..nyquist_bin] .iter() - .map(|c| c.norm()) + .map(|c| c.norm() / (fft_size as f32)) .collect(); - // Apply less aggressive logarithmic scaling to preserve dynamics - for mag in spectrum.iter_mut() { - // Use square root scaling instead of log for better dynamic range - *mag = (*mag).sqrt().min(1.0); - // Boost low frequencies slightly for better bass visibility - // (this is frequency-dependent scaling) - } - spectrum } +/// Draw the spectrometer bars with logarithmic frequency mapping. +fn draw_spectrometer( + buffer: &mut ImageBuffer, Vec>, + spectrum: &[f32], + x_offset: u32, + y_offset: u32, + width: u32, + height: u32, + num_bars: usize, + color: image::Rgb, + sample_rate: u32, +) { + let spacing = 1; + let bar_width = (width - (num_bars as u32 - 1) * 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); + + // Map frequencies to FFT bin indices + let bin_start = (f_start / nyquist * spectrum.len() as f32) as usize; + let bin_end = (f_end / nyquist * spectrum.len() as f32) as usize; + let bin_end = bin_end.max(bin_start + 1).min(spectrum.len()); + + // Aggregate magnitude in this frequency range + let mut magnitude = 0.0f32; + if bin_start < spectrum.len() { + magnitude = spectrum[bin_start..bin_end].iter().fold(0.0f32, |acc, &x| acc.max(x)); + } + + // 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 mut val = magnitude * freq_factor; + + // Dynamic range compression/scaling + val = (val * 20.0).sqrt().min(1.0); + + // Noise floor + if val < 0.05 { val = 0.0; } + + let bar_height = (val * height as f32) as u32; + let x = x_offset + i as u32 * (bar_width + spacing); + + for y in 0..bar_height { + let pixel_y = y_offset + height - 1 - y; + for dx in 0..bar_width { + let pixel_x = x + dx; + if pixel_x < buffer.width() && pixel_y < buffer.height() { + buffer.put_pixel(pixel_x, pixel_y, color); + } + } + } + } +} + /// Draw a single frame of the visualization. pub fn draw_frame( audio_data: &AudioData, @@ -308,36 +362,20 @@ pub fn draw_frame( let spec_x_offset = half_width; let spec_y_offset = half_height; - let window_size = 512.min(samples_per_frame); // Shorter window for better temporal resolution + let window_size = 1024.min(samples_per_frame); let spectrum = compute_spectrum(audio_data, start_sample, window_size); - // More bars with spacing for proper spectrum analyzer look - let num_bars = 32usize; // Fewer bars for better definition - let spacing = 1; // 1 pixel spacing between bars - let total_spacing = (num_bars - 1) * spacing; - let available_width = spec_width - total_spacing as u32; - let bar_width = (available_width / num_bars as u32).max(1); - - for i in 0..num_bars { - // Map spectrum bins to bars (group multiple bins per bar) - let bin_start = (i * spectrum.len() / num_bars).min(spectrum.len()); - let bin_end = ((i + 1) * spectrum.len() / num_bars).min(spectrum.len()); - let magnitude = spectrum[bin_start..bin_end].iter().fold(0.0f32, |acc, &x| acc.max(x)); - - let bar_height = (magnitude * spec_height as f32 * 0.9) as u32; // Scale to 90% of quadrant height - let x = spec_x_offset + (i as u32) * (bar_width + spacing as u32); - - // Draw vertical bar from bottom up within the quadrant - for y in 0..bar_height { - let pixel_y = spec_y_offset + spec_height - 1 - y; // Bottom to top in quadrant - for dx in 0..bar_width { - let pixel_x = x + dx; - if pixel_x < width && pixel_y < height && pixel_x >= spec_x_offset { - buffer.put_pixel(pixel_x, pixel_y, options.left_color); - } - } - } - } + draw_spectrometer( + &mut buffer, + &spectrum, + spec_x_offset, + spec_y_offset, + spec_width, + spec_height, + 32, + options.left_color, + audio_data.sample_rate, + ); // Draw grid lines separating quadrants for x in 0..width { @@ -348,37 +386,20 @@ pub fn draw_frame( } } RenderMode::Spectrometer => { - // Use a window of samples for FFT - let window_size = 512.min(samples_per_frame); // Shorter window for better temporal resolution + let window_size = 1024.min(samples_per_frame); let spectrum = compute_spectrum(audio_data, start_sample, window_size); - // Spectrum analyzer style with individual bars and spacing - let num_bars = 64usize; // Good number for full screen spectrum analyzer - let spacing = 2; // 2 pixel spacing between bars for classic look - let total_spacing = (num_bars - 1) * spacing; - let available_width = width - total_spacing as u32; - let bar_width = (available_width / num_bars as u32).max(1); - - for i in 0..num_bars { - // Map spectrum bins to bars (group multiple bins per bar) - let bin_start = (i * spectrum.len() / num_bars).min(spectrum.len()); - let bin_end = ((i + 1) * spectrum.len() / num_bars).min(spectrum.len()); - let magnitude = spectrum[bin_start..bin_end].iter().fold(0.0f32, |acc, &x| acc.max(x)); - - let bar_height = (magnitude * height as f32 * 0.95) as u32; // Scale to 95% of screen height - let x = (i as u32) * (bar_width + spacing as u32); - - // Draw vertical bar from bottom up - for y in 0..bar_height { - let pixel_y = height - 1 - y; // Bottom to top - for dx in 0..bar_width { - let pixel_x = x + dx; - if pixel_x < width && pixel_y < height { - buffer.put_pixel(pixel_x, pixel_y, options.left_color); - } - } - } - } + draw_spectrometer( + &mut buffer, + &spectrum, + 0, + 0, + width, + height, + 64, + options.left_color, + audio_data.sample_rate, + ); } }