use crate::dsp::db::power_to_db;
use crate::matrix::Matrix;
pub fn dct_matrix(n_mfcc: usize, n_mels: usize) -> Matrix {
let scale0 = (1.0 / n_mels as f64).sqrt();
let scale = (2.0 / n_mels as f64).sqrt();
Matrix::from_fn(n_mfcc, n_mels, |k, n| {
let s = if k == 0 { scale0 } else { scale };
let angle = std::f64::consts::PI * k as f64 * (2 * n + 1) as f64 / (2.0 * n_mels as f64);
s * angle.cos()
})
}
pub struct MfccParams {
pub n_mfcc: usize,
pub ref_power: Option<f64>,
pub amin: f64,
pub top_db: Option<f64>,
}
impl Default for MfccParams {
fn default() -> Self {
Self {
n_mfcc: 20,
ref_power: Some(1.0),
amin: 1e-10,
top_db: Some(80.0),
}
}
}
pub fn mfcc(mel: &Matrix, params: &MfccParams) -> Matrix {
let n_mels = mel.rows();
let ref_val = params.ref_power.unwrap_or_else(|| {
mel.as_slice()
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
});
let log_mel_flat = power_to_db(mel.as_slice(), ref_val, params.amin, params.top_db);
let log_mel = Matrix::from_vec(log_mel_flat, n_mels, mel.cols());
let d = dct_matrix(params.n_mfcc, n_mels);
d.matmul(&log_mel)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dct_matrix_orthonormal() {
let d = dct_matrix(20, 128);
let dt = d.transpose();
let eye = d.matmul(&dt);
for k in 0..20 {
for l in 0..20 {
let expected = if k == l { 1.0 } else { 0.0 };
let diff = (eye.get(k, l) - expected).abs();
assert!(
diff < 1e-12,
"D @ D^T [{k},{l}] = {}, expected {expected}",
eye.get(k, l)
);
}
}
}
#[test]
fn test_mfcc_shape() {
let mel = Matrix::from_fn(128, 50, |r, c| (r + c) as f64 * 0.01 + 1e-6);
let result = mfcc(&mel, &MfccParams::default());
assert_eq!(result.shape(), (20, 50));
}
}