use rustfft::FftPlanner;
use rustfft::num_complex::Complex32;
pub const SAMPLE_RATE: u32 = 16000;
pub const N_FFT: usize = 640;
pub const WIN_SIZE: usize = 640;
pub const HOP_SIZE: usize = 160;
pub const N_BINS: usize = 128;
pub const CLIP_VAL: f32 = 1e-5;
const SYMMETRIC_HANN: bool = false;
fn hann_window_periodic(size: usize) -> Vec<f32> {
let denom = if SYMMETRIC_HANN { size - 1 } else { size };
(0..size)
.map(|i| 0.5 - 0.5 * (2.0 * std::f32::consts::PI * i as f32 / denom as f32).cos())
.collect()
}
fn reflect_pad(signal: &[f32], left: usize, right: usize) -> Vec<f32> {
let len = signal.len();
let mut out = Vec::with_capacity(len + left + right);
for i in 0..left {
out.push(signal[left - i]);
}
out.extend_from_slice(signal);
for k in 0..right {
out.push(signal[len - 2 - k.min(len - 2)]);
}
out
}
pub fn wav_to_mel(audio: &[f32]) -> Vec<Vec<f32>> {
let mut window = hann_window_periodic(WIN_SIZE);
window.resize(N_FFT, 0.0);
let pad_left = (WIN_SIZE - HOP_SIZE) / 2;
let natural_right = (WIN_SIZE - HOP_SIZE).div_ceil(2);
let needed_right =
(WIN_SIZE as isize - audio.len() as isize - pad_left as isize).max(0) as usize;
let pad_right = natural_right.max(needed_right);
let padded = if pad_right < audio.len() {
reflect_pad(audio, pad_left, pad_right)
} else {
let mut out = vec![0.0_f32; audio.len() + pad_left + pad_right];
out[pad_left..pad_left + audio.len()].copy_from_slice(audio);
out
};
let n_frames = if padded.len() >= WIN_SIZE {
(padded.len() - WIN_SIZE) / HOP_SIZE + 1
} else {
0
};
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(N_FFT);
let mut frame = vec![Complex32::new(0.0, 0.0); N_FFT];
let mut features = vec![vec![0.0_f32; N_BINS]; n_frames];
for (t, feature_row) in features.iter_mut().enumerate() {
let start = t * HOP_SIZE;
for (i, bin) in frame.iter_mut().enumerate() {
*bin = Complex32::new(padded[start + i] * window[i], 0.0);
}
fft.process(&mut frame);
for (f, out) in feature_row.iter_mut().enumerate() {
let re = frame[f].re;
let im = frame[f].im;
*out = (re * re + im * im + 1e-9).sqrt().max(CLIP_VAL).ln();
}
}
let target_frames = audio.len() / HOP_SIZE + 1;
if target_frames > features.len() && !features.is_empty() {
let last = features.last().cloned().unwrap();
features.resize(target_frames, last);
} else if target_frames < features.len() {
features.truncate(target_frames);
}
features
}
#[cfg(test)]
mod tests {
use super::*;
fn sine(freq: f32, n: usize) -> Vec<f32> {
(0..n)
.map(|i| {
0.5 * (2.0 * std::f32::consts::PI * freq * i as f32 / SAMPLE_RATE as f32).sin()
})
.collect()
}
#[test]
fn frame_count_matches_wrapper() {
let mel = wav_to_mel(&sine(220.0, 16000));
assert_eq!(mel.len(), 16000 / HOP_SIZE + 1);
assert_eq!(mel[0].len(), N_BINS);
assert!(mel[10][20] < 0.0);
}
#[test]
fn spectral_peak_lands_on_expected_bin() {
for (freq, bin) in [(250.0f32, 10usize), (1000.0, 40), (2000.0, 80)] {
let mel = wav_to_mel(&sine(freq, 16000));
let (peak, _) = mel[50]
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
assert!(
(peak as i32 - bin as i32).abs() <= 1,
"freq {freq}: peak bin {peak}, expected {bin}"
);
}
}
#[test]
fn silence_is_low_energy() {
let mel = wav_to_mel(&vec![0.0_f32; 16000]);
assert!(mel[50].iter().all(|&v| v < -9.0));
}
#[test]
fn reflect_padding() {
let x = [1.0_f32, 2.0, 3.0, 4.0];
let padded = reflect_pad(&x, 2, 2);
assert_eq!(padded, vec![3.0, 2.0, 1.0, 2.0, 3.0, 4.0, 3.0, 2.0]);
}
}