Spectrometer didn't seem to calculate the graph correctly so i fixed
this with good logic which does work
This commit is contained in:
parent
fa020e0b36
commit
e3ff11d8a8
169
src/render.rs
169
src/render.rs
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user