pub fn hz_to_mel(hz: f64, htk: bool) -> f64 {
if htk {
2595.0 * (1.0 + hz / 700.0).log10()
} else {
if hz < 1000.0 {
hz * 3.0 / 200.0
} else {
15.0 + 27.0 * (hz / 1000.0).ln() / f64::ln(6.4)
}
}
}
pub fn mel_to_hz(mel: f64, htk: bool) -> f64 {
if htk {
700.0 * (10.0_f64.powf(mel / 2595.0) - 1.0)
} else {
if mel < 15.0 {
mel * 200.0 / 3.0
} else {
1000.0 * (f64::ln(6.4) * (mel - 15.0) / 27.0).exp()
}
}
}
pub fn fft_frequencies(sr: f64, n_fft: usize) -> Vec<f64> {
let n_bins = 1 + n_fft / 2;
(0..n_bins).map(|i| i as f64 * sr / n_fft as f64).collect()
}
pub fn mel_frequencies(n_mels: usize, fmin: f64, fmax: f64, htk: bool) -> Vec<f64> {
let min_mel = hz_to_mel(fmin, htk);
let max_mel = hz_to_mel(fmax, htk);
if n_mels <= 1 {
return vec![mel_to_hz(min_mel, htk)];
}
let step = (max_mel - min_mel) / (n_mels - 1) as f64;
(0..n_mels)
.map(|i| mel_to_hz(min_mel + i as f64 * step, htk))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct ConvTestCase {
name: String,
function: String,
#[serde(default)]
hz: Option<f64>,
#[serde(default)]
mel: Option<f64>,
#[serde(default)]
htk: Option<bool>,
#[serde(default)]
sr: Option<f64>,
#[serde(default)]
n_fft: Option<usize>,
#[serde(default)]
n_mels: Option<usize>,
#[serde(default)]
fmin: Option<f64>,
#[serde(default)]
fmax: Option<f64>,
#[serde(default)]
expected: serde_json::Value,
}
fn load_refs() -> Vec<ConvTestCase> {
let data = include_str!("tests/references/mel_conversions.json");
serde_json::from_str(data).expect("Failed to parse mel_conversions.json")
}
fn assert_approx(actual: f64, expected: f64, tol: f64, name: &str) {
let diff = (actual - expected).abs();
assert!(
diff < tol,
"{name}: got {actual}, expected {expected}, diff {diff} > tol {tol}"
);
}
fn assert_vec_approx(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(actual.len(), expected.len(), "{name}: length mismatch");
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
let diff = (a - e).abs();
assert!(
diff < tol,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
);
}
}
#[test]
fn test_hz_to_mel_matches_librosa() {
let refs = load_refs();
let mut count = 0;
for case in refs.iter().filter(|c| c.function == "hz_to_mel") {
let hz = case.hz.unwrap();
let htk = case.htk.unwrap();
let expected = case.expected.as_f64().unwrap();
let actual = hz_to_mel(hz, htk);
assert_approx(actual, expected, 1e-12, &case.name);
count += 1;
}
assert!(count > 0, "No hz_to_mel test cases found");
}
#[test]
fn test_mel_to_hz_matches_librosa() {
let refs = load_refs();
let mut count = 0;
for case in refs.iter().filter(|c| c.function == "mel_to_hz") {
let mel = case.mel.unwrap();
let htk = case.htk.unwrap();
let expected = case.expected.as_f64().unwrap();
let actual = mel_to_hz(mel, htk);
assert_approx(actual, expected, 1e-10, &case.name);
count += 1;
}
assert!(count > 0, "No mel_to_hz test cases found");
}
#[test]
fn test_fft_frequencies_matches_librosa() {
let refs = load_refs();
let mut count = 0;
for case in refs.iter().filter(|c| c.function == "fft_frequencies") {
let sr = case.sr.unwrap();
let n_fft = case.n_fft.unwrap();
let expected: Vec<f64> = case
.expected
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect();
let actual = fft_frequencies(sr, n_fft);
assert_vec_approx(&actual, &expected, 1e-12, &case.name);
count += 1;
}
assert!(count > 0, "No fft_frequencies test cases found");
}
#[test]
fn test_mel_frequencies_matches_librosa() {
let refs = load_refs();
let mut count = 0;
for case in refs.iter().filter(|c| c.function == "mel_frequencies") {
let n_mels = case.n_mels.unwrap();
let fmin = case.fmin.unwrap();
let fmax = case.fmax.unwrap();
let htk = case.htk.unwrap();
let expected: Vec<f64> = case
.expected
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect();
let actual = mel_frequencies(n_mels, fmin, fmax, htk);
assert_vec_approx(&actual, &expected, 1e-10, &case.name);
count += 1;
}
assert!(count > 0, "No mel_frequencies test cases found");
}
}