use std::f32::consts::PI;
use std::sync::Arc;
use rustfft::num_complex::Complex32;
use rustfft::{Fft, FftPlanner};
pub const N_SAMPLES: usize = 480_000;
pub const N_FRAMES: usize = 3_000;
pub const N_FFT: usize = 400;
pub const HOP_LENGTH: usize = 160;
pub const N_MELS: usize = 128;
pub struct WhisperMel {
window: Vec<f32>,
mel_filters: Vec<MelFilter>,
fft: Arc<dyn Fft<f32> + Send + Sync>,
}
#[derive(Debug, Clone)]
struct MelFilter {
start_bin: usize,
weights: Vec<f32>,
}
impl Default for WhisperMel {
fn default() -> Self {
Self::new()
}
}
impl WhisperMel {
pub fn new() -> Self {
let window = periodic_hann(N_FFT);
let mel_filters = build_slaney_mel_filters(16_000.0, N_FFT, N_MELS, 0.0, 8000.0);
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(N_FFT);
Self {
window,
mel_filters,
fft,
}
}
pub fn compute(&self, samples: &[f32]) -> Vec<f32> {
let mut audio = vec![0.0_f32; N_SAMPLES];
let take = samples.len().min(N_SAMPLES);
audio[..take].copy_from_slice(&samples[..take]);
let padded = reflect_pad(&audio, N_FFT / 2);
let n_bins = N_FFT / 2 + 1;
let total_frames = (padded.len() - N_FFT) / HOP_LENGTH + 1;
let keep_frames = total_frames.saturating_sub(1).min(N_FRAMES);
let mut log_spec = vec![0.0_f32; N_MELS * N_FRAMES];
let mut buf = vec![Complex32::new(0.0, 0.0); N_FFT];
for f in 0..keep_frames {
let start = f * HOP_LENGTH;
for i in 0..N_FFT {
buf[i] = Complex32::new(padded[start + i] * self.window[i], 0.0);
}
self.fft.process(&mut buf);
for (m, filt) in self.mel_filters.iter().enumerate() {
let mut energy = 0.0_f32;
for (k, w) in filt.weights.iter().enumerate() {
let bin = filt.start_bin + k;
if bin < n_bins {
let c = buf[bin];
energy += (c.re * c.re + c.im * c.im) * *w;
}
}
let clamped = energy.max(1e-10);
log_spec[m * N_FRAMES + f] = clamped.log10();
}
}
if keep_frames < N_FRAMES {
for m in 0..N_MELS {
for f in keep_frames..N_FRAMES {
log_spec[m * N_FRAMES + f] = f32::NEG_INFINITY;
}
}
}
let mut peak = f32::NEG_INFINITY;
for &v in &log_spec {
if v > peak {
peak = v;
}
}
let floor = peak - 8.0;
for v in log_spec.iter_mut() {
if *v < floor {
*v = floor;
}
*v = (*v + 4.0) / 4.0;
}
log_spec
}
}
fn reflect_pad(audio: &[f32], half: usize) -> Vec<f32> {
let n = audio.len();
assert!(n > half, "reflect_pad needs n > half");
let mut out = Vec::with_capacity(n + 2 * half);
for i in 0..half {
out.push(audio[half - i]);
}
out.extend_from_slice(audio);
for i in 0..half {
out.push(audio[n - 2 - i]);
}
out
}
fn periodic_hann(n: usize) -> Vec<f32> {
(0..n)
.map(|i| 0.5 - 0.5 * (2.0 * PI * i as f32 / n as f32).cos())
.collect()
}
fn hz_to_mel_slaney(hz: f32) -> f32 {
let f_sp = 200.0_f32 / 3.0;
let min_log_hz = 1000.0_f32;
let min_log_mel = min_log_hz / f_sp;
let logstep = (6.4_f32).ln() / 27.0;
if hz < min_log_hz {
hz / f_sp
} else {
min_log_mel + (hz / min_log_hz).ln() / logstep
}
}
fn mel_to_hz_slaney(mel: f32) -> f32 {
let f_sp = 200.0_f32 / 3.0;
let min_log_hz = 1000.0_f32;
let min_log_mel = min_log_hz / f_sp;
let logstep = (6.4_f32).ln() / 27.0;
if mel < min_log_mel {
mel * f_sp
} else {
min_log_hz * (logstep * (mel - min_log_mel)).exp()
}
}
fn build_slaney_mel_filters(
sample_rate: f32,
n_fft: usize,
n_mels: usize,
low_freq: f32,
high_freq: f32,
) -> Vec<MelFilter> {
let n_bins = n_fft / 2 + 1;
let bin_hz = sample_rate / n_fft as f32;
let mel_lo = hz_to_mel_slaney(low_freq);
let mel_hi = hz_to_mel_slaney(high_freq);
let mel_step = (mel_hi - mel_lo) / (n_mels + 1) as f32;
let mel_points: Vec<f32> = (0..=n_mels + 1)
.map(|i| mel_lo + i as f32 * mel_step)
.collect();
let hz_points: Vec<f32> = mel_points.iter().map(|m| mel_to_hz_slaney(*m)).collect();
let mut filters = Vec::with_capacity(n_mels);
for m in 0..n_mels {
let left_hz = hz_points[m];
let centre_hz = hz_points[m + 1];
let right_hz = hz_points[m + 2];
let enorm = 2.0 / (right_hz - left_hz).max(1e-10);
let mut start_bin: Option<usize> = None;
let mut weights_full: Vec<f32> = Vec::new();
for bin in 0..n_bins {
let hz = bin as f32 * bin_hz;
if hz <= left_hz || hz >= right_hz {
continue;
}
let w_unnorm = if hz <= centre_hz {
(hz - left_hz) / (centre_hz - left_hz).max(1e-10)
} else {
(right_hz - hz) / (right_hz - centre_hz).max(1e-10)
};
if !w_unnorm.is_finite() || w_unnorm <= 0.0 {
continue;
}
let w = w_unnorm * enorm;
if start_bin.is_none() {
start_bin = Some(bin);
}
let s = start_bin.unwrap();
while weights_full.len() < bin - s {
weights_full.push(0.0);
}
weights_full.push(w);
}
filters.push(MelFilter {
start_bin: start_bin.unwrap_or(0),
weights: weights_full,
});
}
filters
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn periodic_hann_starts_at_zero_and_peaks_near_middle() {
let w = periodic_hann(400);
assert!(w[0].abs() < 1e-7, "{}", w[0]);
assert!(w[200] > 0.999, "{}", w[200]);
assert!(w[399] > 0.0 && w[399] < 0.01, "{}", w[399]);
}
#[test]
fn reflect_pad_matches_numpy_example() {
let audio = [1.0, 2.0, 3.0, 4.0, 5.0];
let got = reflect_pad(&audio, 2);
assert_eq!(got, vec![3.0, 2.0, 1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0]);
}
#[test]
fn output_shape_is_n_mels_x_n_frames() {
let mel = WhisperMel::new();
let silence = vec![0.0_f32; N_SAMPLES];
let out = mel.compute(&silence);
assert_eq!(out.len(), N_MELS * N_FRAMES);
}
#[test]
fn output_finite_and_in_post_norm_range_for_silence() {
let mel = WhisperMel::new();
let silence = vec![0.0_f32; N_SAMPLES];
let out = mel.compute(&silence);
for (i, v) in out.iter().enumerate() {
assert!(v.is_finite(), "cell {i} = {v}");
assert!((-3.5..=1.0).contains(v), "cell {i} = {v}");
}
}
#[test]
fn short_audio_is_zero_padded_and_yields_full_grid() {
let mel = WhisperMel::new();
let one_sec = vec![0.0_f32; 16_000];
let out = mel.compute(&one_sec);
assert_eq!(out.len(), N_MELS * N_FRAMES);
for &v in &out {
assert!(v.is_finite());
}
}
#[test]
fn slaney_mel_known_anchors() {
assert!(hz_to_mel_slaney(0.0).abs() < 1e-6);
assert!((hz_to_mel_slaney(1000.0) - 15.0).abs() < 1e-4);
for hz in [50.0_f32, 200.0, 500.0, 1000.0, 4000.0, 8000.0] {
let rt = mel_to_hz_slaney(hz_to_mel_slaney(hz));
assert!((rt - hz).abs() <= hz * 1e-4, "hz={hz} rt={rt}");
}
}
#[test]
fn filterbank_has_correct_count_and_no_empty_bands() {
let filters = build_slaney_mel_filters(16_000.0, N_FFT, N_MELS, 0.0, 8000.0);
assert_eq!(filters.len(), N_MELS);
for (i, f) in filters.iter().enumerate() {
let any_nonzero = f.weights.iter().any(|&w| w > 0.0);
assert!(any_nonzero, "filter {i} is empty");
}
}
}