Spectrometer didn't seem to calculate the graph correctly so i fixed

this with good logic which does work
This commit is contained in:
JorySeverijnse 2026-01-18 19:19:46 +01:00
parent fa020e0b36
commit e3ff11d8a8

View File

@ -109,17 +109,17 @@ pub fn parse_rgb_hex(hex: &str) -> Result<image::Rgb<u8>> {
/// Compute frequency spectrum from audio samples using FFT. /// Compute frequency spectrum from audio samples using FFT.
fn compute_spectrum(audio_data: &AudioData, start_sample: usize, window_size: usize) -> Vec<f32> { fn compute_spectrum(audio_data: &AudioData, start_sample: usize, window_size: usize) -> Vec<f32> {
// Use shorter FFT for better temporal resolution // Use a larger FFT size for better frequency resolution, especially in the bass
let fft_size = (window_size / 2).next_power_of_two().max(256); let fft_size = 2048;
let mut planner = FftPlanner::new(); let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(fft_size); 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<Complex<f32>> = (0..fft_size) let mut buffer: Vec<Complex<f32>> = (0..fft_size)
.map(|i| { .map(|i| {
let sample_idx = start_sample + i; let sample_idx = start_sample + i;
if sample_idx < audio_data.left_channel.len() { 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]; let sample = audio_data.left_channel[sample_idx] + audio_data.right_channel[sample_idx];
Complex::new(sample, 0.0) Complex::new(sample, 0.0)
} else { } else {
@ -128,34 +128,88 @@ fn compute_spectrum(audio_data: &AudioData, start_sample: usize, window_size: us
}) })
.collect(); .collect();
// Apply Hann window to reduce spectral leakage // Apply Hann window
for (i, sample) in buffer.iter_mut().enumerate() { 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()); 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.re *= window;
sample.im *= window; sample.im *= window;
} }
// Apply FFT
fft.process(&mut buffer); fft.process(&mut buffer);
// Compute magnitude spectrum (only positive frequencies, up to Nyquist)
let nyquist_bin = fft_size / 2; let nyquist_bin = fft_size / 2;
let mut spectrum: Vec<f32> = buffer[0..nyquist_bin] // Normalize and skip DC
let spectrum: Vec<f32> = buffer[1..nyquist_bin]
.iter() .iter()
.map(|c| c.norm()) .map(|c| c.norm() / (fft_size as f32))
.collect(); .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 spectrum
} }
/// Draw the spectrometer bars with logarithmic frequency mapping.
fn draw_spectrometer(
buffer: &mut ImageBuffer<image::Rgb<u8>, Vec<u8>>,
spectrum: &[f32],
x_offset: u32,
y_offset: u32,
width: u32,
height: u32,
num_bars: usize,
color: image::Rgb<u8>,
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. /// Draw a single frame of the visualization.
pub fn draw_frame( pub fn draw_frame(
audio_data: &AudioData, audio_data: &AudioData,
@ -308,36 +362,20 @@ pub fn draw_frame(
let spec_x_offset = half_width; let spec_x_offset = half_width;
let spec_y_offset = half_height; 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); let spectrum = compute_spectrum(audio_data, start_sample, window_size);
// More bars with spacing for proper spectrum analyzer look draw_spectrometer(
let num_bars = 32usize; // Fewer bars for better definition &mut buffer,
let spacing = 1; // 1 pixel spacing between bars &spectrum,
let total_spacing = (num_bars - 1) * spacing; spec_x_offset,
let available_width = spec_width - total_spacing as u32; spec_y_offset,
let bar_width = (available_width / num_bars as u32).max(1); spec_width,
spec_height,
for i in 0..num_bars { 32,
// Map spectrum bins to bars (group multiple bins per bar) options.left_color,
let bin_start = (i * spectrum.len() / num_bars).min(spectrum.len()); audio_data.sample_rate,
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 grid lines separating quadrants // Draw grid lines separating quadrants
for x in 0..width { for x in 0..width {
@ -348,37 +386,20 @@ pub fn draw_frame(
} }
} }
RenderMode::Spectrometer => { RenderMode::Spectrometer => {
// Use a window of samples for FFT let window_size = 1024.min(samples_per_frame);
let window_size = 512.min(samples_per_frame); // Shorter window for better temporal resolution
let spectrum = compute_spectrum(audio_data, start_sample, window_size); let spectrum = compute_spectrum(audio_data, start_sample, window_size);
// Spectrum analyzer style with individual bars and spacing draw_spectrometer(
let num_bars = 64usize; // Good number for full screen spectrum analyzer &mut buffer,
let spacing = 2; // 2 pixel spacing between bars for classic look &spectrum,
let total_spacing = (num_bars - 1) * spacing; 0,
let available_width = width - total_spacing as u32; 0,
let bar_width = (available_width / num_bars as u32).max(1); width,
height,
for i in 0..num_bars { 64,
// Map spectrum bins to bars (group multiple bins per bar) options.left_color,
let bin_start = (i * spectrum.len() / num_bars).min(spectrum.len()); audio_data.sample_rate,
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);
}
}
}
}
} }
} }