use crate::dsp::db::power_to_db;
use crate::matrix::Matrix;
use crate::stft::{MelSpectrogramParams, mel_spectrogram};
pub use crate::matrix::Aggregate;
pub struct OnsetStrengthParams {
pub sr: f64,
pub n_fft: usize,
pub hop_length: usize,
pub lag: usize,
pub max_size: usize,
pub detrend: bool,
pub center: bool,
pub aggregate: Aggregate,
}
impl OnsetStrengthParams {
pub fn new(sr: f64) -> Self {
Self {
sr,
n_fft: 2048,
hop_length: 512,
lag: 1,
max_size: 1,
detrend: false,
center: true,
aggregate: Aggregate::Mean,
}
}
}
fn median(values: &mut [f64]) -> f64 {
let n = values.len();
if n == 0 {
return 0.0;
}
values.sort_by(|a, b| a.total_cmp(b));
if n.is_multiple_of(2) {
(values[n / 2 - 1] + values[n / 2]) / 2.0
} else {
values[n / 2]
}
}
pub fn onset_strength(y: &[f64], params: &OnsetStrengthParams) -> Vec<f64> {
let sr = params.sr;
let n_fft = params.n_fft;
let hop_length = params.hop_length;
let lag = params.lag;
let mut mel_params = MelSpectrogramParams::new(sr);
mel_params.stft.n_fft = n_fft;
mel_params.stft.hop_length = hop_length;
mel_params.mel.n_fft = n_fft;
mel_params.mel.fmax = Some(sr / 2.0);
let mel_s = mel_spectrogram(y, &mel_params);
let n_mels = mel_s.rows();
let n_frames = mel_s.cols();
let db_flat = power_to_db(mel_s.as_slice(), 1.0, 1e-10, Some(80.0));
let s_db = Matrix::from_vec(db_flat, n_mels, n_frames);
let ref_db = if params.max_size > 1 {
let mut ref_data = vec![0.0; n_mels * n_frames];
for t in 0..n_frames {
let col: Vec<f64> = (0..n_mels).map(|r| s_db.get(r, t)).collect();
let filtered = crate::dsp::util::maximum_filter1d(&col, params.max_size);
for (r, &val) in filtered.iter().enumerate() {
ref_data[r * n_frames + t] = val;
}
}
Matrix::from_vec(ref_data, n_mels, n_frames)
} else {
s_db.clone()
};
if n_frames <= lag {
return vec![0.0; n_frames];
}
let flux_frames = n_frames - lag;
let mut onset_env = vec![0.0; flux_frames];
for t in 0..flux_frames {
let mut values: Vec<f64> = Vec::with_capacity(n_mels);
for r in 0..n_mels {
let diff = s_db.get(r, t + lag) - ref_db.get(r, t);
values.push(diff.max(0.0));
}
onset_env[t] = match params.aggregate {
Aggregate::Mean => values.iter().sum::<f64>() / n_mels as f64,
Aggregate::Median => median(&mut values),
};
}
let mut pad_width = lag;
if params.center {
pad_width += n_fft / (2 * hop_length);
}
let mut padded = vec![0.0; pad_width];
padded.extend_from_slice(&onset_env);
if params.detrend {
let mut filtered = vec![0.0; padded.len()];
if !padded.is_empty() {
filtered[0] = padded[0];
for i in 1..padded.len() {
filtered[i] = padded[i] - padded[i - 1] + 0.99 * filtered[i - 1];
}
}
padded = filtered;
}
if params.center {
padded.truncate(n_frames);
}
padded
}