1use crate::config::PreprocessorConfig;
2use crate::error::{Error, Result};
3use hound::{WavReader, WavSpec};
4use ndarray::Array2;
5use realfft::RealToComplex;
6use std::f32::consts::PI;
7use std::path::Path;
8use std::sync::Arc;
9
10pub struct FeatureCache {
13 pub mel_basis: Array2<f32>,
14 pub fft_plan: Arc<dyn RealToComplex<f32>>,
15}
16
17impl FeatureCache {
18 pub fn from_config(config: &PreprocessorConfig) -> Self {
19 let mel_basis =
20 create_mel_filterbank(config.n_fft, config.feature_size, config.sampling_rate);
21 let mut planner = realfft::RealFftPlanner::<f32>::new();
22 let fft_plan = planner.plan_fft_forward(config.n_fft);
23 Self {
24 mel_basis,
25 fft_plan,
26 }
27 }
28}
29
30pub fn load_audio<P: AsRef<Path>>(path: P) -> Result<(Vec<f32>, WavSpec)> {
31 let mut reader = WavReader::open(path)?;
32 let spec = reader.spec();
33
34 let samples: Vec<f32> = match spec.sample_format {
35 hound::SampleFormat::Float => reader
36 .samples::<f32>()
37 .collect::<std::result::Result<Vec<_>, _>>()
38 .map_err(|e| Error::Audio(format!("Failed to read float samples: {e}")))?,
39 hound::SampleFormat::Int => reader
40 .samples::<i16>()
41 .map(|s| s.map(|s| s as f32 / 32768.0))
42 .collect::<std::result::Result<Vec<_>, _>>()
43 .map_err(|e| Error::Audio(format!("Failed to read int samples: {e}")))?,
44 };
45
46 Ok((samples, spec))
47}
48
49pub fn apply_preemphasis(audio: &[f32], coef: f32) -> Vec<f32> {
50 if audio.is_empty() {
51 return Vec::new();
52 }
53
54 let mut result = Vec::with_capacity(audio.len());
55 result.push(audio[0]);
56
57 for i in 1..audio.len() {
58 result.push(audio[i] - coef * audio[i - 1]);
59 }
60
61 result
62}
63
64fn hann_window(window_length: usize) -> Vec<f32> {
65 (0..window_length)
66 .map(|i| 0.5 - 0.5 * ((2.0 * PI * i as f32) / (window_length as f32 - 1.0)).cos())
67 .collect()
68}
69
70pub fn stft(
75 audio: &[f32],
76 n_fft: usize,
77 hop_length: usize,
78 win_length: usize,
79) -> Result<Array2<f32>> {
80 let mut planner = realfft::RealFftPlanner::<f32>::new();
81 let plan = planner.plan_fft_forward(n_fft);
82 stft_with_plan(audio, &plan, n_fft, hop_length, win_length)
83}
84
85pub fn stft_with_plan(
88 audio: &[f32],
89 plan: &Arc<dyn RealToComplex<f32>>,
90 n_fft: usize,
91 hop_length: usize,
92 win_length: usize,
93) -> Result<Array2<f32>> {
94 let pad_amount = n_fft / 2;
95 let mut padded = vec![0.0f32; pad_amount];
96 padded.extend_from_slice(audio);
97 padded.resize(padded.len() + pad_amount, 0.0);
98
99 let window = hann_window(win_length);
100 let num_frames = audio.len() / hop_length;
104 let freq_bins = n_fft / 2 + 1;
105 let mut spectrogram = Array2::<f32>::zeros((freq_bins, num_frames));
106 let window_offset = (n_fft - win_length) / 2;
108
109 let mut input = vec![0.0f32; n_fft];
110 let mut output = plan.make_output_vec();
111 let mut scratch = plan.make_scratch_vec();
112
113 for frame_idx in 0..num_frames {
114 let start = frame_idx * hop_length;
115
116 input.fill(0.0);
117 for i in 0..win_length {
118 input[window_offset + i] = padded[start + window_offset + i] * window[i];
119 }
120
121 plan.process_with_scratch(&mut input, &mut output, &mut scratch)
122 .map_err(|e| Error::Audio(format!("FFT failed: {e}")))?;
123
124 for k in 0..freq_bins {
125 spectrogram[[k, frame_idx]] = output[k].norm_sqr();
126 }
127 }
128
129 Ok(spectrogram)
130}
131
132const F_SP: f64 = 200.0 / 3.0;
134const MIN_LOG_HZ: f64 = 1000.0;
135const MIN_LOG_MEL: f64 = MIN_LOG_HZ / F_SP;
136const LOG_STEP: f64 = 0.06875177742094912;
137
138fn hz_to_mel_slaney(hz: f64) -> f64 {
139 if hz < MIN_LOG_HZ {
140 hz / F_SP
141 } else {
142 MIN_LOG_MEL + (hz / MIN_LOG_HZ).ln() / LOG_STEP
143 }
144}
145
146fn mel_to_hz_slaney(mel: f64) -> f64 {
147 if mel < MIN_LOG_MEL {
148 mel * F_SP
149 } else {
150 MIN_LOG_HZ * ((mel - MIN_LOG_MEL) * LOG_STEP).exp()
151 }
152}
153
154pub fn create_mel_filterbank(n_fft: usize, n_mels: usize, sample_rate: usize) -> Array2<f32> {
155 let freq_bins = n_fft / 2 + 1;
156 let mut filterbank = Array2::<f32>::zeros((n_mels, freq_bins));
157
158 let fmax = sample_rate as f64 / 2.0;
159 let mel_min = hz_to_mel_slaney(0.0);
160 let mel_max = hz_to_mel_slaney(fmax);
161
162 let mel_points: Vec<f64> = (0..=n_mels + 1)
164 .map(|i| mel_to_hz_slaney(mel_min + (mel_max - mel_min) * i as f64 / (n_mels + 1) as f64))
165 .collect();
166
167 let fft_freqs: Vec<f64> = (0..freq_bins)
169 .map(|i| i as f64 * sample_rate as f64 / n_fft as f64)
170 .collect();
171
172 let fdiff: Vec<f64> = mel_points.windows(2).map(|w| w[1] - w[0]).collect();
174
175 for i in 0..n_mels {
176 for (k, &freq) in fft_freqs.iter().enumerate() {
177 let lower = (freq - mel_points[i]) / fdiff[i];
178 let upper = (mel_points[i + 2] - freq) / fdiff[i + 1];
179 filterbank[[i, k]] = 0.0f64.max(lower.min(upper)) as f32;
180 }
181 }
182
183 for i in 0..n_mels {
185 let enorm = 2.0 / (mel_points[i + 2] - mel_points[i]);
186 for k in 0..freq_bins {
187 filterbank[[i, k]] *= enorm as f32;
188 }
189 }
190
191 filterbank
192}
193
194pub fn extract_features_with_cache(
212 mut audio: Vec<f32>,
213 sample_rate: u32,
214 channels: u16,
215 config: &PreprocessorConfig,
216 cache: &FeatureCache,
217) -> Result<Array2<f32>> {
218 if sample_rate != config.sampling_rate as u32 {
219 return Err(Error::Audio(format!(
220 "Audio sample rate {} doesn't match expected {}. Please resample your audio first.",
221 sample_rate, config.sampling_rate
222 )));
223 }
224
225 if channels > 1 {
226 let mono: Vec<f32> = audio
227 .chunks(channels as usize)
228 .map(|chunk| chunk.iter().sum::<f32>() / channels as f32)
229 .collect();
230 audio = mono;
231 }
232
233 audio = apply_preemphasis(&audio, config.preemphasis);
234
235 let spectrogram = stft_with_plan(
236 &audio,
237 &cache.fft_plan,
238 config.n_fft,
239 config.hop_length,
240 config.win_length,
241 )?;
242
243 let mel_spectrogram = cache.mel_basis.dot(&spectrogram);
244 let log_zero_guard: f32 = 2.0f32.powi(-24);
246 let mel_spectrogram = mel_spectrogram.mapv(|x| (x + log_zero_guard).ln());
247
248 let mut mel_spectrogram = mel_spectrogram.t().to_owned();
249
250 let num_frames = mel_spectrogram.shape()[0];
252 let num_features = mel_spectrogram.shape()[1];
253
254 if num_frames <= 1 {
255 return Ok(mel_spectrogram);
256 }
257
258 for feat_idx in 0..num_features {
259 let mut column = mel_spectrogram.column_mut(feat_idx);
260 let mean: f32 = column.iter().sum::<f32>() / num_frames as f32;
261 let variance: f32 =
262 column.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / (num_frames as f32 - 1.0);
263 let std = variance.sqrt() + 1e-5;
264
265 for val in column.iter_mut() {
266 *val = (*val - mean) / std;
267 }
268 }
269
270 Ok(mel_spectrogram)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 fn sine_wave(freq_hz: f32, sample_rate: usize, num_samples: usize) -> Vec<f32> {
279 (0..num_samples)
280 .map(|i| (2.0 * PI * freq_hz * i as f32 / sample_rate as f32).sin())
281 .collect()
282 }
283
284 #[test]
285 fn stft_concentrates_power_at_expected_bin() {
286 let n_fft = 512;
288 let hop_length = 160;
289 let win_length = 400;
290 let sample_rate = 16000;
291 let audio = sine_wave(1000.0, sample_rate, sample_rate);
292
293 let spec = stft(&audio, n_fft, hop_length, win_length).unwrap();
294
295 let expected_bin = 32;
297 let freq_bins = n_fft / 2 + 1;
298 let num_frames = spec.shape()[1];
299
300 let mut correct_frames = 0;
302 for frame in 2..num_frames.saturating_sub(2) {
303 let mut max_bin = 0;
304 let mut max_power = 0.0f32;
305 for bin in 0..freq_bins {
306 if spec[[bin, frame]] > max_power {
307 max_power = spec[[bin, frame]];
308 max_bin = bin;
309 }
310 }
311 if max_bin == expected_bin {
312 correct_frames += 1;
313 }
314 }
315
316 let interior_frames = num_frames.saturating_sub(4);
317 assert!(
318 correct_frames > interior_frames / 2,
319 "Expected bin {expected_bin} to dominate in most frames, but only {correct_frames}/{interior_frames}"
320 );
321 }
322
323 #[test]
324 fn stft_output_shape_is_correct() {
325 let n_fft = 512;
326 let hop_length = 160;
327 let win_length = 400;
328 let audio = vec![0.0f32; 16000]; let spec = stft(&audio, n_fft, hop_length, win_length).unwrap();
331
332 let freq_bins = n_fft / 2 + 1;
333 assert_eq!(spec.shape()[0], freq_bins);
334 assert_eq!(spec.shape()[1], audio.len() / hop_length);
335 }
336}