use crate::dsp::db::power_to_db;
use crate::matrix::Matrix;
pub fn spectral_contrast(
s: &Matrix,
freq: &[f64],
fmin: f64,
n_bands: usize,
quantile: f64,
linear: bool,
) -> Matrix {
assert_eq!(
s.rows(),
freq.len(),
"freq length {} != spectrogram rows {}",
freq.len(),
s.rows()
);
let n_frames = s.cols();
let mut octa = vec![0.0; n_bands + 2];
for i in 0..=n_bands {
octa[i + 1] = fmin * 2.0_f64.powi(i as i32);
}
let mut valley = Matrix::zeros(n_bands + 1, n_frames);
let mut peak = Matrix::zeros(n_bands + 1, n_frames);
for k in 0..=n_bands {
let f_low = octa[k];
let f_high = octa[k + 1];
let mut current_band: Vec<bool> = freq.iter().map(|&f| f >= f_low && f <= f_high).collect();
let first_idx = current_band.iter().position(|&b| b);
let last_idx = current_band.iter().rposition(|&b| b);
if let (Some(first), Some(_last)) = (first_idx, last_idx) {
if k > 0 && first > 0 {
current_band[first - 1] = true;
}
if k == n_bands
&& let Some(last) = last_idx
{
for val in &mut current_band[(last + 1)..] {
*val = true;
}
}
}
let active_indices: Vec<usize> = current_band
.iter()
.enumerate()
.filter(|&(_, b)| *b)
.map(|(i, _)| i)
.collect();
let sub_indices: Vec<usize> = if k < n_bands && active_indices.len() > 1 {
active_indices[..active_indices.len() - 1].to_vec()
} else {
active_indices.clone()
};
if sub_indices.is_empty() {
continue;
}
let n_active: usize = current_band.iter().filter(|&&b| b).count();
let idx = ((quantile * n_active as f64).round() as usize).max(1);
for t in 0..n_frames {
let mut band_vals: Vec<f64> = sub_indices.iter().map(|&i| s.get(i, t)).collect();
band_vals.sort_by(|a, b| a.total_cmp(b));
let n = band_vals.len();
let take = idx.min(n);
let valley_val: f64 = band_vals[..take].iter().sum::<f64>() / take as f64;
let peak_val: f64 = band_vals[n - take..].iter().sum::<f64>() / take as f64;
*valley.get_mut(k, t) = valley_val;
*peak.get_mut(k, t) = peak_val;
}
}
if linear {
peak.zip_map(&valley, |p, v| p - v)
} else {
let peak_db = power_to_db(peak.as_slice(), 1.0, 1e-10, Some(80.0));
let valley_db = power_to_db(valley.as_slice(), 1.0, 1e-10, Some(80.0));
let contrast_data: Vec<f64> = peak_db
.iter()
.zip(valley_db.iter())
.map(|(&p, &v)| p - v)
.collect();
Matrix::from_vec(contrast_data, n_bands + 1, n_frames)
}
}