use rustfft::FftPlanner;
use rustfft::num_complex::Complex32;
use crate::model::audio_preprocessor::{build_hann_window, build_mel_filterbank};
pub const SAMPLE_RATE: usize = 16_000;
pub const N_FFT: usize = 400;
pub const HOP_LEN: usize = 160;
pub const CHUNK_SAMPLES: usize = 30 * SAMPLE_RATE; pub const CHUNK_FRAMES: usize = CHUNK_SAMPLES / HOP_LEN; pub const N_FFT_BINS: usize = N_FFT / 2 + 1;
pub fn extract_whisper_mel(pcm: &[f32], n_mels: usize) -> Vec<f32> {
if n_mels != 80 && n_mels != 128 {
return Vec::new();
}
let pad = N_FFT / 2;
let mut padded_audio = vec![0.0f32; CHUNK_SAMPLES + 2 * pad];
let copy_len = pcm.len().min(CHUNK_SAMPLES);
if copy_len > 0 {
for (dst, &src) in padded_audio[pad..pad + copy_len]
.iter_mut()
.zip(&pcm[..copy_len])
{
*dst = if src.is_finite() { src } else { 0.0 };
}
for i in 0..pad {
if i + 1 < copy_len {
let v = pcm[i + 1];
padded_audio[pad - 1 - i] = if v.is_finite() { v } else { 0.0 };
}
}
if copy_len == CHUNK_SAMPLES {
for i in 0..pad {
let src_idx = CHUNK_SAMPLES.saturating_sub(2 + i);
let v = pcm[src_idx];
padded_audio[pad + CHUNK_SAMPLES + i] = if v.is_finite() { v } else { 0.0 };
}
}
}
let hann = build_hann_window(N_FFT);
let mel_filters = build_mel_filterbank(n_mels, N_FFT, SAMPLE_RATE);
thread_local! {
static FFT_PLANNER: std::cell::RefCell<FftPlanner<f32>> =
std::cell::RefCell::new(FftPlanner::new());
}
let fft = FFT_PLANNER.with(|p| p.borrow_mut().plan_fft_forward(N_FFT));
let mut fft_buf = vec![Complex32::new(0.0, 0.0); N_FFT];
let mut power_spec = vec![0.0f32; CHUNK_FRAMES * N_FFT_BINS];
for frame in 0..CHUNK_FRAMES {
let start = frame * HOP_LEN;
for i in 0..N_FFT {
fft_buf[i] = Complex32::new(padded_audio[start + i] * hann[i], 0.0);
}
fft.process(&mut fft_buf);
let row = &mut power_spec[frame * N_FFT_BINS..(frame + 1) * N_FFT_BINS];
for (k, slot) in row.iter_mut().enumerate().take(N_FFT_BINS) {
*slot = fft_buf[k].norm_sqr();
}
}
let mut log_mel = vec![0.0f32; n_mels * CHUNK_FRAMES];
let mut max_val = f32::NEG_INFINITY;
for m in 0..n_mels {
let filter_row = &mel_filters[m * N_FFT_BINS..(m + 1) * N_FFT_BINS];
let k_first = filter_row.iter().position(|&v| v > 0.0).unwrap_or(0);
let k_last = filter_row
.iter()
.rposition(|&v| v > 0.0)
.map_or(0, |i| i + 1);
let active_filter = &filter_row[k_first..k_last];
for t in 0..CHUNK_FRAMES {
let power_row = &power_spec[t * N_FFT_BINS..(t + 1) * N_FFT_BINS];
let sum = if !active_filter.is_empty() {
crate::backend::cpu::dot_f32(active_filter, &power_row[k_first..k_last])
} else {
0.0
};
let val = (sum.max(1e-10)).log10();
if val > max_val {
max_val = val;
}
log_mel[m * CHUNK_FRAMES + t] = val;
}
}
let min_allowed = max_val - 8.0;
for slot in log_mel.iter_mut() {
let clamped = slot.max(min_allowed);
*slot = (clamped + 4.0) / 4.0;
}
log_mel
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_whisper_mel_silence() {
let silence = vec![0.0f32; 16000]; let mel = extract_whisper_mel(&silence, 80);
assert_eq!(mel.len(), 80 * CHUNK_FRAMES);
for (idx, &v) in mel.iter().enumerate() {
assert!(v.is_finite(), "mel[{idx}] is not finite: {v}");
assert!(
(-2.0..=2.0).contains(&v),
"mel[{idx}] out of expected range: {v}"
);
}
}
#[test]
fn test_extract_whisper_mel_sine_wave() {
let mut pcm = Vec::with_capacity(16000);
for i in 0..16000 {
let t = i as f32 / 16000.0;
pcm.push((2.0 * std::f32::consts::PI * 440.0 * t).sin());
}
let mel_80 = extract_whisper_mel(&pcm, 80);
assert_eq!(mel_80.len(), 80 * 3000);
let mel_128 = extract_whisper_mel(&pcm, 128);
assert_eq!(mel_128.len(), 128 * 3000);
let max_val_80 = mel_80.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_val_80 = mel_80.iter().copied().fold(f32::INFINITY, f32::min);
assert!(max_val_80.is_finite());
assert!(min_val_80.is_finite());
assert!(
(max_val_80 - min_val_80) <= 2.0001,
"range exceeds 2.0: {} to {}",
min_val_80,
max_val_80
);
assert!(
(max_val_80 - min_val_80) >= 1.9999,
"range less than 2.0: {} to {}",
min_val_80,
max_val_80
);
}
#[test]
fn test_extract_whisper_mel_invalid_bins() {
let pcm = vec![0.0f32; 16000];
let mel = extract_whisper_mel(&pcm, 64);
assert!(mel.is_empty(), "invalid mel bins should yield empty output");
}
#[test]
fn test_extract_whisper_mel_non_finite_pcm() {
let pcm = vec![f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.5];
let mel = extract_whisper_mel(&pcm, 80);
assert_eq!(mel.len(), 80 * CHUNK_FRAMES);
for &val in &mel {
assert!(val.is_finite(), "mel value must be finite: {val}");
}
}
#[test]
fn test_extract_whisper_mel_single_sample() {
let pcm = vec![0.5f32];
let mel = extract_whisper_mel(&pcm, 80);
assert_eq!(mel.len(), 80 * CHUNK_FRAMES);
for &val in &mel {
assert!(
val.is_finite(),
"mel value for single-sample input must be finite: {val}"
);
}
}
}