use std::f32::consts::PI;
use anyhow::{bail, Result};
use rustfft::num_complex::Complex32;
use rustfft::FftPlanner;
pub const SAMPLE_RATE: u32 = 16_000;
pub const FRAME_LENGTH_MS: f32 = 25.0;
pub const FRAME_SHIFT_MS: f32 = 10.0;
pub const NUM_MEL_BINS: usize = 80;
pub const FFT_SIZE: usize = 512;
pub const LOW_FREQ_HZ: f32 = 20.0;
pub const HIGH_FREQ_HZ: f32 = (SAMPLE_RATE / 2) as f32;
pub const PREEMPHASIS: f32 = 0.97;
const EPSILON: f32 = 1e-10;
pub fn build_mel_filterbank(
num_bins: usize,
fft_size: usize,
sample_rate: u32,
) -> Result<Vec<Vec<f32>>> {
if num_bins != NUM_MEL_BINS {
bail!("wespeaker FBANK requires {NUM_MEL_BINS} mel bins, got {num_bins}");
}
let mel_low = hz_to_mel(LOW_FREQ_HZ);
let mel_high = hz_to_mel(HIGH_FREQ_HZ);
let num_points = num_bins + 2;
let mel_points: Vec<f32> = (0..num_points)
.map(|i| (mel_high - mel_low).mul_add(i as f32 / (num_points - 1) as f32, mel_low))
.collect();
let hz_points: Vec<f32> = mel_points.iter().copied().map(mel_to_hz).collect();
let num_bins_fft = fft_size / 2 + 1;
let bin_hz: Vec<f32> = (0..num_bins_fft)
.map(|k| k as f32 * sample_rate as f32 / fft_size as f32)
.collect();
let mut filters = vec![vec![0f32; num_bins_fft]; num_bins];
for m in 0..num_bins {
let left = hz_points[m];
let centre = hz_points[m + 1];
let right = hz_points[m + 2];
for (k, &f) in bin_hz.iter().enumerate() {
let w = if f < left || f > right {
0.0
} else if f <= centre {
(f - left) / (centre - left).max(EPSILON)
} else {
(right - f) / (right - centre).max(EPSILON)
};
filters[m][k] = w.max(0.0);
}
}
Ok(filters)
}
fn hz_to_mel(hz: f32) -> f32 {
1127.0 * (hz / 700.0).ln_1p()
}
fn mel_to_hz(mel: f32) -> f32 {
700.0 * (mel / 1127.0).exp_m1()
}
fn hamming(n: usize) -> Vec<f32> {
(0..n)
.map(|i| (-0.46_f32).mul_add((2.0 * PI * i as f32 / (n - 1) as f32).cos(), 0.54))
.collect()
}
pub fn compute_fbank(pcm: &[f32], mel_filters: &[Vec<f32>]) -> Result<Vec<Vec<f32>>> {
let frame_length = ((FRAME_LENGTH_MS / 1000.0) * SAMPLE_RATE as f32) as usize;
let frame_shift = ((FRAME_SHIFT_MS / 1000.0) * SAMPLE_RATE as f32) as usize;
if pcm.len() < frame_length {
bail!(
"PCM window has {} samples; need at least {} (one 25 ms frame at 16 kHz)",
pcm.len(),
frame_length
);
}
let num_frames = 1 + (pcm.len() - frame_length) / frame_shift;
let window = hamming(frame_length);
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(FFT_SIZE);
let mut feats: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
let mut scratch = vec![Complex32::new(0.0, 0.0); FFT_SIZE];
let mut emph = vec![0f32; frame_length];
for i in 0..num_frames {
let start = i * frame_shift;
let frame = &pcm[start..start + frame_length];
emph[0] = (-PREEMPHASIS).mul_add(frame[0], frame[0]);
for n in 1..frame_length {
emph[n] = (-PREEMPHASIS).mul_add(frame[n - 1], frame[n]);
}
for n in 0..FFT_SIZE {
let v = if n < frame_length {
emph[n] * window[n]
} else {
0.0
};
scratch[n] = Complex32::new(v, 0.0);
}
fft.process(&mut scratch);
let num_bins_fft = FFT_SIZE / 2 + 1;
let mut mel = vec![0f32; mel_filters.len()];
for (m, filter) in mel_filters.iter().enumerate() {
let mut energy = 0f32;
for (k, &w) in filter.iter().enumerate().take(num_bins_fft) {
let c = scratch[k];
let power = c.re.mul_add(c.re, c.im * c.im);
energy = w.mul_add(power, energy);
}
mel[m] = (energy + EPSILON).ln();
}
feats.push(mel);
}
let n = feats.len() as f32;
let mut mean = vec![0f32; mel_filters.len()];
for frame in &feats {
for (m, &v) in frame.iter().enumerate() {
mean[m] += v;
}
}
for v in &mut mean {
*v /= n;
}
for frame in &mut feats {
for (m, v) in frame.iter_mut().enumerate() {
*v -= mean[m];
}
}
Ok(feats)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn sine(freq_hz: f32, secs: f32) -> Vec<f32> {
let n = (secs * SAMPLE_RATE as f32) as usize;
(0..n)
.map(|i| (2.0 * PI * freq_hz * i as f32 / SAMPLE_RATE as f32).sin())
.collect()
}
#[test]
fn build_mel_filterbank_returns_80_by_257_matrix() {
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
assert_eq!(filters.len(), NUM_MEL_BINS);
assert_eq!(filters[0].len(), FFT_SIZE / 2 + 1);
}
#[test]
fn build_mel_filterbank_rejects_non_80_bins() {
let err = build_mel_filterbank(64, FFT_SIZE, SAMPLE_RATE).unwrap_err();
assert!(err.to_string().contains("80 mel bins"), "got: {err}");
}
#[test]
fn build_mel_filterbank_filters_are_non_negative() {
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
for (m, filter) in filters.iter().enumerate() {
for (k, &w) in filter.iter().enumerate() {
assert!(w >= 0.0, "filter[{m}][{k}] = {w} is negative");
}
}
}
#[test]
fn compute_fbank_frame_count_matches_formula() {
let pcm = sine(1_000.0, 0.5); let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
let feats = compute_fbank(&pcm, &filters).unwrap();
let frame_length = 400;
let frame_shift = 160;
let expected = 1 + (pcm.len() - frame_length) / frame_shift;
assert_eq!(feats.len(), expected);
}
#[test]
fn compute_fbank_emits_80_dim_frames() {
let pcm = sine(1_000.0, 0.5);
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
let feats = compute_fbank(&pcm, &filters).unwrap();
for (i, frame) in feats.iter().enumerate() {
assert_eq!(frame.len(), NUM_MEL_BINS, "frame {i}: {}", frame.len());
}
}
#[test]
fn compute_fbank_errors_on_too_short_pcm() {
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
let err = compute_fbank(&vec![0.0; 100], &filters).unwrap_err();
assert!(err.to_string().contains("at least"), "got: {err}");
}
#[test]
fn mel_filter_centres_are_monotonically_increasing_in_hz() {
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
let mut prev_centre_bin = 0usize;
for (m, filter) in filters.iter().enumerate() {
let centre_bin = filter
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.total_cmp(b))
.map(|(i, _)| i)
.unwrap();
assert!(
centre_bin >= prev_centre_bin,
"mel filter {m} centre bin {centre_bin} < previous {prev_centre_bin}"
);
prev_centre_bin = centre_bin;
}
}
#[test]
fn compute_fbank_cmn_zeros_mean_per_bin() {
let pcm = sine(1_000.0, 0.5);
let filters = build_mel_filterbank(NUM_MEL_BINS, FFT_SIZE, SAMPLE_RATE).unwrap();
let feats = compute_fbank(&pcm, &filters).unwrap();
let nf = feats.len() as f32;
for m in 0..NUM_MEL_BINS {
let mean: f32 = feats.iter().map(|f| f[m]).sum::<f32>() / nf;
assert!(
mean.abs() < 1e-3,
"bin {m} post-CMN mean = {mean} (expected ~0)"
);
}
}
#[test]
fn hz_mel_round_trip() {
for &hz in &[20.0_f32, 200.0, 1000.0, 4000.0, 8000.0] {
let back = mel_to_hz(hz_to_mel(hz));
assert!((back - hz).abs() < 1e-2, "{hz} -> {back}");
}
}
}