use crate::error::AnalysisError;
pub fn detect_hfc_onsets(
fft_magnitudes: &[Vec<f32>],
sample_rate: u32,
threshold_percentile: f32,
) -> Result<Vec<usize>, AnalysisError> {
if fft_magnitudes.is_empty() {
return Ok(Vec::new());
}
if sample_rate == 0 {
return Err(AnalysisError::InvalidInput("Sample rate must be > 0".to_string()));
}
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 HFC onsets: {} frames, {} bins per frame, sample_rate={} Hz, threshold_percentile={:.2}",
fft_magnitudes.len(), n_bins, sample_rate, threshold_percentile);
let mut hfc_values = Vec::with_capacity(fft_magnitudes.len());
for frame in fft_magnitudes {
let mut hfc = 0.0f32;
for (bin_idx, &magnitude) in frame.iter().enumerate() {
hfc += bin_idx as f32 * magnitude * magnitude;
}
hfc_values.push(hfc);
}
if hfc_values.is_empty() {
return Ok(Vec::new());
}
let mut hfc_flux = Vec::with_capacity(hfc_values.len() - 1);
for i in 1..hfc_values.len() {
let flux = (hfc_values[i] - hfc_values[i - 1]).max(0.0);
hfc_flux.push(flux);
}
if hfc_flux.is_empty() {
return Ok(Vec::new());
}
let mut sorted_flux = hfc_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!("HFC 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..(hfc_flux.len() - 1) {
let flux = hfc_flux[i];
let prev_flux = hfc_flux[i - 1];
let next_flux = hfc_flux[i + 1];
if flux > threshold && flux > prev_flux && flux >= next_flux {
onsets.push(i + 1);
}
}
if hfc_flux.len() > 1 &&
hfc_flux[0] > threshold &&
hfc_flux[0] >= hfc_flux[1] {
onsets.push(1);
}
let last_idx = hfc_flux.len() - 1;
if hfc_flux.len() > 1 &&
hfc_flux[last_idx] > threshold &&
hfc_flux[last_idx] > hfc_flux[last_idx - 1] {
onsets.push(hfc_flux.len());
}
onsets.sort_unstable();
onsets.dedup();
log::debug!("HFC detected {} onsets", onsets.len());
Ok(onsets)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hfc_basic() {
let mut spectrogram = vec![vec![0.01f32; 1024]; 10];
for i in 0..5 {
for bin in 0..100 {
spectrogram[i][bin] = 0.5f32;
}
}
for bin in 800..1024 {
spectrogram[5][bin] = 1.0f32;
}
for i in 6..10 {
for bin in 0..100 {
spectrogram[i][bin] = 0.5f32;
}
}
let onsets = detect_hfc_onsets(&spectrogram, 44100, 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_hfc_empty() {
let spectrogram = vec![];
let onsets = detect_hfc_onsets(&spectrogram, 44100, 0.8).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_hfc_single_frame() {
let spectrogram = vec![vec![0.5f32; 1024]];
let onsets = detect_hfc_onsets(&spectrogram, 44100, 0.8).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_hfc_invalid_percentile() {
let spectrogram = vec![vec![0.5f32; 1024]; 10];
let result = detect_hfc_onsets(&spectrogram, 44100, -0.1);
assert!(result.is_err());
let result = detect_hfc_onsets(&spectrogram, 44100, 1.5);
assert!(result.is_err());
}
#[test]
fn test_hfc_zero_sample_rate() {
let spectrogram = vec![vec![0.5f32; 1024]; 10];
let result = detect_hfc_onsets(&spectrogram, 0, 0.8);
assert!(result.is_err());
}
#[test]
fn test_hfc_inconsistent_lengths() {
let mut spectrogram = vec![vec![0.5f32; 1024]; 10];
spectrogram[5] = vec![0.5f32; 512];
let result = detect_hfc_onsets(&spectrogram, 44100, 0.8);
assert!(result.is_err());
}
#[test]
fn test_hfc_all_zeros() {
let spectrogram = vec![vec![0.0f32; 1024]; 10];
let onsets = detect_hfc_onsets(&spectrogram, 44100, 0.8).unwrap();
assert!(onsets.is_empty());
}
#[test]
fn test_hfc_threshold_sensitivity() {
let mut spectrogram = vec![vec![0.01f32; 1024]; 20];
for i in 0..20 {
let amplitude = 0.1 + (i as f32 / 20.0) * 0.9;
for bin in 800..1024 {
spectrogram[i][bin] = amplitude;
}
}
let onsets_low = detect_hfc_onsets(&spectrogram, 44100, 0.5).unwrap();
let onsets_high = detect_hfc_onsets(&spectrogram, 44100, 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_hfc_frequency_weighting() {
let mut spectrogram = vec![vec![0.0f32; 1024]; 2];
for bin in 0..100 {
spectrogram[0][bin] = 1.0f32;
}
for bin in 900..1024 {
spectrogram[1][bin] = 1.0f32;
}
let _onsets = detect_hfc_onsets(&spectrogram, 44100, 0.5).unwrap();
assert!(true, "HFC frequency weighting test completed");
}
#[test]
fn test_hfc_multiple_changes() {
let mut spectrogram = vec![vec![0.01f32; 1024]; 20];
for frame_idx in [5, 10, 15] {
for bin in 800..1024 {
spectrogram[frame_idx][bin] = 1.0f32;
}
}
let onsets = detect_hfc_onsets(&spectrogram, 44100, 0.3).unwrap();
assert!(onsets.len() >= 2, "Should detect multiple onsets, got {}: {:?}", onsets.len(), onsets);
}
}