use crate::error::AnalysisError;
const EPSILON: f32 = 1e-10;
pub fn detect_spectral_flux_onsets(
fft_magnitudes: &[Vec<f32>],
threshold_percentile: f32,
) -> Result<Vec<usize>, AnalysisError> {
if fft_magnitudes.is_empty() {
return Ok(Vec::new());
}
if threshold_percentile < 0.0 || threshold_percentile > 1.0 {
return Err(AnalysisError::InvalidInput(
format!("Threshold percentile must be in [0, 1], got {}", threshold_percentile)
));
}
let n_bins = fft_magnitudes[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty magnitude frames".to_string()));
}
for (i, frame) in fft_magnitudes.iter().enumerate() {
if frame.len() != n_bins {
return Err(AnalysisError::InvalidInput(
format!("Inconsistent frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins, i, frame.len())
));
}
}
if fft_magnitudes.len() < 2 {
return Ok(Vec::new());
}
log::debug!("Detecting spectral flux onsets: {} frames, {} bins per frame, threshold_percentile={:.2}",
fft_magnitudes.len(), n_bins, threshold_percentile);
let mut normalized: Vec<Vec<f32>> = Vec::with_capacity(fft_magnitudes.len());
for frame in fft_magnitudes {
let max_mag = frame.iter().copied().fold(0.0f32, f32::max);
if max_mag > EPSILON {
let normalized_frame: Vec<f32> = frame.iter()
.map(|&x| x / max_mag)
.collect();
normalized.push(normalized_frame);
} else {
normalized.push(vec![0.0f32; n_bins]);
}
}
let mut spectral_flux = Vec::with_capacity(fft_magnitudes.len() - 1);
for i in 1..normalized.len() {
let prev_frame = &normalized[i - 1];
let curr_frame = &normalized[i];
let sum_sq_diff: f32 = prev_frame.iter()
.zip(curr_frame.iter())
.map(|(&prev, &curr)| {
let diff = curr - prev;
diff.max(0.0)
})
.map(|x| x * x)
.sum();
let l2_distance = sum_sq_diff.sqrt();
spectral_flux.push(l2_distance);
}
if spectral_flux.is_empty() {
return Ok(Vec::new());
}
let mut sorted_flux = spectral_flux.clone();
sorted_flux.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let threshold_idx = ((sorted_flux.len() as f32) * threshold_percentile) as usize;
let threshold_idx = threshold_idx.min(sorted_flux.len() - 1);
let threshold = sorted_flux[threshold_idx];
log::debug!("Spectral flux: max={:.6}, threshold={:.6} ({}th percentile)",
sorted_flux.last().unwrap_or(&0.0), threshold, threshold_percentile);
let mut onsets = Vec::new();
for i in 1..(spectral_flux.len() - 1) {
let flux = spectral_flux[i];
let prev_flux = spectral_flux[i - 1];
let next_flux = spectral_flux[i + 1];
if flux > threshold && flux > prev_flux && flux >= next_flux {
onsets.push(i + 1);
}
}
if spectral_flux.len() > 1 &&
spectral_flux[0] > threshold &&
spectral_flux[0] >= spectral_flux[1] {
onsets.push(1);
}
let last_idx = spectral_flux.len() - 1;
if spectral_flux.len() > 1 &&
spectral_flux[last_idx] > threshold &&
spectral_flux[last_idx] > spectral_flux[last_idx - 1] {
onsets.push(spectral_flux.len());
}
onsets.sort_unstable();
onsets.dedup();
log::debug!("Spectral flux detected {} onsets", onsets.len());
Ok(onsets)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_flux_basic() {
let mut spectrogram = vec![vec![0.01f32; 1024]; 10];
for i in 0..5 {
for bin in 0..256 {
spectrogram[i][bin] = 1.0f32;
}
}
for bin in 768..1024 {
spectrogram[5][bin] = 1.0f32;
}
for i in 6..10 {
for bin in 0..256 {
spectrogram[i][bin] = 1.0f32;
}
}
let onsets = detect_spectral_flux_onsets(&spectrogram, 0.3).unwrap();
assert!(!onsets.is_empty(), "Should detect at least one onset");
assert!(onsets.iter().any(|&f| f >= 4 && f <= 7),
"Onset should be near frame 5-6, got {:?}", onsets);
}
#[test]
fn test_spectral_flux_empty() {
let spectrogram = vec![];
let onsets = detect_spectral_flux_onsets(&spectrogram, 0.8).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_spectral_flux_single_frame() {
let spectrogram = vec![vec![0.5f32; 1024]];
let onsets = detect_spectral_flux_onsets(&spectrogram, 0.8).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_spectral_flux_invalid_percentile() {
let spectrogram = vec![vec![0.5f32; 1024]; 10];
let result = detect_spectral_flux_onsets(&spectrogram, -0.1);
assert!(result.is_err());
let result = detect_spectral_flux_onsets(&spectrogram, 1.5);
assert!(result.is_err());
}
#[test]
fn test_spectral_flux_inconsistent_lengths() {
let mut spectrogram = vec![vec![0.5f32; 1024]; 10];
spectrogram[5] = vec![0.5f32; 512];
let result = detect_spectral_flux_onsets(&spectrogram, 0.8);
assert!(result.is_err());
}
#[test]
fn test_spectral_flux_all_zeros() {
let spectrogram = vec![vec![0.0f32; 1024]; 10];
let onsets = detect_spectral_flux_onsets(&spectrogram, 0.8).unwrap();
assert!(onsets.is_empty() || onsets.len() < 3);
}
#[test]
fn test_spectral_flux_threshold_sensitivity() {
let mut spectrogram = vec![vec![0.1f32; 1024]; 20];
for i in 0..20 {
let amplitude = 0.1 + (i as f32 / 20.0) * 0.9;
for bin in 0..1024 {
spectrogram[i][bin] = amplitude;
}
}
let onsets_low = detect_spectral_flux_onsets(&spectrogram, 0.5).unwrap();
let onsets_high = detect_spectral_flux_onsets(&spectrogram, 0.9).unwrap();
assert!(onsets_low.len() >= onsets_high.len(),
"Lower threshold should detect more onsets: {} vs {}",
onsets_low.len(), onsets_high.len());
}
#[test]
fn test_spectral_flux_normalization() {
let mut spectrogram = vec![vec![0.5f32; 1024]; 2];
spectrogram[1] = vec![1.0f32; 1024];
let _onsets = detect_spectral_flux_onsets(&spectrogram, 0.5).unwrap();
}
#[test]
fn test_spectral_flux_multiple_changes() {
let mut spectrogram = vec![vec![0.1f32; 1024]; 20];
for bin in 0..512 {
spectrogram[5][bin] = 1.0f32;
}
for bin in 512..1024 {
spectrogram[10][bin] = 1.0f32;
}
for bin in 256..768 {
spectrogram[15][bin] = 1.0f32;
}
let onsets = detect_spectral_flux_onsets(&spectrogram, 0.3).unwrap();
assert!(onsets.len() >= 2, "Should detect multiple onsets, got {}: {:?}", onsets.len(), onsets);
}
}