use crate::matrix::Matrix;
pub struct SalienceParams {
pub harmonics: Vec<f64>,
pub weights: Option<Vec<f64>>,
pub filter_peaks: bool,
pub fill_value: f64,
}
impl Default for SalienceParams {
fn default() -> Self {
Self {
harmonics: vec![1.0, 2.0, 3.0, 4.0, 5.0],
weights: None,
filter_peaks: true,
fill_value: f64::NAN,
}
}
}
fn interp1d_linear(x_grid: &[f64], y_grid: &[f64], x_new: &[f64], fill_value: f64) -> Vec<f64> {
let n = x_grid.len();
x_new
.iter()
.map(|&x| {
if n == 0 || x < x_grid[0] || x > x_grid[n - 1] {
return fill_value;
}
let pos = x_grid.partition_point(|&v| v < x);
if pos == 0 {
return y_grid[0];
}
if pos >= n {
return y_grid[n - 1];
}
let x0 = x_grid[pos - 1];
let x1 = x_grid[pos];
let dx = x1 - x0;
if dx.abs() < f64::MIN_POSITIVE {
return y_grid[pos - 1];
}
let t = (x - x0) / dx;
y_grid[pos - 1] * (1.0 - t) + y_grid[pos] * t
})
.collect()
}
pub fn interp_harmonics(
s: &Matrix,
freqs: &[f64],
harmonics: &[f64],
fill_value: f64,
) -> Vec<Matrix> {
let n_freqs = s.rows();
let n_frames = s.cols();
harmonics
.iter()
.map(|&h| {
let target_freqs: Vec<f64> = freqs.iter().map(|&f| h * f).collect();
let mut data = vec![0.0f64; n_freqs * n_frames];
for t in 0..n_frames {
let col: Vec<f64> = (0..n_freqs).map(|r| s.get(r, t)).collect();
let interpolated = interp1d_linear(freqs, &col, &target_freqs, fill_value);
for r in 0..n_freqs {
data[r * n_frames + t] = interpolated[r];
}
}
Matrix::from_vec(data, n_freqs, n_frames)
})
.collect()
}
pub fn f0_harmonics(
s: &Matrix,
f0: &[f64],
freqs: &[f64],
harmonics: &[f64],
fill_value: f64,
) -> Matrix {
let n_freqs = s.rows();
let n_frames = s.cols();
let n_harmonics = harmonics.len();
assert_eq!(f0.len(), n_frames, "f0 length must match number of frames");
let mut data = vec![fill_value; n_harmonics * n_frames];
for t in 0..n_frames {
if f0[t].is_nan() || f0[t] <= 0.0 {
continue;
}
let col: Vec<f64> = (0..n_freqs).map(|r| s.get(r, t)).collect();
for (h_idx, &h) in harmonics.iter().enumerate() {
let target = f0[t] * h;
let result = interp1d_linear(freqs, &col, &[target], fill_value);
data[h_idx * n_frames + t] = result[0];
}
}
Matrix::from_vec(data, n_harmonics, n_frames)
}
pub fn salience(s: &Matrix, freqs: &[f64], params: &SalienceParams) -> Matrix {
let n_freqs = s.rows();
let n_frames = s.cols();
let harm_data = interp_harmonics(s, freqs, ¶ms.harmonics, 0.0);
let n_harmonics = params.harmonics.len();
let weights = match ¶ms.weights {
Some(w) => w.clone(),
None => vec![1.0 / n_harmonics as f64; n_harmonics],
};
let weight_sum: f64 = weights.iter().sum();
let mut sal_data = vec![0.0f64; n_freqs * n_frames];
for f in 0..n_freqs {
for t in 0..n_frames {
let mut sum = 0.0;
for (h, w) in weights.iter().enumerate() {
sum += w * harm_data[h].get(f, t);
}
sal_data[f * n_frames + t] = sum / weight_sum;
}
}
let mut result = Matrix::from_vec(sal_data, n_freqs, n_frames);
if params.filter_peaks {
for t in 0..n_frames {
let col: Vec<f64> = (0..n_freqs).map(|f| result.get(f, t)).collect();
let peaks = crate::dsp::util::localmax(&col);
for f in 0..n_freqs {
if !peaks[f] {
*result.get_mut(f, t) = params.fill_value;
}
}
}
}
result
}
pub fn tonnetz(y: Option<&[f64]>, sr: f64, chroma: Option<&Matrix>) -> Matrix {
let owned_chroma;
let chroma = match chroma {
Some(c) => c,
None => {
let y = y.expect("tonnetz: either y or chroma must be provided");
let params = super::chroma::ChromaCqtParams {
sr,
..Default::default()
};
owned_chroma = super::chroma::chroma_cqt(y, ¶ms);
&owned_chroma
}
};
let n_chroma = chroma.rows();
use crate::matrix::Axis;
let chroma_norm = chroma.normalize_axis(Axis::Row, 1.0);
let angles = [
7.0 / 6.0,
7.0 / 6.0,
3.0 / 2.0,
3.0 / 2.0,
2.0 / 3.0,
2.0 / 3.0,
];
let radii = [1.0, 1.0, 1.0, 1.0, 0.5, 0.5];
let mut phi_data = vec![0.0f64; 6 * n_chroma];
for i in 0..6 {
for j in 0..n_chroma {
let mut v = angles[i] * (j as f64) * (12.0 / n_chroma as f64);
if i % 2 == 0 {
v -= 0.5;
}
phi_data[i * n_chroma + j] = radii[i] * (std::f64::consts::PI * v).cos();
}
}
let phi = Matrix::from_vec(phi_data, 6, n_chroma);
phi.matmul(&chroma_norm)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interp1d_linear_basic() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 10.0, 20.0, 30.0];
let x_new = vec![0.5, 1.5, 2.5];
let result = interp1d_linear(&x, &y, &x_new, -1.0);
assert!((result[0] - 5.0).abs() < 1e-12);
assert!((result[1] - 15.0).abs() < 1e-12);
assert!((result[2] - 25.0).abs() < 1e-12);
}
#[test]
fn test_interp1d_linear_out_of_bounds() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![10.0, 20.0, 30.0];
let x_new = vec![0.0, 4.0];
let result = interp1d_linear(&x, &y, &x_new, -999.0);
assert_eq!(result[0], -999.0);
assert_eq!(result[1], -999.0);
}
#[test]
fn test_interp_harmonics_shape() {
let s = Matrix::from_vec(vec![1.0; 10 * 5], 10, 5);
let freqs: Vec<f64> = (0..10).map(|i| 100.0 + i as f64 * 100.0).collect();
let harmonics = vec![1.0, 2.0, 3.0];
let result = interp_harmonics(&s, &freqs, &harmonics, 0.0);
assert_eq!(result.len(), 3);
for m in &result {
assert_eq!(m.rows(), 10);
assert_eq!(m.cols(), 5);
}
}
#[test]
fn test_tonnetz_shape() {
let mut data = vec![0.0; 12 * 5];
for f in 0..5 {
data[0 * 5 + f] = 1.0;
}
let chroma = Matrix::from_vec(data, 12, 5);
let result = tonnetz(None, 22050.0, Some(&chroma));
assert_eq!(result.rows(), 6);
assert_eq!(result.cols(), 5);
}
}