1use crate::signal::{Spectrogram, TimeSignal};
2use ndarray::prelude::*;
3use num::complex::Complex64;
4use realfft::RealFftPlanner;
5
6pub use crate::dsp::window::WindowFn;
7use crate::dsp::window::generate_periodic_window;
8
9#[derive(Debug, thiserror::Error)]
10pub enum StftError {
11 #[error("window_size must be > 0")]
12 WindowSizeZero,
13 #[error("hop_size must be > 0")]
14 HopSizeZero,
15 #[error("window_size ({window_size}) must be <= signal length ({signal_length})")]
16 WindowLargerThanSignal {
17 window_size: usize,
18 signal_length: usize,
19 },
20}
21
22#[derive(Debug, Clone)]
23pub struct StftConfig {
24 pub window_size: usize,
26 pub hop_size: usize,
28 pub window_fn: WindowFn,
30}
31
32impl StftConfig {
33 pub fn new(window_size: usize) -> Self {
34 Self {
35 window_size,
36 hop_size: window_size / 4,
37 window_fn: WindowFn::Hann,
38 }
39 }
40}
41
42impl Default for StftConfig {
43 fn default() -> Self {
44 Self::new(1024)
45 }
46}
47
48pub fn stft(signal: &TimeSignal, config: &StftConfig) -> Result<Spectrogram, StftError> {
55 if config.window_size == 0 {
56 return Err(StftError::WindowSizeZero);
57 }
58 if config.hop_size == 0 {
59 return Err(StftError::HopSizeZero);
60 }
61 let num_samples = signal.num_time_steps();
62 if config.window_size > num_samples {
63 return Err(StftError::WindowLargerThanSignal {
64 window_size: config.window_size,
65 signal_length: num_samples,
66 });
67 }
68
69 let num_channels = signal.num_channels();
70 let window_size = config.window_size;
71 let hop_size = config.hop_size;
72 let num_frames = (num_samples - window_size) / hop_size + 1;
73 let num_freq_bins = window_size / 2 + 1;
74 let sample_rate = signal.sample_rate();
75
76 let window = generate_periodic_window(config.window_fn, window_size);
77
78 let fft = RealFftPlanner::<f64>::new().plan_fft_forward(window_size);
79 let mut data: Array3<Complex64> = Array3::zeros((num_channels, num_frames, num_freq_bins));
80
81 let mut windowed_frame = fft.make_input_vec();
83 let mut scratch = fft.make_scratch_vec();
84
85 for (ch_data, mut ch_out) in signal.channel_iter().zip(data.outer_iter_mut()) {
86 for (frame_idx, mut frame_out) in ch_out.outer_iter_mut().enumerate() {
87 let start = frame_idx * hop_size;
88 azip!((wf in &mut windowed_frame, &s in ch_data.slice(s![start..start + window_size]), &w in &window) {
89 *wf = s * w;
90 });
91
92 fft.process_with_scratch(
93 &mut windowed_frame,
94 frame_out
95 .as_slice_mut()
96 .expect("spectrogram rows are contiguous"),
97 &mut scratch,
98 )
99 .expect("buffer lengths come from the FFT plan");
100 }
101 }
102
103 let mut spectrogram =
104 Spectrogram::new(data, sample_rate, window_size, hop_size, config.window_fn)
105 .expect("inputs were validated above and the data has window_size / 2 + 1 bins");
106 for ch in 0..signal.num_channels() {
107 spectrogram.set_channel_label(ch, signal.channel_label(ch));
108 }
109 Ok(spectrogram)
110}
111
112#[cfg(test)]
113mod tests {
114 use crate::signal::{
115 NoiseConfig, SineConfig, Spectrum, SweepConfig, SweepType, TimeSignal, generate_noise,
116 generate_sine, generate_sweep,
117 };
118 use approx::assert_abs_diff_eq;
119 use ndarray::arr2;
120
121 use super::*;
122
123 #[test]
124 fn stft_errors() {
125 let signal = TimeSignal::new(arr2(&[[0.0; 16]]), 16.0).unwrap();
126
127 assert!(matches!(
128 stft(
129 &signal,
130 &StftConfig {
131 window_size: 0,
132 hop_size: 4,
133 window_fn: WindowFn::Hann
134 }
135 ),
136 Err(StftError::WindowSizeZero)
137 ));
138 assert!(matches!(
139 stft(
140 &signal,
141 &StftConfig {
142 window_size: 8,
143 hop_size: 0,
144 window_fn: WindowFn::Hann
145 }
146 ),
147 Err(StftError::HopSizeZero)
148 ));
149 assert!(matches!(
150 stft(
151 &signal,
152 &StftConfig {
153 window_size: 32,
154 hop_size: 8,
155 window_fn: WindowFn::Hann
156 }
157 ),
158 Err(StftError::WindowLargerThanSignal { .. })
159 ));
160 let spec = stft(
162 &signal,
163 &StftConfig {
164 window_size: 2,
165 hop_size: 1,
166 window_fn: WindowFn::Hann,
167 },
168 )
169 .unwrap();
170 assert!(spec.normalization().coherent_gain() > 0.0);
171 }
172
173 #[test]
174 fn stft_dimensions() {
175 let signal = TimeSignal::new(arr2(&[[1.0; 32]]), 16.0).unwrap();
177 let config = StftConfig {
178 window_size: 8,
179 hop_size: 4,
180 window_fn: WindowFn::Rectangular,
181 };
182 let spec = stft(&signal, &config).unwrap();
183
184 assert_eq!(spec.num_channels(), 1);
185 assert_eq!(spec.num_frames(), 7);
186 assert_eq!(spec.num_freq_bins(), 5);
187 assert_eq!(spec.sample_rate(), 16.0);
188 assert_eq!(spec.window_size(), 8);
189 assert_eq!(spec.hop_size(), 4);
190 }
191
192 #[test]
193 fn stft_frame_times_and_freq_bins() {
194 let signal = TimeSignal::new(arr2(&[[0.0; 16]]), 8.0).unwrap();
195 let config = StftConfig {
197 window_size: 8,
198 hop_size: 4,
199 window_fn: WindowFn::Rectangular,
200 };
201 let spec = stft(&signal, &config).unwrap();
202
203 assert_abs_diff_eq!(spec.frame_times()[0], 0.5, epsilon = 1e-10);
205 assert_abs_diff_eq!(spec.frame_times()[1], 1.0, epsilon = 1e-10);
206 assert_abs_diff_eq!(spec.frame_times()[2], 1.5, epsilon = 1e-10);
207
208 assert_abs_diff_eq!(spec.freq_bins()[0], 0.0, epsilon = 1e-10);
210 assert_abs_diff_eq!(spec.freq_bins()[4], 4.0, epsilon = 1e-10);
211 }
212
213 #[test]
214 fn stft_dc_signal() {
215 let window_size = 8;
218 let signal = TimeSignal::new(arr2(&[[1.0; 32]]), 16.0).unwrap();
219 let config = StftConfig {
220 window_size,
221 hop_size: 4,
222 window_fn: WindowFn::Rectangular,
223 };
224 let spec = stft(&signal, &config).unwrap();
225
226 for frame in 0..spec.num_frames() {
227 let ch_data = spec.channel(0);
228 let dc = ch_data[[frame, 0]];
229 assert_abs_diff_eq!(dc.re, window_size as f64, epsilon = 1e-10);
230 assert_abs_diff_eq!(dc.im, 0.0, epsilon = 1e-10);
231 for bin in 1..spec.num_freq_bins() {
232 assert_abs_diff_eq!(ch_data[[frame, bin]].norm(), 0.0, epsilon = 1e-8);
233 }
234 }
235 }
236
237 #[test]
238 fn stft_multi_channel() {
239 let data = arr2(&[[1.0; 32], [2.0; 32]]);
240 let signal = TimeSignal::new(data, 16.0).unwrap();
241 let config = StftConfig {
242 window_size: 8,
243 hop_size: 4,
244 window_fn: WindowFn::Rectangular,
245 };
246 let spec = stft(&signal, &config).unwrap();
247
248 assert_eq!(spec.num_channels(), 2);
249 for frame in 0..spec.num_frames() {
251 let dc_ch0 = spec.channel(0)[[frame, 0]].re;
252 let dc_ch1 = spec.channel(1)[[frame, 0]].re;
253 assert_abs_diff_eq!(dc_ch1, 2.0 * dc_ch0, epsilon = 1e-10);
254 }
255 }
256
257 #[test]
258 fn calibrated_spectra_for_bin_centered_sine() {
259 let sample_rate = 8.0;
260 let window_size = 8;
261 let signal = TimeSignal::new(
262 arr2(&[std::array::from_fn::<f64, 8, _>(|n| {
263 (std::f64::consts::TAU * 2.0 * n as f64 / window_size as f64).sin()
264 })]),
265 sample_rate,
266 )
267 .unwrap();
268 let spec = stft(
269 &signal,
270 &StftConfig {
271 window_size,
272 hop_size: window_size,
273 window_fn: WindowFn::Rectangular,
274 },
275 )
276 .unwrap();
277
278 assert_abs_diff_eq!(spec.raw_magnitude()[[0, 0, 2]], 4.0, epsilon = 1e-10);
279 assert_abs_diff_eq!(spec.amplitude_spectrum()[[0, 0, 2]], 1.0, epsilon = 1e-10);
280 assert_abs_diff_eq!(spec.power_spectrum()[[0, 0, 2]], 0.5, epsilon = 1e-10);
281 assert_abs_diff_eq!(
282 spec.power_spectral_density()[[0, 0, 2]],
283 0.5,
284 epsilon = 1e-10
285 );
286 }
287
288 #[test]
289 fn calibrated_dc_amplitude_is_window_corrected() {
290 let signal = TimeSignal::new(arr2(&[[1.0; 8]]), 8.0).unwrap();
291 let spec = stft(
292 &signal,
293 &StftConfig {
294 window_size: 8,
295 hop_size: 8,
296 window_fn: WindowFn::Hann,
297 },
298 )
299 .unwrap();
300
301 assert_abs_diff_eq!(spec.amplitude_spectrum()[[0, 0, 0]], 1.0, epsilon = 1e-10);
302 }
303
304 #[test]
305 fn stft_generated_sine_has_peak_at_expected_frequency() {
306 let signal = generate_sine(
307 800,
308 440.0,
309 SineConfig {
310 amplitude: 0.75,
311 sample_rate: 8_000.0,
312 num_channels: 1,
313 },
314 )
315 .unwrap();
316 let spec = stft(
317 &signal,
318 &StftConfig {
319 window_size: 800,
320 hop_size: 800,
321 window_fn: WindowFn::Rectangular,
322 },
323 )
324 .unwrap();
325
326 let magnitudes = spec.amplitude_spectrum();
327 let peak_bin = (0..spec.num_freq_bins())
328 .max_by(|&left, &right| {
329 magnitudes[[0, 0, left]]
330 .partial_cmp(&magnitudes[[0, 0, right]])
331 .unwrap()
332 })
333 .unwrap();
334
335 assert_abs_diff_eq!(spec.freq_bins()[peak_bin], 440.0, epsilon = 1e-12);
336 assert_abs_diff_eq!(magnitudes[[0, 0, peak_bin]], 0.75, epsilon = 1e-10);
337 }
338
339 #[test]
340 fn stft_generated_sweep_shows_increasing_dominant_frequency() {
341 let signal = generate_sweep(
342 4_096,
343 200.0..3_200.0,
344 SweepConfig {
345 sample_rate: 16_000.0,
346 fade_out: 0,
347 sweep_type: SweepType::Linear,
348 ..Default::default()
349 },
350 )
351 .unwrap();
352 let spec = stft(
353 &signal,
354 &StftConfig {
355 window_size: 512,
356 hop_size: 256,
357 window_fn: WindowFn::Hann,
358 },
359 )
360 .unwrap();
361 let magnitudes = spec.amplitude_spectrum();
362
363 let dominant_frequency = |frame: usize| {
364 let peak_bin = (0..spec.num_freq_bins())
365 .max_by(|&left, &right| {
366 magnitudes[[0, frame, left]]
367 .partial_cmp(&magnitudes[[0, frame, right]])
368 .unwrap()
369 })
370 .unwrap();
371 spec.freq_bins()[peak_bin]
372 };
373
374 let first_peak = dominant_frequency(0);
375 let last_peak = dominant_frequency(spec.num_frames() - 1);
376
377 assert!(first_peak < last_peak);
378 assert!(first_peak >= 0.0);
379 assert!(last_peak <= signal.sample_rate() / 2.0);
380 }
381
382 #[test]
383 fn stft_generated_noise_has_broadband_energy() {
384 let signal = generate_noise(
385 2_048,
386 NoiseConfig {
387 sample_rate: 16_000.0,
388 spectrum: Spectrum::White,
389 seed: Some(123),
390 ..Default::default()
391 },
392 )
393 .unwrap();
394 let spec = stft(
395 &signal,
396 &StftConfig {
397 window_size: 256,
398 hop_size: 128,
399 window_fn: WindowFn::Hann,
400 },
401 )
402 .unwrap();
403 let magnitudes = spec.amplitude_spectrum();
404
405 for frame in 0..spec.num_frames() {
406 let frame_energy: f64 = (0..spec.num_freq_bins())
407 .map(|bin| magnitudes[[0, frame, bin]])
408 .sum();
409 assert!(frame_energy > 0.0);
410 }
411
412 let active_bins = (0..spec.num_freq_bins())
413 .filter(|&bin| magnitudes[[0, 0, bin]] > 1e-3)
414 .count();
415 assert!(active_bins > spec.num_freq_bins() / 2);
416 }
417}