use std::f32::consts::PI;
use realfft::{RealFftPlanner, RealToComplex};
use rustfft::num_complex::Complex;
pub const DEFAULT_SAMPLE_RATE: u32 = 16000;
pub const DEFAULT_FRAME_SIZE: usize = 400;
pub const DEFAULT_FRAME_SHIFT: usize = 160;
pub const DEFAULT_NUM_MELS: usize = 40;
pub const DEFAULT_NUM_MFCC: usize = 13;
pub const DEFAULT_PRE_EMPHASIS: f32 = 0.97;
pub const DEFAULT_LOW_FREQ: f32 = 20.0;
pub const DEFAULT_HIGH_FREQ: f32 = 8000.0;
pub const LOG_EPSILON: f32 = 1e-10;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WindowType {
Hanning,
Hamming,
Rectangular,
Blackman,
}
impl Default for WindowType {
fn default() -> Self {
Self::Hanning
}
}
#[derive(Clone, Debug)]
pub struct FeatureConfig {
pub sample_rate: u32,
pub frame_size: usize,
pub frame_shift: usize,
pub fft_size: usize,
pub num_mels: usize,
pub num_mfcc: usize,
pub pre_emphasis: f32,
pub window_type: WindowType,
pub low_freq: f32,
pub high_freq: f32,
pub use_power: bool,
pub normalize_mean: bool,
pub normalize_variance: bool,
pub include_delta: bool,
pub include_delta_delta: bool,
pub delta_window: usize,
}
impl Default for FeatureConfig {
fn default() -> Self {
let frame_size = DEFAULT_FRAME_SIZE;
Self {
sample_rate: DEFAULT_SAMPLE_RATE,
frame_size,
frame_shift: DEFAULT_FRAME_SHIFT,
fft_size: frame_size.next_power_of_two(),
num_mels: DEFAULT_NUM_MELS,
num_mfcc: DEFAULT_NUM_MFCC,
pre_emphasis: DEFAULT_PRE_EMPHASIS,
window_type: WindowType::Hanning,
low_freq: DEFAULT_LOW_FREQ,
high_freq: DEFAULT_HIGH_FREQ,
use_power: true,
normalize_mean: true,
normalize_variance: false,
include_delta: false,
include_delta_delta: false,
delta_window: 2,
}
}
}
impl FeatureConfig {
pub fn telephony() -> Self {
Self {
sample_rate: 8000,
frame_size: 200, frame_shift: 80, fft_size: 256,
high_freq: 4000.0, ..Default::default()
}
}
pub fn wideband() -> Self {
Self::default()
}
pub fn music() -> Self {
Self {
sample_rate: 44100,
frame_size: 1102, frame_shift: 441, fft_size: 2048,
high_freq: 22050.0, num_mels: 80, ..Default::default()
}
}
pub fn frame_duration_ms(&self) -> f32 {
1000.0 * self.frame_size as f32 / self.sample_rate as f32
}
pub fn frame_shift_ms(&self) -> f32 {
1000.0 * self.frame_shift as f32 / self.sample_rate as f32
}
pub fn feature_dim(&self) -> usize {
let base = self.num_mels;
let mut dim = base;
if self.include_delta {
dim += base;
}
if self.include_delta_delta {
dim += base;
}
dim
}
}
#[derive(Clone, Debug)]
pub struct MelFilterbank {
num_mels: usize,
fft_size: usize,
sample_rate: u32,
low_freq: f32,
high_freq: f32,
filters: Vec<MelFilter>,
}
#[derive(Clone, Debug)]
struct MelFilter {
start_bin: usize,
weights: Vec<f32>,
}
impl MelFilterbank {
pub fn new(
num_mels: usize,
fft_size: usize,
sample_rate: u32,
low_freq: f32,
high_freq: f32,
) -> Self {
let mut filterbank = Self {
num_mels,
fft_size,
sample_rate,
low_freq,
high_freq,
filters: Vec::with_capacity(num_mels),
};
filterbank.build_filters();
filterbank
}
#[inline]
pub fn hz_to_mel(hz: f32) -> f32 {
2595.0 * (1.0 + hz / 700.0).log10()
}
#[inline]
pub fn mel_to_hz(mel: f32) -> f32 {
700.0 * (10.0_f32.powf(mel / 2595.0) - 1.0)
}
fn build_filters(&mut self) {
let num_fft_bins = self.fft_size / 2 + 1;
let hz_per_bin = self.sample_rate as f32 / self.fft_size as f32;
let low_mel = Self::hz_to_mel(self.low_freq);
let high_mel = Self::hz_to_mel(self.high_freq);
let num_points = self.num_mels + 2;
let mel_points: Vec<f32> = (0..num_points)
.map(|i| low_mel + (high_mel - low_mel) * i as f32 / (num_points - 1) as f32)
.collect();
let hz_points: Vec<f32> = mel_points.iter().map(|&m| Self::mel_to_hz(m)).collect();
let bin_points: Vec<f32> = hz_points.iter().map(|&f| f / hz_per_bin).collect();
self.filters.clear();
for m in 0..self.num_mels {
let left = bin_points[m];
let center = bin_points[m + 1];
let right = bin_points[m + 2];
let start_bin = left.floor() as usize;
let end_bin = (right.ceil() as usize).min(num_fft_bins);
if start_bin >= end_bin {
self.filters.push(MelFilter {
start_bin: 0,
weights: vec![],
});
continue;
}
let mut weights = Vec::with_capacity(end_bin - start_bin);
for bin in start_bin..end_bin {
let bin_f = bin as f32;
let weight = if bin_f < left {
0.0
} else if bin_f < center {
(bin_f - left) / (center - left)
} else if bin_f < right {
(right - bin_f) / (right - center)
} else {
0.0
};
weights.push(weight);
}
self.filters.push(MelFilter { start_bin, weights });
}
}
pub fn apply(&self, spectrum: &[f32]) -> Vec<f32> {
let mut mel_energies = vec![0.0f32; self.num_mels];
for (m, filter) in self.filters.iter().enumerate() {
let mut energy = 0.0f32;
for (i, &weight) in filter.weights.iter().enumerate() {
let bin = filter.start_bin + i;
if bin < spectrum.len() {
energy += weight * spectrum[bin];
}
}
mel_energies[m] = energy;
}
mel_energies
}
pub fn num_mels(&self) -> usize {
self.num_mels
}
pub fn to_dense(&self) -> Vec<Vec<f32>> {
let num_fft_bins = self.fft_size / 2 + 1;
let mut dense = vec![vec![0.0f32; num_fft_bins]; self.num_mels];
for (m, filter) in self.filters.iter().enumerate() {
for (i, &weight) in filter.weights.iter().enumerate() {
let bin = filter.start_bin + i;
if bin < num_fft_bins {
dense[m][bin] = weight;
}
}
}
dense
}
}
#[derive(Clone, Debug)]
struct DctTransform {
num_input: usize,
num_output: usize,
matrix: Vec<Vec<f32>>,
}
impl DctTransform {
fn new(num_input: usize, num_output: usize) -> Self {
let mut matrix = vec![vec![0.0f32; num_input]; num_output];
let scale = (2.0 / num_input as f32).sqrt();
for k in 0..num_output {
for n in 0..num_input {
matrix[k][n] = scale * (PI * k as f32 * (n as f32 + 0.5) / num_input as f32).cos();
}
}
if !matrix.is_empty() {
let scale0 = (1.0 / num_input as f32).sqrt();
for n in 0..num_input {
matrix[0][n] = scale0;
}
}
Self {
num_input,
num_output,
matrix,
}
}
fn apply(&self, input: &[f32]) -> Vec<f32> {
let mut output = vec![0.0f32; self.num_output];
for k in 0..self.num_output {
let mut sum = 0.0f32;
for (n, &x) in input.iter().enumerate().take(self.num_input) {
sum += self.matrix[k][n] * x;
}
output[k] = sum;
}
output
}
}
pub struct FeatureExtractor {
config: FeatureConfig,
window: Vec<f32>,
filterbank: MelFilterbank,
dct: DctTransform,
fft: std::sync::Arc<dyn RealToComplex<f32>>,
}
impl FeatureExtractor {
pub fn new(config: FeatureConfig) -> Self {
let window = Self::build_window(config.frame_size, config.window_type);
let filterbank = MelFilterbank::new(
config.num_mels,
config.fft_size,
config.sample_rate,
config.low_freq,
config.high_freq,
);
let dct = DctTransform::new(config.num_mels, config.num_mfcc);
let mut planner = RealFftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(config.fft_size);
Self {
config,
window,
filterbank,
dct,
fft,
}
}
pub fn config(&self) -> &FeatureConfig {
&self.config
}
fn build_window(size: usize, window_type: WindowType) -> Vec<f32> {
(0..size)
.map(|n| {
let x = 2.0 * PI * n as f32 / (size - 1) as f32;
match window_type {
WindowType::Hanning => 0.5 * (1.0 - x.cos()),
WindowType::Hamming => 0.54 - 0.46 * x.cos(),
WindowType::Rectangular => 1.0,
WindowType::Blackman => 0.42 - 0.5 * x.cos() + 0.08 * (2.0 * x).cos(),
}
})
.collect()
}
fn apply_pre_emphasis(&self, audio: &[f32]) -> Vec<f32> {
if self.config.pre_emphasis == 0.0 || audio.is_empty() {
return audio.to_vec();
}
let mut output = Vec::with_capacity(audio.len());
output.push(audio[0]);
for i in 1..audio.len() {
output.push(audio[i] - self.config.pre_emphasis * audio[i - 1]);
}
output
}
fn extract_frame(&self, audio: &[f32], start: usize) -> Vec<f32> {
let mut frame = vec![0.0f32; self.config.fft_size];
for i in 0..self.config.frame_size {
let sample_idx = start + i;
if sample_idx < audio.len() {
frame[i] = audio[sample_idx] * self.window[i];
}
}
frame
}
fn compute_spectrum(&self, frame: &mut [f32]) -> Vec<f32> {
let mut spectrum = vec![Complex::new(0.0f32, 0.0f32); self.config.fft_size / 2 + 1];
self.fft.process(frame, &mut spectrum).expect("FFT failed");
spectrum
.iter()
.map(|c| {
if self.config.use_power {
c.norm_sqr() } else {
c.norm() }
})
.collect()
}
fn compute_delta(&self, features: &[Vec<f32>], window: usize) -> Vec<Vec<f32>> {
let num_frames = features.len();
if num_frames == 0 {
return vec![];
}
let dim = features[0].len();
let mut delta = vec![vec![0.0f32; dim]; num_frames];
let norm: f32 = 2.0 * (1..=window).map(|n| (n * n) as f32).sum::<f32>();
for t in 0..num_frames {
for d in 0..dim {
let mut sum = 0.0f32;
for n in 1..=window {
let t_minus = if t >= n { t - n } else { 0 };
let t_plus = (t + n).min(num_frames - 1);
sum += n as f32 * (features[t_plus][d] - features[t_minus][d]);
}
delta[t][d] = sum / norm;
}
}
delta
}
fn normalize(&self, features: &mut [Vec<f32>]) {
if features.is_empty() {
return;
}
let num_frames = features.len();
let dim = features[0].len();
if self.config.normalize_mean {
let mut mean = vec![0.0f32; dim];
for frame in features.iter() {
for (d, &v) in frame.iter().enumerate() {
mean[d] += v;
}
}
for m in mean.iter_mut() {
*m /= num_frames as f32;
}
for frame in features.iter_mut() {
for (d, v) in frame.iter_mut().enumerate() {
*v -= mean[d];
}
}
}
if self.config.normalize_variance {
let mut var = vec![0.0f32; dim];
for frame in features.iter() {
for (d, &v) in frame.iter().enumerate() {
var[d] += v * v;
}
}
for v in var.iter_mut() {
*v = (*v / num_frames as f32).sqrt().max(1e-10);
}
for frame in features.iter_mut() {
for (d, v) in frame.iter_mut().enumerate() {
*v /= var[d];
}
}
}
}
pub fn extract_filterbank(&self, audio: &[f32]) -> Vec<Vec<f32>> {
if audio.is_empty() {
return vec![];
}
let emphasized = self.apply_pre_emphasis(audio);
let num_frames = if emphasized.len() > self.config.frame_size {
(emphasized.len() - self.config.frame_size) / self.config.frame_shift + 1
} else {
1
};
let mut features: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * self.config.frame_shift;
let mut frame = self.extract_frame(&emphasized, start);
let spectrum = self.compute_spectrum(&mut frame);
let mel_energies = self.filterbank.apply(&spectrum);
let log_mel: Vec<f32> = mel_energies
.iter()
.map(|&e| (e + LOG_EPSILON).ln())
.collect();
features.push(log_mel);
}
self.normalize(&mut features);
if self.config.include_delta || self.config.include_delta_delta {
let delta = self.compute_delta(&features, self.config.delta_window);
if self.config.include_delta_delta {
let delta_delta = self.compute_delta(&delta, self.config.delta_window);
for (i, frame) in features.iter_mut().enumerate() {
frame.extend_from_slice(&delta[i]);
frame.extend_from_slice(&delta_delta[i]);
}
} else {
for (i, frame) in features.iter_mut().enumerate() {
frame.extend_from_slice(&delta[i]);
}
}
}
features
}
pub fn extract_mfcc(&self, audio: &[f32]) -> Vec<Vec<f32>> {
if audio.is_empty() {
return vec![];
}
let emphasized = self.apply_pre_emphasis(audio);
let num_frames = if emphasized.len() > self.config.frame_size {
(emphasized.len() - self.config.frame_size) / self.config.frame_shift + 1
} else {
1
};
let mut features: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * self.config.frame_shift;
let mut frame = self.extract_frame(&emphasized, start);
let spectrum = self.compute_spectrum(&mut frame);
let mel_energies = self.filterbank.apply(&spectrum);
let log_mel: Vec<f32> = mel_energies
.iter()
.map(|&e| (e + LOG_EPSILON).ln())
.collect();
let mfcc = self.dct.apply(&log_mel);
features.push(mfcc);
}
self.normalize(&mut features);
if self.config.include_delta || self.config.include_delta_delta {
let delta = self.compute_delta(&features, self.config.delta_window);
if self.config.include_delta_delta {
let delta_delta = self.compute_delta(&delta, self.config.delta_window);
for (i, frame) in features.iter_mut().enumerate() {
frame.extend_from_slice(&delta[i]);
frame.extend_from_slice(&delta_delta[i]);
}
} else {
for (i, frame) in features.iter_mut().enumerate() {
frame.extend_from_slice(&delta[i]);
}
}
}
features
}
pub fn extract_spectrogram(&self, audio: &[f32]) -> Vec<Vec<f32>> {
if audio.is_empty() {
return vec![];
}
let emphasized = self.apply_pre_emphasis(audio);
let num_frames = if emphasized.len() > self.config.frame_size {
(emphasized.len() - self.config.frame_size) / self.config.frame_shift + 1
} else {
1
};
let mut spectrogram: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * self.config.frame_shift;
let mut frame = self.extract_frame(&emphasized, start);
let spectrum = self.compute_spectrum(&mut frame);
spectrogram.push(spectrum);
}
spectrogram
}
pub fn extract_log_mel(&self, audio: &[f32]) -> Vec<Vec<f32>> {
if audio.is_empty() {
return vec![];
}
let emphasized = self.apply_pre_emphasis(audio);
let num_frames = if emphasized.len() > self.config.frame_size {
(emphasized.len() - self.config.frame_size) / self.config.frame_shift + 1
} else {
1
};
let mut features: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * self.config.frame_shift;
let mut frame = self.extract_frame(&emphasized, start);
let spectrum = self.compute_spectrum(&mut frame);
let mel_energies = self.filterbank.apply(&spectrum);
let log_mel: Vec<f32> = mel_energies
.iter()
.map(|&e| (e + LOG_EPSILON).ln())
.collect();
features.push(log_mel);
}
features
}
pub fn num_frames(&self, audio_length: usize) -> usize {
if audio_length <= self.config.frame_size {
return if audio_length > 0 { 1 } else { 0 };
}
(audio_length - self.config.frame_size) / self.config.frame_shift + 1
}
pub fn filterbank(&self) -> &MelFilterbank {
&self.filterbank
}
}
pub struct StreamingFeatureExtractor {
extractor: FeatureExtractor,
buffer: Vec<f32>,
samples_processed: usize,
}
impl StreamingFeatureExtractor {
pub fn new(config: FeatureConfig) -> Self {
Self {
extractor: FeatureExtractor::new(config),
buffer: Vec::new(),
samples_processed: 0,
}
}
pub fn config(&self) -> &FeatureConfig {
self.extractor.config()
}
pub fn add_samples(&mut self, samples: &[f32]) -> usize {
self.buffer.extend_from_slice(samples);
self.available_frames()
}
pub fn available_frames(&self) -> usize {
self.extractor.num_frames(self.buffer.len())
}
pub fn extract_filterbank(&mut self) -> Vec<Vec<f32>> {
let num_frames = self.available_frames();
if num_frames == 0 {
return vec![];
}
let features = self.extractor.extract_filterbank(&self.buffer);
let consumed = num_frames * self.extractor.config.frame_shift;
self.buffer.drain(..consumed);
self.samples_processed += consumed;
features
}
pub fn extract_mfcc(&mut self) -> Vec<Vec<f32>> {
let num_frames = self.available_frames();
if num_frames == 0 {
return vec![];
}
let features = self.extractor.extract_mfcc(&self.buffer);
let consumed = num_frames * self.extractor.config.frame_shift;
self.buffer.drain(..consumed);
self.samples_processed += consumed;
features
}
pub fn flush_filterbank(&mut self) -> Vec<Vec<f32>> {
if self.buffer.is_empty() {
return vec![];
}
while self.buffer.len() < self.extractor.config.frame_size {
self.buffer.push(0.0);
}
let features = self.extractor.extract_filterbank(&self.buffer);
self.buffer.clear();
features
}
pub fn flush_mfcc(&mut self) -> Vec<Vec<f32>> {
if self.buffer.is_empty() {
return vec![];
}
while self.buffer.len() < self.extractor.config.frame_size {
self.buffer.push(0.0);
}
let features = self.extractor.extract_mfcc(&self.buffer);
self.buffer.clear();
features
}
pub fn reset(&mut self) {
self.buffer.clear();
self.samples_processed = 0;
}
pub fn samples_processed(&self) -> usize {
self.samples_processed
}
pub fn buffer_len(&self) -> usize {
self.buffer.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mel_scale_conversion() {
assert!((MelFilterbank::hz_to_mel(0.0) - 0.0).abs() < 0.01);
assert!((MelFilterbank::hz_to_mel(1000.0) - 1000.0).abs() < 50.0);
for hz in [100.0, 500.0, 1000.0, 4000.0, 8000.0] {
let mel = MelFilterbank::hz_to_mel(hz);
let hz_back = MelFilterbank::mel_to_hz(mel);
assert!((hz - hz_back).abs() < 0.1, "Round-trip failed for {}", hz);
}
}
#[test]
fn test_mel_filterbank_creation() {
let fb = MelFilterbank::new(40, 512, 16000, 20.0, 8000.0);
assert_eq!(fb.num_mels(), 40);
let dense = fb.to_dense();
for (m, filter) in dense.iter().enumerate() {
let sum: f32 = filter.iter().sum();
assert!(sum > 0.0, "Filter {} has zero weights", m);
}
}
#[test]
fn test_filterbank_apply() {
let fb = MelFilterbank::new(10, 512, 16000, 20.0, 8000.0);
let spectrum = vec![1.0f32; 257];
let mel_energies = fb.apply(&spectrum);
assert_eq!(mel_energies.len(), 10);
for e in &mel_energies {
assert!(*e > 0.0);
}
}
#[test]
fn test_feature_config_defaults() {
let config = FeatureConfig::default();
assert_eq!(config.sample_rate, 16000);
assert_eq!(config.frame_size, 400);
assert_eq!(config.frame_shift, 160);
assert_eq!(config.num_mels, 40);
assert_eq!(config.num_mfcc, 13);
}
#[test]
fn test_feature_extractor_creation() {
let config = FeatureConfig::default();
let extractor = FeatureExtractor::new(config);
assert_eq!(extractor.config().num_mels, 40);
}
#[test]
fn test_extract_filterbank() {
let config = FeatureConfig {
normalize_mean: false,
normalize_variance: false,
..Default::default()
};
let extractor = FeatureExtractor::new(config);
let audio: Vec<f32> = (0..16000)
.map(|i| (2.0 * PI * 440.0 * i as f32 / 16000.0).sin())
.collect();
let features = extractor.extract_filterbank(&audio);
assert!(!features.is_empty());
assert!(features.len() >= 90 && features.len() <= 100);
assert_eq!(features[0].len(), 40);
}
#[test]
fn test_extract_mfcc() {
let config = FeatureConfig {
normalize_mean: false,
..Default::default()
};
let extractor = FeatureExtractor::new(config);
let audio: Vec<f32> = (0..16000)
.map(|i| (2.0 * PI * 440.0 * i as f32 / 16000.0).sin())
.collect();
let features = extractor.extract_mfcc(&audio);
assert!(!features.is_empty());
assert_eq!(features[0].len(), 13);
}
#[test]
fn test_extract_spectrogram() {
let config = FeatureConfig::default();
let fft_size = config.fft_size;
let extractor = FeatureExtractor::new(config);
let audio: Vec<f32> = (0..8000)
.map(|i| (2.0 * PI * 1000.0 * i as f32 / 16000.0).sin())
.collect();
let spectrogram = extractor.extract_spectrogram(&audio);
assert!(!spectrogram.is_empty());
assert_eq!(spectrogram[0].len(), fft_size / 2 + 1);
}
#[test]
fn test_delta_features() {
let config = FeatureConfig {
include_delta: true,
include_delta_delta: true,
normalize_mean: false,
..Default::default()
};
let extractor = FeatureExtractor::new(config);
let audio: Vec<f32> = (0..16000)
.map(|i| (2.0 * PI * 440.0 * i as f32 / 16000.0).sin())
.collect();
let features = extractor.extract_filterbank(&audio);
assert_eq!(features[0].len(), 120);
}
#[test]
fn test_streaming_extractor() {
let config = FeatureConfig::default();
let mut streaming = StreamingFeatureExtractor::new(config);
let audio: Vec<f32> = (0..8000)
.map(|i| (2.0 * PI * 440.0 * i as f32 / 16000.0).sin())
.collect();
let frames1 = streaming.add_samples(&audio[..4000]);
assert!(frames1 > 0);
let features1 = streaming.extract_filterbank();
assert_eq!(features1.len(), frames1);
let frames2 = streaming.add_samples(&audio[4000..]);
assert!(frames2 > 0);
let features2 = streaming.extract_filterbank();
assert_eq!(features2.len(), frames2);
let final_features = streaming.flush_filterbank();
assert!(final_features.len() <= 1);
}
#[test]
fn test_empty_audio() {
let config = FeatureConfig::default();
let extractor = FeatureExtractor::new(config);
let features = extractor.extract_filterbank(&[]);
assert!(features.is_empty());
let mfcc = extractor.extract_mfcc(&[]);
assert!(mfcc.is_empty());
}
#[test]
fn test_short_audio() {
let config = FeatureConfig::default();
let extractor = FeatureExtractor::new(config);
let audio = vec![0.5f32; 200];
let features = extractor.extract_filterbank(&audio);
assert_eq!(features.len(), 1); }
#[test]
fn test_pre_emphasis() {
let config = FeatureConfig {
pre_emphasis: 0.97,
..Default::default()
};
let extractor = FeatureExtractor::new(config);
let audio = vec![1.0f32; 100];
let emphasized = extractor.apply_pre_emphasis(&audio);
assert_eq!(emphasized[0], 1.0);
assert!((emphasized[1] - 0.03).abs() < 0.01);
}
#[test]
fn test_window_functions() {
let size = 400;
let hanning = FeatureExtractor::build_window(size, WindowType::Hanning);
assert!(hanning[0].abs() < 0.01);
assert!(hanning[size - 1].abs() < 0.01);
assert!((hanning[size / 2] - 1.0).abs() < 0.01);
let hamming = FeatureExtractor::build_window(size, WindowType::Hamming);
assert!((hamming[0] - 0.08).abs() < 0.01);
let rect = FeatureExtractor::build_window(size, WindowType::Rectangular);
assert!(rect.iter().all(|&w| (w - 1.0).abs() < 0.001));
}
#[test]
fn test_num_frames_calculation() {
let config = FeatureConfig::default();
let extractor = FeatureExtractor::new(config);
let num_frames = extractor.num_frames(16000);
assert_eq!(num_frames, 98);
let num_frames_single = extractor.num_frames(400);
assert_eq!(num_frames_single, 1);
assert_eq!(extractor.num_frames(0), 0);
}
#[test]
fn test_telephony_config() {
let config = FeatureConfig::telephony();
assert_eq!(config.sample_rate, 8000);
assert_eq!(config.high_freq, 4000.0);
}
#[test]
fn test_music_config() {
let config = FeatureConfig::music();
assert_eq!(config.sample_rate, 44100);
assert_eq!(config.num_mels, 80);
}
}