mod arch;
mod dispatch;
mod scalar;
#[cfg(feature = "_bench-internals")]
#[doc(hidden)]
pub mod __bench_internals {
pub const NUM_MEL_BINS: usize = super::NUM_MEL_BINS;
pub const FRAME_LENGTH_SAMPLES: usize = super::FRAME_LENGTH_SAMPLES;
pub const FFT_SIZE: usize = super::FFT_SIZE;
pub const FFT_BINS: usize = super::FFT_BINS;
pub const PRE_EMPHASIS: f32 = super::PRE_EMPHASIS;
pub const LOG_FLOOR: f32 = super::LOG_FLOOR;
pub const INT16_SCALE: f32 = super::INT16_SCALE;
pub mod scalar {
use super::super::scalar as inner;
#[inline]
pub fn pcm_scale_extend(pcm: &[f32], out: &mut [f32]) {
inner::pcm_scale_extend(pcm, out);
}
#[inline]
pub fn dc_remove(window: &[f32], out: &mut [f32]) {
inner::dc_remove(window, out);
}
#[inline]
pub fn pre_emphasis(samples: &mut [f32]) {
inner::pre_emphasis(samples);
}
#[inline]
pub fn window_apply(samples: &mut [f32], window: &[f32]) {
inner::window_apply(samples, window);
}
#[inline]
pub fn power_spectrum(complex: &[rustfft::num_complex::Complex<f32>], out: &mut [f32]) {
inner::power_spectrum(complex, out);
}
#[inline]
pub fn cmvn_apply(feature: &mut [f32], means: &[f32], istd: &[f32]) {
inner::cmvn_apply(feature, means, istd);
}
#[inline]
pub fn mel_dot_log(power_slice: &[f32], weights: &[f32]) -> f32 {
inner::mel_dot_log(power_slice, weights)
}
}
#[cfg(target_arch = "aarch64")]
#[allow(unsafe_code)]
pub mod neon {
use super::super::arch::neon as inner;
#[inline]
pub unsafe fn dc_remove(window: &[f32], out: &mut [f32]) {
unsafe { inner::dc_remove(window, out) }
}
#[inline]
pub unsafe fn window_apply(samples: &mut [f32], window: &[f32]) {
unsafe { inner::window_apply(samples, window) }
}
#[inline]
pub unsafe fn power_spectrum(complex: &[rustfft::num_complex::Complex<f32>], out: &mut [f32]) {
unsafe { inner::power_spectrum(complex, out) }
}
#[inline]
pub unsafe fn cmvn_apply(feature: &mut [f32], means: &[f32], istd: &[f32]) {
unsafe { inner::cmvn_apply(feature, means, istd) }
}
#[inline]
pub unsafe fn mel_dot_log(power_slice: &[f32], weights: &[f32]) -> f32 {
unsafe { inner::mel_dot_log(power_slice, weights) }
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[allow(unsafe_code)]
pub mod x86_sse41 {
use super::super::arch::x86_sse41 as inner;
#[inline]
pub unsafe fn dc_remove(window: &[f32], out: &mut [f32]) {
unsafe { inner::dc_remove(window, out) }
}
#[inline]
pub unsafe fn window_apply(samples: &mut [f32], window: &[f32]) {
unsafe { inner::window_apply(samples, window) }
}
#[inline]
pub unsafe fn power_spectrum(complex: &[rustfft::num_complex::Complex<f32>], out: &mut [f32]) {
unsafe { inner::power_spectrum(complex, out) }
}
#[inline]
pub unsafe fn cmvn_apply(feature: &mut [f32], means: &[f32], istd: &[f32]) {
unsafe { inner::cmvn_apply(feature, means, istd) }
}
#[inline]
pub unsafe fn mel_dot_log(power_slice: &[f32], weights: &[f32]) -> f32 {
unsafe { inner::mel_dot_log(power_slice, weights) }
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[allow(unsafe_code)]
pub mod x86_avx2 {
use super::super::arch::x86_avx2 as inner;
#[inline]
pub unsafe fn dc_remove(window: &[f32], out: &mut [f32]) {
unsafe { inner::dc_remove(window, out) }
}
#[inline]
pub unsafe fn window_apply(samples: &mut [f32], window: &[f32]) {
unsafe { inner::window_apply(samples, window) }
}
#[inline]
pub unsafe fn power_spectrum(complex: &[rustfft::num_complex::Complex<f32>], out: &mut [f32]) {
unsafe { inner::power_spectrum(complex, out) }
}
#[inline]
pub unsafe fn cmvn_apply(feature: &mut [f32], means: &[f32], istd: &[f32]) {
unsafe { inner::cmvn_apply(feature, means, istd) }
}
#[inline]
pub unsafe fn mel_dot_log(power_slice: &[f32], weights: &[f32]) -> f32 {
unsafe { inner::mel_dot_log(power_slice, weights) }
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[allow(unsafe_code)]
pub mod x86_avx512 {
use super::super::arch::x86_avx512 as inner;
#[inline]
pub unsafe fn dc_remove(window: &[f32], out: &mut [f32]) {
unsafe { inner::dc_remove(window, out) }
}
#[inline]
pub unsafe fn window_apply(samples: &mut [f32], window: &[f32]) {
unsafe { inner::window_apply(samples, window) }
}
#[inline]
pub unsafe fn power_spectrum(complex: &[rustfft::num_complex::Complex<f32>], out: &mut [f32]) {
unsafe { inner::power_spectrum(complex, out) }
}
#[inline]
pub unsafe fn cmvn_apply(feature: &mut [f32], means: &[f32], istd: &[f32]) {
unsafe { inner::cmvn_apply(feature, means, istd) }
}
#[inline]
pub unsafe fn mel_dot_log(power_slice: &[f32], weights: &[f32]) -> f32 {
unsafe { inner::mel_dot_log(power_slice, weights) }
}
}
}
use crate::error::{Error, Result};
pub(crate) const NUM_MEL_BINS: usize = 80;
pub(crate) const SAMPLE_RATE_HZ: u32 = 16_000;
pub(crate) const FRAME_LENGTH_SAMPLES: usize = 400;
pub(crate) const FRAME_SHIFT_SAMPLES: usize = 160;
pub(crate) const FFT_SIZE: usize = 512;
pub(crate) const FFT_BINS: usize = FFT_SIZE / 2 + 1;
pub(crate) const PRE_EMPHASIS: f32 = 0.97;
const MEL_LOW_FREQ_HZ: f32 = 20.0;
const MEL_HIGH_FREQ_HZ: f32 = 8_000.0;
pub(crate) const LOG_FLOOR: f32 = f32::EPSILON;
pub(crate) const INT16_SCALE: f32 = 32_768.0;
#[derive(Debug, Clone)]
struct MelFilter {
start_bin: usize,
weights: Vec<f32>,
}
pub(crate) struct MelFilterbank {
fft: rustfft::algorithm::Radix4<f32>,
fft_buf: Vec<rustfft::num_complex::Complex<f32>>,
povey_window: Vec<f32>,
filters: Vec<MelFilter>,
samples_scratch: Vec<f32>,
power_scratch: Vec<f32>,
}
impl std::fmt::Debug for MelFilterbank {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MelFilterbank")
.field("fft_buf_len", &self.fft_buf.len())
.field("povey_window_len", &self.povey_window.len())
.field("filters_len", &self.filters.len())
.finish()
}
}
#[derive(Debug, Clone)]
pub(crate) struct Cmvn {
means: Vec<f32>,
inverse_std_variances: Vec<f32>,
}
impl Cmvn {
pub(crate) fn from_ark_bytes(bytes: &[u8]) -> Result<Self> {
let mut i: usize = 0;
let need = |i: usize, n: usize| -> Result<()> {
if bytes.len() < i + n {
Err(Error::InvalidCmvn {
reason: "truncated header",
})
} else {
Ok(())
}
};
need(i, 2)?;
if &bytes[i..i + 2] != b"\x00B" {
return Err(Error::InvalidCmvn {
reason: "missing \\0B magic",
});
}
i += 2;
need(i, 3)?;
if &bytes[i..i + 3] != b"DM " {
return Err(Error::InvalidCmvn {
reason: "expected double-matrix marker 'DM '",
});
}
i += 3;
need(i, 1)?;
if bytes[i] != 4 {
return Err(Error::InvalidCmvn {
reason: "expected 4-byte int32 size token before rows",
});
}
i += 1;
need(i, 4)?;
let rows = i32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
i += 4;
if rows != 2 {
return Err(Error::InvalidCmvn {
reason: "expected exactly 2 rows (sums, sum_sqs)",
});
}
need(i, 1)?;
if bytes[i] != 4 {
return Err(Error::InvalidCmvn {
reason: "expected 4-byte int32 size token before cols",
});
}
i += 1;
need(i, 4)?;
let cols = i32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
i += 4;
if cols != (NUM_MEL_BINS as i32) + 1 {
return Err(Error::InvalidCmvn {
reason: "expected NUM_MEL_BINS + 1 columns",
});
}
let total = (rows as usize) * (cols as usize) * 8;
need(i, total)?;
let mut data = Vec::with_capacity((rows as usize) * (cols as usize));
let mut p = i;
for _ in 0..(rows as usize) * (cols as usize) {
let chunk = [
bytes[p],
bytes[p + 1],
bytes[p + 2],
bytes[p + 3],
bytes[p + 4],
bytes[p + 5],
bytes[p + 6],
bytes[p + 7],
];
data.push(f64::from_le_bytes(chunk));
p += 8;
}
let count = data[NUM_MEL_BINS]; if !(count.is_finite() && count >= 1.0) {
return Err(Error::InvalidCmvn {
reason: "non-positive CMVN count",
});
}
let mut means = Vec::with_capacity(NUM_MEL_BINS);
let mut inverse_std_variances = Vec::with_capacity(NUM_MEL_BINS);
let row_stride = cols as usize;
for d in 0..NUM_MEL_BINS {
let sum = data[d];
let sum_sq = data[row_stride + d];
if !sum.is_finite() || !sum_sq.is_finite() {
return Err(Error::InvalidCmvn {
reason: "non-finite stat (NaN or Inf) in CMVN matrix",
});
}
let mean = sum / count;
let mut var = sum_sq / count - mean * mean;
if var < 1e-20 {
var = 1e-20;
}
let istd = 1.0 / var.sqrt();
let mean_f32 = mean as f32;
let istd_f32 = istd as f32;
if !mean_f32.is_finite() || !istd_f32.is_finite() {
return Err(Error::InvalidCmvn {
reason: "non-finite mean or inverse-std after normalization",
});
}
means.push(mean_f32);
inverse_std_variances.push(istd_f32);
}
Ok(Self {
means,
inverse_std_variances,
})
}
#[cfg(test)]
pub(crate) fn from_components(means: Vec<f32>, inverse_std_variances: Vec<f32>) -> Self {
debug_assert_eq!(means.len(), NUM_MEL_BINS);
debug_assert_eq!(inverse_std_variances.len(), NUM_MEL_BINS);
Self {
means,
inverse_std_variances,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn apply(&self, feature: &mut [f32]) {
dispatch::cmvn_apply(feature, &self.means, &self.inverse_std_variances);
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn hz_to_mel(hz: f32) -> f32 {
1127.0 * (1.0 + hz / 700.0).ln()
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn fft_bin_hz(i: usize) -> f32 {
(i as f32) * (SAMPLE_RATE_HZ as f32) / (FFT_SIZE as f32)
}
fn build_povey_window() -> Vec<f32> {
let n = FRAME_LENGTH_SAMPLES;
let a: f64 = std::f64::consts::TAU / ((n - 1) as f64);
(0..n)
.map(|i| {
let i_fl = i as f64;
((0.5 - 0.5 * (a * i_fl).cos()).powf(0.85)) as f32
})
.collect()
}
fn build_mel_filters() -> Vec<MelFilter> {
let mel_low = hz_to_mel(MEL_LOW_FREQ_HZ);
let mel_high = hz_to_mel(MEL_HIGH_FREQ_HZ);
let mel_freq_delta = (mel_high - mel_low) / (NUM_MEL_BINS as f32 + 1.0);
let num_fft_bins = FFT_SIZE / 2;
let mut filters = Vec::with_capacity(NUM_MEL_BINS);
for b in 0..NUM_MEL_BINS {
let left_mel = mel_low + (b as f32) * mel_freq_delta;
let center_mel = mel_low + (b as f32 + 1.0) * mel_freq_delta;
let right_mel = mel_low + (b as f32 + 2.0) * mel_freq_delta;
let mut start_bin = num_fft_bins;
let mut end_bin: usize = 0;
let mut found_any = false;
for i in 0..num_fft_bins {
let freq = fft_bin_hz(i);
let mel = hz_to_mel(freq);
if mel > left_mel && mel < right_mel {
if !found_any {
start_bin = i;
found_any = true;
}
end_bin = i;
}
}
let mut weights = Vec::new();
if found_any {
weights.reserve(end_bin - start_bin + 1);
for i in start_bin..=end_bin {
let freq = fft_bin_hz(i);
let mel = hz_to_mel(freq);
let w = if mel <= center_mel {
(mel - left_mel) / (center_mel - left_mel)
} else {
(right_mel - mel) / (right_mel - center_mel)
};
weights.push(w);
}
} else {
start_bin = 0;
}
filters.push(MelFilter { start_bin, weights });
}
filters
}
impl MelFilterbank {
pub(crate) fn new() -> Self {
use rustfft::FftDirection;
Self {
fft: rustfft::algorithm::Radix4::<f32>::new(FFT_SIZE, FftDirection::Forward),
fft_buf: vec![rustfft::num_complex::Complex::new(0.0, 0.0); FFT_SIZE],
povey_window: build_povey_window(),
filters: build_mel_filters(),
samples_scratch: vec![0.0; FRAME_LENGTH_SAMPLES],
power_scratch: vec![0.0; FFT_BINS],
}
}
pub(crate) fn extract(&mut self, window: &[f32], out: &mut [f32]) {
debug_assert_eq!(window.len(), FRAME_LENGTH_SAMPLES);
debug_assert_eq!(out.len(), NUM_MEL_BINS);
dispatch::dc_remove(window, &mut self.samples_scratch);
dispatch::pre_emphasis(&mut self.samples_scratch);
dispatch::window_apply(&mut self.samples_scratch, &self.povey_window);
for (buf, &s) in self.fft_buf.iter_mut().zip(self.samples_scratch.iter()) {
buf.re = s;
buf.im = 0.0;
}
for buf in &mut self.fft_buf[FRAME_LENGTH_SAMPLES..] {
buf.re = 0.0;
buf.im = 0.0;
}
use rustfft::Fft;
self.fft.process(&mut self.fft_buf);
dispatch::power_spectrum(&self.fft_buf[..FFT_BINS], &mut self.power_scratch);
for (out_val, f) in out.iter_mut().zip(self.filters.iter()) {
let bins = &self.power_scratch[f.start_bin..f.start_bin + f.weights.len()];
*out_val = dispatch::mel_dot_log(bins, &f.weights);
}
}
}
#[derive(Debug)]
pub(crate) struct FeatureExtractor {
fbank: MelFilterbank,
cmvn: Cmvn,
pcm_tail: Vec<f32>,
feature_scratch: Vec<f32>,
}
impl FeatureExtractor {
pub(crate) fn new(cmvn_bytes: &[u8]) -> Result<Self> {
Ok(Self {
fbank: MelFilterbank::new(),
cmvn: Cmvn::from_ark_bytes(cmvn_bytes)?,
pcm_tail: Vec::with_capacity(FRAME_LENGTH_SAMPLES),
feature_scratch: vec![0.0; NUM_MEL_BINS],
})
}
pub(crate) fn reset(&mut self) {
self.pcm_tail.clear();
}
pub(crate) fn push_pcm(&mut self, pcm: &[f32]) {
let offset = self.pcm_tail.len();
self.pcm_tail.resize(offset + pcm.len(), 0.0);
dispatch::pcm_scale_extend(pcm, &mut self.pcm_tail[offset..]);
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn pending_samples(&self) -> usize {
self.pcm_tail.len()
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn has_full_window(&self) -> bool {
self.pcm_tail.len() >= FRAME_LENGTH_SAMPLES
}
pub(crate) fn extract_one(&mut self, out: &mut [f32]) {
debug_assert_eq!(out.len(), NUM_MEL_BINS);
debug_assert!(self.has_full_window());
self.fbank.extract(
&self.pcm_tail[..FRAME_LENGTH_SAMPLES],
&mut self.feature_scratch,
);
self.cmvn.apply(&mut self.feature_scratch);
out.copy_from_slice(&self.feature_scratch);
self.pcm_tail.drain(..FRAME_SHIFT_SAMPLES);
}
}
#[cfg(test)]
mod tests {
use super::*;
const BUNDLED_CMVN: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/models/cmvn.ark"));
#[test]
fn parses_bundled_cmvn_into_80_means_and_istds() {
let cmvn = Cmvn::from_ark_bytes(BUNDLED_CMVN).expect("parse cmvn");
let mut probe = vec![0.0f32; NUM_MEL_BINS];
let baseline = probe.clone();
cmvn.apply(&mut probe);
assert_ne!(probe, baseline);
for v in &probe {
assert!(v.is_finite(), "non-finite CMVN output: {v}");
}
}
#[test]
fn rejects_truncated_input() {
let bytes = b"\x00BDM ";
assert!(matches!(
Cmvn::from_ark_bytes(bytes),
Err(Error::InvalidCmvn { .. })
));
}
#[test]
fn rejects_wrong_magic() {
let mut bytes = b"\x00BDM \x04\x02\x00\x00\x00\x04\x51\x00\x00\x00".to_vec();
bytes[0] = 0xFF;
assert!(matches!(
Cmvn::from_ark_bytes(&bytes),
Err(Error::InvalidCmvn { reason: r }) if r.contains("magic")
));
}
#[test]
fn apply_subtracts_mean_and_divides_by_std() {
let cmvn = Cmvn::from_components(vec![1.0; NUM_MEL_BINS], vec![2.0; NUM_MEL_BINS]);
let mut feature = vec![3.0f32; NUM_MEL_BINS];
cmvn.apply(&mut feature);
for value in &feature {
assert!((*value - 4.0).abs() < f32::EPSILON);
}
}
#[test]
fn povey_window_endpoints_are_zero_and_centre_is_one() {
let w = build_povey_window();
assert_eq!(w.len(), FRAME_LENGTH_SAMPLES);
assert!(w[0].abs() < 1e-6);
assert!(w[FRAME_LENGTH_SAMPLES - 1].abs() < 1e-6);
let centre = (FRAME_LENGTH_SAMPLES - 1) / 2;
assert!(
(w[centre] - 1.0).abs() < 1e-3,
"centre weight = {}",
w[centre]
);
}
#[test]
fn mel_filters_cover_the_target_frequency_range() {
let filters = build_mel_filters();
assert_eq!(filters.len(), NUM_MEL_BINS);
let first_centre_hz = fft_bin_hz(filters[0].start_bin + filters[0].weights.len() / 2);
assert!(first_centre_hz > MEL_LOW_FREQ_HZ);
assert!(first_centre_hz < 200.0);
let last = &filters[NUM_MEL_BINS - 1];
let last_max_bin = last.start_bin + last.weights.len();
assert!(fft_bin_hz(last_max_bin) > 7_000.0);
}
#[test]
fn mel_filterbank_silence_produces_log_floor_features() {
let mut bank = MelFilterbank::new();
let window = vec![0.0f32; FRAME_LENGTH_SAMPLES];
let mut out = vec![0.0f32; NUM_MEL_BINS];
bank.extract(&window, &mut out);
let log_floor = LOG_FLOOR.ln();
for v in &out {
assert!(
(*v - log_floor).abs() < 1e-3,
"expected log_floor, got {}",
v
);
}
}
#[test]
fn mel_filterbank_responds_to_a_pure_tone() {
let mut bank = MelFilterbank::new();
let mut window = vec![0.0f32; FRAME_LENGTH_SAMPLES];
let f = 1_000.0f32;
let amp = 8_000.0f32;
for (n, w) in window.iter_mut().enumerate() {
*w = amp * (std::f32::consts::TAU * f * (n as f32) / SAMPLE_RATE_HZ as f32).sin();
}
let mut out = vec![0.0f32; NUM_MEL_BINS];
bank.extract(&window, &mut out);
let max_bin = (0..NUM_MEL_BINS)
.max_by(|a, b| out[*a].partial_cmp(&out[*b]).unwrap())
.unwrap();
assert!((20..40).contains(&max_bin), "peak Mel bin = {max_bin}");
}
const BUNDLED_CMVN_BYTES: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/models/cmvn.ark"));
#[test]
fn feature_extractor_buffers_partial_frames() {
let mut fx = FeatureExtractor::new(BUNDLED_CMVN_BYTES).expect("init");
fx.push_pcm(&vec![0.0; 100]);
assert!(!fx.has_full_window());
assert_eq!(fx.pending_samples(), 100);
fx.push_pcm(&vec![0.0; 300]);
assert!(fx.has_full_window());
assert_eq!(fx.pending_samples(), 400);
let mut out = vec![0.0; NUM_MEL_BINS];
fx.extract_one(&mut out);
assert_eq!(fx.pending_samples(), 240);
}
#[test]
fn feature_extractor_emits_consistent_features_for_silence() {
let mut fx = FeatureExtractor::new(BUNDLED_CMVN_BYTES).expect("init");
fx.push_pcm(&vec![0.0; FRAME_LENGTH_SAMPLES + 3 * FRAME_SHIFT_SAMPLES]);
let mut a = vec![0.0; NUM_MEL_BINS];
let mut b = vec![0.0; NUM_MEL_BINS];
fx.extract_one(&mut a);
fx.extract_one(&mut b);
assert_eq!(
a, b,
"two consecutive silence frames must produce identical features"
);
}
#[test]
fn feature_extractor_reset_clears_pending() {
let mut fx = FeatureExtractor::new(BUNDLED_CMVN_BYTES).expect("init");
fx.push_pcm(&vec![0.0; 100]);
fx.reset();
assert_eq!(fx.pending_samples(), 0);
assert!(!fx.has_full_window());
}
}