use crate::error::AnalysisError;
const EPSILON: f32 = 1e-10;
pub fn detect_energy_flux_onsets(
samples: &[f32],
frame_size: usize,
hop_size: usize,
threshold_db: f32,
) -> Result<Vec<usize>, AnalysisError> {
if samples.is_empty() {
return Ok(Vec::new());
}
if frame_size == 0 {
return Err(AnalysisError::InvalidInput("Frame size must be > 0".to_string()));
}
if hop_size == 0 {
return Err(AnalysisError::InvalidInput("Hop size must be > 0".to_string()));
}
if frame_size > samples.len() {
log::warn!("Frame size ({}) larger than audio length ({}), returning empty onsets",
frame_size, samples.len());
return Ok(Vec::new());
}
log::debug!("Detecting energy flux onsets: {} samples, frame={}, hop={}, threshold={:.1} dB",
samples.len(), frame_size, hop_size, threshold_db);
let num_frames = if samples.len() >= frame_size {
(samples.len() - frame_size) / hop_size + 1
} else {
0
};
if num_frames < 2 {
return Ok(Vec::new());
}
let mut frame_energies = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * hop_size;
let end = (start + frame_size).min(samples.len());
let sum_sq: f32 = samples[start..end]
.iter()
.map(|&x| x * x)
.sum();
let rms = (sum_sq / (end - start) as f32).sqrt();
frame_energies.push(rms);
}
if frame_energies.is_empty() {
return Ok(Vec::new());
}
let mut energy_flux = Vec::with_capacity(num_frames - 1);
for i in 1..frame_energies.len() {
let flux = (frame_energies[i] - frame_energies[i - 1]).max(0.0);
energy_flux.push(flux);
}
if energy_flux.is_empty() {
return Ok(Vec::new());
}
let max_flux = energy_flux.iter().copied().fold(0.0f32, f32::max);
if max_flux <= EPSILON {
log::debug!("All energy flux values are zero or negative, no onsets detected");
return Ok(Vec::new());
}
let threshold_linear = max_flux * 10.0_f32.powf(threshold_db / 20.0);
log::debug!("Energy flux: max={:.6}, threshold={:.6} ({:.1} dB)",
max_flux, threshold_linear, threshold_db);
let mut onsets = Vec::new();
for i in 1..(energy_flux.len() - 1) {
let flux = energy_flux[i];
let prev_flux = energy_flux[i - 1];
let next_flux = energy_flux[i + 1];
if flux > threshold_linear && flux > prev_flux && flux >= next_flux {
let onset_sample = (i + 1) * hop_size;
if onset_sample < samples.len() {
onsets.push(onset_sample);
}
}
}
if energy_flux.len() > 1 && energy_flux[0] > threshold_linear && energy_flux[0] >= energy_flux[1] {
let onset_sample = hop_size;
if onset_sample < samples.len() {
onsets.push(onset_sample);
}
}
let last_idx = energy_flux.len() - 1;
if energy_flux.len() > 1 &&
energy_flux[last_idx] > threshold_linear &&
energy_flux[last_idx] > energy_flux[last_idx - 1] {
let onset_sample = (last_idx + 1) * hop_size;
if onset_sample < samples.len() {
onsets.push(onset_sample);
}
}
onsets.sort_unstable();
if !onsets.is_empty() {
let mut deduplicated = Vec::with_capacity(onsets.len());
deduplicated.push(onsets[0]);
for &onset in onsets.iter().skip(1) {
let last_onset = *deduplicated.last().unwrap();
if onset >= last_onset + hop_size / 2 {
deduplicated.push(onset);
}
}
onsets = deduplicated;
}
log::debug!("Energy flux detected {} onsets", onsets.len());
Ok(onsets)
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_kick_pattern(
duration_seconds: f32,
bpm: f32,
sample_rate: f32,
kick_duration_ms: f32,
) -> Vec<f32> {
let num_samples = (duration_seconds * sample_rate) as usize;
let mut samples = vec![0.0f32; num_samples];
let beat_interval = (60.0 / bpm * sample_rate) as usize;
let kick_samples = (kick_duration_ms / 1000.0 * sample_rate) as usize;
let mut kick_envelope = Vec::with_capacity(kick_samples);
for i in 0..kick_samples {
let t = i as f32 / kick_samples as f32;
let amplitude = (-t * 5.0).exp();
kick_envelope.push(amplitude);
}
let mut pos = 0;
while pos < num_samples {
let end = (pos + kick_samples).min(num_samples);
for (i, &) in kick_envelope[..(end - pos)].iter().enumerate() {
samples[pos + i] = amp * 0.8; }
pos += beat_interval;
}
samples
}
#[test]
fn test_energy_flux_basic() {
let mut samples = vec![0.0f32; 44100];
for i in 5000..44100 {
samples[i] = 0.5; }
let onsets = detect_energy_flux_onsets(&samples, 2048, 512, -30.0).unwrap();
assert!(!onsets.is_empty(), "Should detect at least one onset for step function");
assert!(onsets[0] >= 3000 && onsets[0] <= 8000,
"Onset should be near step at sample 5000, got {}", onsets[0]);
}
#[test]
fn test_energy_flux_kick_pattern_120_bpm() {
let sample_rate = 44100.0;
let duration = 4.0; let samples = generate_kick_pattern(duration, 120.0, sample_rate, 150.0);
let onsets = detect_energy_flux_onsets(&samples, 2048, 512, -30.0).unwrap();
assert!(onsets.len() >= 6 && onsets.len() <= 20,
"Expected 6-20 onsets for 120 BPM 4-on-floor, got {}", onsets.len());
if onsets.len() >= 2 {
let expected_interval = (60.0 / 120.0 * sample_rate) as usize; let intervals: Vec<usize> = onsets.windows(2)
.map(|w| w[1] - w[0])
.collect();
let avg_interval = intervals.iter().sum::<usize>() / intervals.len();
let tolerance = expected_interval / 2;
assert!((avg_interval as i32 - expected_interval as i32).abs() < tolerance as i32,
"Onset intervals should be ~{} samples, got average {}",
expected_interval, avg_interval);
}
}
#[test]
fn test_energy_flux_empty_samples() {
let samples = vec![];
let onsets = detect_energy_flux_onsets(&samples, 2048, 512, -20.0).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_energy_flux_silent_audio() {
let samples = vec![0.0f32; 44100];
let onsets = detect_energy_flux_onsets(&samples, 2048, 512, -20.0).unwrap();
assert!(onsets.is_empty(), "Silent audio should produce no onsets");
}
#[test]
fn test_energy_flux_too_short_audio() {
let samples = vec![0.5f32; 1000]; let onsets = detect_energy_flux_onsets(&samples, 2048, 512, -20.0).unwrap();
assert!(onsets.is_empty(), "Audio shorter than frame_size should produce no onsets");
}
#[test]
fn test_energy_flux_invalid_parameters() {
let samples = vec![0.5f32; 44100];
let result = detect_energy_flux_onsets(&samples, 0, 512, -20.0);
assert!(result.is_err());
let result = detect_energy_flux_onsets(&samples, 2048, 0, -20.0);
assert!(result.is_err());
}
#[test]
fn test_energy_flux_threshold_sensitivity() {
let samples = generate_kick_pattern(2.0, 120.0, 44100.0, 50.0);
let onsets_low = detect_energy_flux_onsets(&samples, 2048, 512, -30.0).unwrap();
let onsets_high = detect_energy_flux_onsets(&samples, 2048, 512, -10.0).unwrap();
assert!(onsets_low.len() >= onsets_high.len(),
"Lower threshold should detect more onsets: {} vs {}",
onsets_low.len(), onsets_high.len());
}
#[test]
fn test_energy_flux_performance() {
let samples = generate_kick_pattern(30.0, 120.0, 44100.0, 50.0);
let start = std::time::Instant::now();
let _onsets = detect_energy_flux_onsets(&samples, 2048, 512, -20.0).unwrap();
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() <= 200,
"Energy flux detection took {}ms, target is <=200ms (generous CI-safe margin)",
elapsed.as_millis()
);
}
}