#[derive(Debug, Clone, Copy, Default)]
pub struct BandAnalysis {
pub sub_bass: f32,
pub bass: f32,
pub mids: f32,
pub highs: f32,
pub rms: f32,
pub spectral_centroid: f32,
pub beat: bool,
}
pub fn analyze_bands(bins: &[u8], sample_rate: u32, prev_energy: f32) -> BandAnalysis {
if bins.is_empty() || sample_rate == 0 {
return BandAnalysis::default();
}
let n = bins.len();
let bin_width = sample_rate as f32 / (2.0 * n as f32);
let sub_bass_end = ((60.0 / bin_width) as usize).min(n);
let bass_start = ((60.0 / bin_width) as usize).min(n);
let bass_end = ((250.0 / bin_width) as usize).min(n);
let mid_start = ((250.0 / bin_width) as usize).min(n);
let mid_end = ((2000.0 / bin_width) as usize).min(n);
let high_start = ((2000.0 / bin_width) as usize).min(n);
let high_end = ((20000.0 / bin_width) as usize).min(n);
let sub_bass = band_energy(&bins[..sub_bass_end]);
let bass = band_energy(&bins[bass_start..bass_end]);
let mids = band_energy(&bins[mid_start..mid_end]);
let highs = band_energy(&bins[high_start..high_end]);
let total_energy: f32 = bins.iter().map(|&b| (b as f32) * (b as f32)).sum();
let rms = (total_energy / n as f32).sqrt() / 255.0;
let mut weighted_sum = 0.0f32;
let mut mag_sum = 0.0f32;
for (i, &b) in bins.iter().enumerate() {
let mag = b as f32;
weighted_sum += mag * (i as f32 * bin_width);
mag_sum += mag;
}
let centroid = if mag_sum > 0.0 {
(weighted_sum / mag_sum / (sample_rate as f32 / 2.0)).min(1.0)
} else {
0.0
};
let current_energy = rms;
let beat = current_energy > 0.15 && current_energy > prev_energy * 1.4;
BandAnalysis {
sub_bass,
bass,
mids,
highs,
rms,
spectral_centroid: centroid,
beat,
}
}
fn band_energy(bins: &[u8]) -> f32 {
if bins.is_empty() {
return 0.0;
}
let sum: f32 = bins.iter().map(|&b| (b as f32) * (b as f32)).sum();
let rms = (sum / bins.len() as f32).sqrt();
(rms / 255.0).min(1.0)
}
pub fn analyze_bands_pcm(samples: &[f32], sample_rate: u32) -> BandAnalysis {
if samples.is_empty() || sample_rate == 0 {
return BandAnalysis::default();
}
let rms: f32 = (samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt();
BandAnalysis {
sub_bass: 0.0,
bass: rms, mids: 0.0,
highs: 0.0,
rms,
spectral_centroid: 0.0,
beat: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_empty() {
let result = analyze_bands(&[], 48000, 0.0);
assert_eq!(result.rms, 0.0);
assert!(!result.beat);
}
#[test]
fn test_analyze_silence() {
let bins = vec![0u8; 4096];
let result = analyze_bands(&bins, 48000, 0.0);
assert_eq!(result.sub_bass, 0.0);
assert_eq!(result.bass, 0.0);
assert_eq!(result.mids, 0.0);
assert_eq!(result.highs, 0.0);
assert_eq!(result.rms, 0.0);
assert!(!result.beat);
}
#[test]
fn test_analyze_bass_heavy() {
let mut bins = vec![0u8; 4096];
for bin in &mut bins[10..43] {
*bin = 200;
}
let result = analyze_bands(&bins, 48000, 0.0);
assert!(result.bass > 0.5, "bass = {}", result.bass);
assert!(result.bass > result.highs, "bass should > highs");
}
#[test]
fn test_beat_detection() {
let bins = vec![128u8; 4096]; let result = analyze_bands(&bins, 48000, 0.1); assert!(result.beat, "Should detect beat on energy spike");
let result2 = analyze_bands(&bins, 48000, 0.9); assert!(!result2.beat, "Should not detect beat without spike");
}
#[test]
fn test_band_values_in_range() {
let bins: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
let result = analyze_bands(&bins, 48000, 0.0);
assert!(result.sub_bass >= 0.0 && result.sub_bass <= 1.0);
assert!(result.bass >= 0.0 && result.bass <= 1.0);
assert!(result.mids >= 0.0 && result.mids <= 1.0);
assert!(result.highs >= 0.0 && result.highs <= 1.0);
assert!(result.rms >= 0.0 && result.rms <= 1.0);
assert!(result.spectral_centroid >= 0.0 && result.spectral_centroid <= 1.0);
}
}