#[must_use]
pub fn spectral_flux(current: &[f32], previous: &[f32]) -> f32 {
if current.is_empty() || previous.is_empty() || current.len() != previous.len() {
return 0.0;
}
current
.iter()
.zip(previous.iter())
.map(|(&c, &p)| {
let diff = c - p;
diff * diff
})
.sum()
}
#[must_use]
pub fn spectral_flux_hwr(current: &[f32], previous: &[f32]) -> f32 {
if current.is_empty() || previous.is_empty() || current.len() != previous.len() {
return 0.0;
}
current
.iter()
.zip(previous.iter())
.map(|(&c, &p)| {
let diff = (c - p).max(0.0);
diff * diff
})
.sum()
}
#[must_use]
pub fn spectral_flux_normalised(current: &[f32], previous: &[f32]) -> f32 {
if current.is_empty() || previous.is_empty() || current.len() != previous.len() {
return 0.0;
}
let raw = spectral_flux(current, previous);
raw / current.len() as f32
}
#[must_use]
pub fn spectral_flux_track(spectrogram: &[Vec<f32>], normalise: bool) -> Vec<f32> {
if spectrogram.len() < 2 {
return Vec::new();
}
spectrogram
.windows(2)
.map(|w| {
if normalise {
spectral_flux_normalised(&w[1], &w[0])
} else {
spectral_flux(&w[1], &w[0])
}
})
.collect()
}
#[must_use]
pub fn spectral_flux_hwr_track(spectrogram: &[Vec<f32>]) -> Vec<f32> {
if spectrogram.len() < 2 {
return Vec::new();
}
spectrogram
.windows(2)
.map(|w| spectral_flux_hwr(&w[1], &w[0]))
.collect()
}
#[must_use]
pub fn detect_onsets_from_flux(flux: &[f32], threshold: f32) -> Vec<usize> {
flux.iter()
.enumerate()
.filter_map(|(i, &f)| if f > threshold { Some(i + 1) } else { None })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flux_identical_frames() {
let frame = vec![1.0, 2.0, 3.0, 4.0];
assert_eq!(spectral_flux(&frame, &frame), 0.0);
}
#[test]
fn test_flux_empty() {
let empty: Vec<f32> = vec![];
assert_eq!(spectral_flux(&empty, &empty), 0.0);
}
#[test]
fn test_flux_different_lengths() {
let a = vec![1.0, 2.0];
let b = vec![1.0];
assert_eq!(spectral_flux(&a, &b), 0.0);
}
#[test]
fn test_flux_unit_step() {
let prev = vec![0.0, 0.0, 0.0, 0.0];
let curr = vec![1.0, 0.0, 0.0, 0.0];
assert!((spectral_flux(&curr, &prev) - 1.0).abs() < 1e-6);
}
#[test]
fn test_flux_all_bins_change() {
let prev = vec![0.0; 4];
let curr = vec![1.0; 4];
assert!((spectral_flux(&curr, &prev) - 4.0).abs() < 1e-6);
}
#[test]
fn test_flux_symmetry() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![3.0, 1.0, 2.0];
assert!((spectral_flux(&a, &b) - spectral_flux(&b, &a)).abs() < 1e-6);
}
#[test]
fn test_flux_hwr_ignores_decreases() {
let prev = vec![1.0, 0.0, 0.0];
let curr = vec![0.0, 0.0, 0.0]; assert_eq!(spectral_flux_hwr(&curr, &prev), 0.0);
}
#[test]
fn test_flux_hwr_counts_increases() {
let prev = vec![0.0, 0.0, 0.0];
let curr = vec![1.0, 0.0, 2.0];
assert!((spectral_flux_hwr(&curr, &prev) - 5.0).abs() < 1e-6);
}
#[test]
fn test_flux_hwr_mixed() {
let prev = vec![1.0, 0.0, 2.0];
let curr = vec![3.0, 1.0, 0.0]; assert!((spectral_flux_hwr(&curr, &prev) - 5.0).abs() < 1e-6);
}
#[test]
fn test_flux_normalised_scales_with_bins() {
let prev4 = vec![0.0; 4];
let curr4 = vec![1.0; 4];
let prev8 = vec![0.0; 8];
let curr8 = vec![1.0; 8];
let n4 = spectral_flux_normalised(&curr4, &prev4);
let n8 = spectral_flux_normalised(&curr8, &prev8);
assert!((n4 - 1.0).abs() < 1e-6);
assert!((n8 - 1.0).abs() < 1e-6);
}
#[test]
fn test_flux_track_length() {
let spectrogram: Vec<Vec<f32>> = (0..10).map(|_| vec![1.0_f32; 64]).collect();
let track = spectral_flux_track(&spectrogram, false);
assert_eq!(track.len(), 9);
}
#[test]
fn test_flux_track_empty_spectrogram() {
let spectrogram: Vec<Vec<f32>> = vec![];
let track = spectral_flux_track(&spectrogram, false);
assert!(track.is_empty());
}
#[test]
fn test_flux_track_single_frame() {
let spectrogram: Vec<Vec<f32>> = vec![vec![1.0; 64]];
let track = spectral_flux_track(&spectrogram, false);
assert!(track.is_empty());
}
#[test]
fn test_flux_track_increasing_energy() {
let spectrogram: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32 * 0.5; 64]).collect();
let track = spectral_flux_track(&spectrogram, false);
assert_eq!(track.len(), 4);
for &f in &track {
assert!(f > 0.0, "Expected non-zero flux, got {f}");
}
}
#[test]
fn test_flux_track_constant_frames() {
let spectrogram: Vec<Vec<f32>> = (0..5).map(|_| vec![1.0_f32; 32]).collect();
let track = spectral_flux_track(&spectrogram, false);
for &f in &track {
assert!((f).abs() < 1e-6);
}
}
#[test]
fn test_flux_hwr_track_length() {
let spectrogram: Vec<Vec<f32>> = (0..6).map(|_| vec![1.0_f32; 32]).collect();
let track = spectral_flux_hwr_track(&spectrogram);
assert_eq!(track.len(), 5);
}
#[test]
fn test_detect_onsets_basic() {
let flux = vec![0.1, 5.0, 0.2, 0.1, 8.0, 0.3];
let onsets = detect_onsets_from_flux(&flux, 1.0);
assert_eq!(onsets, vec![2, 5]); }
#[test]
fn test_detect_onsets_no_onsets() {
let flux = vec![0.1, 0.2, 0.3];
let onsets = detect_onsets_from_flux(&flux, 1.0);
assert!(onsets.is_empty());
}
#[test]
fn test_detect_onsets_all_above_threshold() {
let flux = vec![2.0, 3.0, 4.0];
let onsets = detect_onsets_from_flux(&flux, 1.0);
assert_eq!(onsets, vec![1, 2, 3]);
}
}