resonant_analysis/
mfcc.rs1extern crate alloc;
9
10use alloc::vec;
11use alloc::vec::Vec;
12
13use resonant_core::signal::Signal;
14use resonant_core::window;
15use resonant_fft::dct;
16use resonant_fft::stft::Stft;
17use resonant_fft::SignalFreqExt;
18
19use crate::error::AnalysisError;
20use crate::mel::{apply_mel_filterbank, build_mel_filterbank, log_mel_energy};
21
22#[derive(Debug, Clone, PartialEq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct MfccFrame {
35 pub coefficients: Vec<f32>,
37}
38
39#[derive(Debug, Clone)]
55pub struct MfccExtractor {
56 sample_rate: f32,
57 num_coefficients: usize,
58 num_mel_bands: usize,
59 window_size: usize,
60 hop_size: usize,
61}
62
63impl MfccExtractor {
64 #[must_use]
68 pub fn new(sample_rate: f32) -> Self {
69 Self {
70 sample_rate,
71 num_coefficients: 13,
72 num_mel_bands: 26,
73 window_size: 1024,
74 hop_size: 512,
75 }
76 }
77
78 #[must_use]
80 pub fn with_num_coefficients(mut self, n: usize) -> Self {
81 self.num_coefficients = n;
82 self
83 }
84
85 #[must_use]
87 pub fn with_num_mel_bands(mut self, n: usize) -> Self {
88 self.num_mel_bands = n;
89 self
90 }
91
92 #[must_use]
94 pub fn with_window_size(mut self, size: usize) -> Self {
95 self.window_size = size;
96 self
97 }
98
99 #[must_use]
101 pub fn with_hop_size(mut self, hop: usize) -> Self {
102 self.hop_size = hop;
103 self
104 }
105
106 pub fn extract(&self, samples: &[f32]) -> Result<Vec<MfccFrame>, AnalysisError> {
113 if samples.is_empty() {
114 return Err(AnalysisError::EmptyInput);
115 }
116 if self.num_coefficients == 0 || self.num_mel_bands == 0 {
117 return Err(AnalysisError::InvalidParameter {
118 name: "num_coefficients/num_mel_bands",
119 reason: "must be positive",
120 });
121 }
122 if self.num_coefficients > self.num_mel_bands {
123 return Err(AnalysisError::InvalidParameter {
124 name: "num_coefficients",
125 reason: "must not exceed num_mel_bands",
126 });
127 }
128
129 if samples.len() < self.window_size {
131 return Ok(vec![MfccFrame {
132 coefficients: vec![0.0; self.num_coefficients],
133 }]);
134 }
135
136 let signal = Signal::from_samples(samples.to_vec());
137 let stft = Stft::builder(self.window_size, self.hop_size)
138 .window_fn(window::hann)
139 .build();
140
141 let stft_frames = stft.analyze(&signal)?;
142 if stft_frames.is_empty() {
143 return Ok(vec![MfccFrame {
144 coefficients: vec![0.0; self.num_coefficients],
145 }]);
146 }
147
148 let fft_size = self.window_size;
150 let filterbank = build_mel_filterbank(self.num_mel_bands, fft_size, self.sample_rate);
151
152 let mut frames = Vec::with_capacity(stft_frames.len());
153
154 for stft_frame in &stft_frames {
155 let magnitudes = stft_frame.magnitude();
156 let mel_energies = apply_mel_filterbank(&magnitudes, &filterbank);
157 let log_mel = log_mel_energy(&mel_energies);
158
159 let mut dct_out = vec![0.0_f32; self.num_mel_bands];
161 dct::dct_ii(&log_mel, &mut dct_out)?;
162
163 let coefficients = dct_out[..self.num_coefficients].to_vec();
164 frames.push(MfccFrame { coefficients });
165 }
166
167 Ok(frames)
168 }
169
170 pub fn deltas(frames: &[MfccFrame], width: usize) -> Result<Vec<MfccFrame>, AnalysisError> {
179 if frames.is_empty() {
180 return Err(AnalysisError::EmptyInput);
181 }
182 if width == 0 {
183 return Err(AnalysisError::InvalidParameter {
184 name: "width",
185 reason: "must be positive",
186 });
187 }
188
189 let n = frames.len();
190 let num_coeffs = frames[0].coefficients.len();
191 let mut result = Vec::with_capacity(n);
192
193 let denom: f32 = 2.0 * (1..=width).map(|w| (w * w) as f32).sum::<f32>();
195
196 for t in 0..n {
197 let mut coefficients = vec![0.0_f32; num_coeffs];
198 if denom > f32::EPSILON {
199 for w in 1..=width {
200 let prev = t.saturating_sub(w);
202 let next = (t + w).min(n - 1);
203 for (c, coeff) in coefficients.iter_mut().enumerate() {
204 *coeff += w as f32
205 * (frames[next].coefficients[c] - frames[prev].coefficients[c]);
206 }
207 }
208 for c in &mut coefficients {
209 *c /= denom;
210 }
211 }
212 result.push(MfccFrame { coefficients });
213 }
214
215 Ok(result)
216 }
217
218 pub fn delta_deltas(
226 frames: &[MfccFrame],
227 width: usize,
228 ) -> Result<Vec<MfccFrame>, AnalysisError> {
229 let d = Self::deltas(frames, width)?;
230 Self::deltas(&d, width)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use core::f32::consts::PI;
238
239 const SR: f32 = 44100.0;
240
241 #[test]
242 fn silence_near_zero_mfccs() {
243 let samples = vec![0.0_f32; 4096];
244 let extractor = MfccExtractor::new(SR);
245 let frames = extractor.extract(&samples).unwrap();
246 for frame in &frames {
247 assert_eq!(frame.coefficients.len(), 13);
248 for &c in &frame.coefficients {
251 assert!(c.is_finite(), "non-finite MFCC: {c}");
252 }
253 }
254 }
255
256 #[test]
257 fn extract_basic_sine() {
258 let samples: Vec<f32> = (0..8192)
259 .map(|i| (2.0 * PI * 440.0 * i as f32 / SR).sin())
260 .collect();
261 let extractor = MfccExtractor::new(SR);
262 let frames = extractor.extract(&samples).unwrap();
263 assert!(!frames.is_empty());
264 for frame in &frames {
265 assert_eq!(frame.coefficients.len(), 13);
266 for &c in &frame.coefficients {
267 assert!(c.is_finite(), "non-finite MFCC: {c}");
268 }
269 }
270 }
271
272 #[test]
273 fn empty_input_error() {
274 let extractor = MfccExtractor::new(SR);
275 assert_eq!(extractor.extract(&[]), Err(AnalysisError::EmptyInput));
276 }
277
278 #[test]
279 fn zero_coefficients_error() {
280 let extractor = MfccExtractor::new(SR).with_num_coefficients(0);
281 let result = extractor.extract(&[1.0; 2048]);
282 assert!(matches!(
283 result,
284 Err(AnalysisError::InvalidParameter { .. })
285 ));
286 }
287
288 #[test]
289 fn coefficients_exceed_bands_error() {
290 let extractor = MfccExtractor::new(SR)
291 .with_num_coefficients(30)
292 .with_num_mel_bands(13);
293 let result = extractor.extract(&[1.0; 2048]);
294 assert!(matches!(
295 result,
296 Err(AnalysisError::InvalidParameter { .. })
297 ));
298 }
299
300 #[test]
301 fn short_signal_returns_zero_frame() {
302 let extractor = MfccExtractor::new(SR).with_window_size(1024);
303 let frames = extractor.extract(&[1.0; 512]).unwrap();
304 assert_eq!(frames.len(), 1);
305 assert!(frames[0].coefficients.iter().all(|&c| c == 0.0));
306 }
307
308 #[test]
309 fn delta_of_constant_is_zero() {
310 let frames: Vec<MfccFrame> = (0..10)
311 .map(|_| MfccFrame {
312 coefficients: vec![1.0, 2.0, 3.0],
313 })
314 .collect();
315 let deltas = MfccExtractor::deltas(&frames, 2).unwrap();
316 assert_eq!(deltas.len(), 10);
317 for d in &deltas {
318 for &c in &d.coefficients {
319 assert!(c.abs() < 1e-6, "delta of constant should be zero, got {c}");
320 }
321 }
322 }
323
324 #[test]
325 fn delta_of_linear_ramp() {
326 let frames: Vec<MfccFrame> = (0..10)
328 .map(|i| MfccFrame {
329 coefficients: vec![i as f32],
330 })
331 .collect();
332 let deltas = MfccExtractor::deltas(&frames, 1).unwrap();
333 for d in &deltas[1..9] {
335 assert!(
336 (d.coefficients[0] - 1.0).abs() < 1e-4,
337 "expected delta ~1.0, got {}",
338 d.coefficients[0]
339 );
340 }
341 }
342
343 #[test]
344 fn delta_empty_error() {
345 let result = MfccExtractor::deltas(&[], 2);
346 assert_eq!(result, Err(AnalysisError::EmptyInput));
347 }
348
349 #[test]
350 fn delta_zero_width_error() {
351 let frames = vec![MfccFrame {
352 coefficients: vec![1.0],
353 }];
354 let result = MfccExtractor::deltas(&frames, 0);
355 assert!(matches!(
356 result,
357 Err(AnalysisError::InvalidParameter { .. })
358 ));
359 }
360
361 #[test]
362 fn delta_delta_of_constant_is_zero() {
363 let frames: Vec<MfccFrame> = (0..10)
364 .map(|_| MfccFrame {
365 coefficients: vec![5.0, 3.0],
366 })
367 .collect();
368 let dd = MfccExtractor::delta_deltas(&frames, 2).unwrap();
369 for d in &dd {
370 for &c in &d.coefficients {
371 assert!(c.abs() < 1e-6, "delta-delta of constant should be zero");
372 }
373 }
374 }
375
376 #[test]
377 fn no_nan_or_inf_in_output() {
378 let samples: Vec<f32> = (0..8192)
380 .map(|i| (i as f32 * 7.3).sin() * (i as f32 * 13.7).cos())
381 .collect();
382 let extractor = MfccExtractor::new(SR);
383 let frames = extractor.extract(&samples).unwrap();
384 for frame in &frames {
385 for &c in &frame.coefficients {
386 assert!(c.is_finite(), "non-finite MFCC in noisy signal: {c}");
387 }
388 }
389 }
390
391 #[test]
392 fn builder_methods() {
393 let e = MfccExtractor::new(SR)
394 .with_num_coefficients(20)
395 .with_num_mel_bands(40)
396 .with_window_size(2048)
397 .with_hop_size(1024);
398 assert_eq!(e.num_coefficients, 20);
399 assert_eq!(e.num_mel_bands, 40);
400 assert_eq!(e.window_size, 2048);
401 assert_eq!(e.hop_size, 1024);
402 }
403
404 #[test]
405 fn custom_num_coefficients() {
406 let samples: Vec<f32> = (0..4096)
407 .map(|i| (2.0 * PI * 440.0 * i as f32 / SR).sin())
408 .collect();
409 let extractor = MfccExtractor::new(SR).with_num_coefficients(20);
410 let frames = extractor.extract(&samples).unwrap();
411 for frame in &frames {
412 assert_eq!(frame.coefficients.len(), 20);
413 }
414 }
415
416 #[cfg(feature = "serde")]
417 #[test]
418 fn mfcc_frame_serde_roundtrip() {
419 let frame = MfccFrame {
420 coefficients: vec![1.0, -0.5, 0.3, 0.0],
421 };
422 let json =
423 serde_json::to_string(&frame).unwrap_or_else(|e| panic!("serialize MfccFrame: {e}"));
424 let back: MfccFrame =
425 serde_json::from_str(&json).unwrap_or_else(|e| panic!("deserialize MfccFrame: {e}"));
426 assert_eq!(frame, back);
427 }
428}