gemini_adk_fluent_rs/voice/dsp/
stft.rs1use std::collections::VecDeque;
33
34use realfft::RealFftPlanner;
35use realfft::num_complex::Complex;
36
37use super::{AudioBus, DspStage};
38
39pub trait SpectralStage: Send {
42 fn name(&self) -> &'static str;
44
45 fn process_spectrum(&mut self, bins: &mut [Complex<f32>], bin_hz: f32);
51}
52
53pub struct Identity;
56
57impl SpectralStage for Identity {
58 fn name(&self) -> &'static str {
59 "stft_identity"
60 }
61
62 fn process_spectrum(&mut self, _bins: &mut [Complex<f32>], _bin_hz: f32) {
63 }
65}
66
67pub struct SpectralFloor {
71 floors: Vec<f32>,
73 alpha: f32,
75 beta: f32,
77 ewma_up: f32,
79 ewma_down: f32,
81}
82
83impl SpectralFloor {
84 pub fn default_speech() -> Self {
86 Self {
87 floors: Vec::new(),
88 alpha: 1.5,
89 beta: 0.15,
90 ewma_up: 0.02, ewma_down: 0.2, }
93 }
94}
95
96impl SpectralStage for SpectralFloor {
97 fn name(&self) -> &'static str {
98 "stft_spectral_floor"
99 }
100
101 fn process_spectrum(&mut self, bins: &mut [Complex<f32>], _bin_hz: f32) {
102 if self.floors.is_empty() {
106 self.floors = bins.iter().map(|b| b.norm()).collect();
107 }
108
109 for (bin, floor) in bins.iter_mut().zip(self.floors.iter_mut()) {
110 let mag = bin.norm();
111
112 if mag < *floor {
118 *floor = self.ewma_down * mag + (1.0 - self.ewma_down) * *floor;
119 } else if mag < 4.0 * *floor {
120 *floor = self.ewma_up * mag + (1.0 - self.ewma_up) * *floor;
121 }
122
123 let subtracted = (mag - self.alpha * *floor).max(self.beta * mag);
126
127 if mag > 1e-8 {
129 let scale = subtracted / mag;
130 bin.re *= scale;
131 bin.im *= scale;
132 } else {
133 bin.re = 0.0;
134 bin.im = 0.0;
135 }
136 }
137 }
138}
139
140pub struct Stft<S: SpectralStage> {
151 stage: S,
152 sample_rate: u32,
153 window_len: usize,
154 hop_len: usize,
155
156 fft: std::sync::Arc<dyn realfft::RealToComplex<f32>>,
157 ifft: std::sync::Arc<dyn realfft::ComplexToReal<f32>>,
158 spectrum: Vec<Complex<f32>>,
159 window_sqrt_hann: Vec<f32>,
160
161 input_fifo: VecDeque<f32>,
162 output_fifo: VecDeque<f32>,
163
164 frame: Vec<f32>,
166 windowed: Vec<f32>,
168 synth: Vec<f32>,
170 acc: Vec<f32>,
172 frame_filled: bool,
174}
175
176impl<S: SpectralStage> Stft<S> {
177 pub fn new(stage: S, sample_rate: u32) -> Self {
180 let window_len = (sample_rate as usize * 20) / 1000;
181 let hop_len = window_len / 2;
182 let mut planner = RealFftPlanner::new();
183 let fft = planner.plan_fft_forward(window_len);
184 let ifft = planner.plan_fft_inverse(window_len);
185 let mut output_fifo = VecDeque::with_capacity(window_len * 4);
186 output_fifo.extend(std::iter::repeat_n(0.0f32, window_len));
188 Self {
189 stage,
190 sample_rate,
191 window_len,
192 hop_len,
193 fft,
194 ifft,
195 spectrum: vec![Complex::new(0.0, 0.0); window_len / 2 + 1],
196 window_sqrt_hann: Self::make_sqrt_hann_window(window_len),
197 input_fifo: VecDeque::with_capacity(window_len * 4),
198 output_fifo,
199 frame: vec![0.0; window_len],
200 windowed: vec![0.0; window_len],
201 synth: vec![0.0; window_len],
202 acc: vec![0.0; window_len],
203 frame_filled: false,
204 }
205 }
206
207 fn make_sqrt_hann_window(len: usize) -> Vec<f32> {
210 (0..len)
211 .map(|i| {
212 let hann = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / len as f32).cos());
213 hann.sqrt()
214 })
215 .collect()
216 }
217
218 fn need(&self) -> usize {
220 if self.frame_filled {
221 self.hop_len
222 } else {
223 self.window_len
224 }
225 }
226
227 fn process_one_hop(&mut self) {
231 let (w, h) = (self.window_len, self.hop_len);
232 if self.frame_filled {
233 self.frame.copy_within(h.., 0);
234 for slot in self.frame[w - h..].iter_mut() {
235 *slot = self.input_fifo.pop_front().unwrap_or(0.0);
236 }
237 } else {
238 for slot in self.frame.iter_mut() {
239 *slot = self.input_fifo.pop_front().unwrap_or(0.0);
240 }
241 self.frame_filled = true;
242 }
243
244 for ((dst, &src), &win) in self
245 .windowed
246 .iter_mut()
247 .zip(&self.frame)
248 .zip(&self.window_sqrt_hann)
249 {
250 *dst = src * win;
251 }
252 self.fft
253 .process(&mut self.windowed, &mut self.spectrum)
254 .expect("forward FFT");
255
256 let bin_hz = self.sample_rate as f32 / w as f32;
257 self.stage.process_spectrum(&mut self.spectrum, bin_hz);
258
259 self.ifft
260 .process(&mut self.spectrum, &mut self.synth)
261 .expect("inverse FFT");
262 let norm = 1.0 / w as f32;
263 for ((acc, &syn), &win) in self
264 .acc
265 .iter_mut()
266 .zip(&self.synth)
267 .zip(&self.window_sqrt_hann)
268 {
269 *acc += syn * norm * win;
270 }
271
272 for i in 0..h {
274 self.output_fifo.push_back(self.acc[i]);
275 }
276 self.acc.copy_within(h.., 0);
277 for slot in self.acc[w - h..].iter_mut() {
278 *slot = 0.0;
279 }
280 }
281
282 fn reinit(&mut self, sample_rate: u32) {
284 let window_len = (sample_rate as usize * 20) / 1000;
285 let hop_len = window_len / 2;
286 let mut planner = RealFftPlanner::new();
287 self.fft = planner.plan_fft_forward(window_len);
288 self.ifft = planner.plan_fft_inverse(window_len);
289 self.spectrum = vec![Complex::new(0.0, 0.0); window_len / 2 + 1];
290 self.window_sqrt_hann = Self::make_sqrt_hann_window(window_len);
291 self.input_fifo.clear();
292 self.output_fifo.clear();
293 self.output_fifo
294 .extend(std::iter::repeat_n(0.0f32, window_len));
295 self.frame = vec![0.0; window_len];
296 self.windowed = vec![0.0; window_len];
297 self.synth = vec![0.0; window_len];
298 self.acc = vec![0.0; window_len];
299 self.frame_filled = false;
300 self.sample_rate = sample_rate;
301 self.window_len = window_len;
302 self.hop_len = hop_len;
303 }
304}
305
306impl<S: SpectralStage> DspStage for Stft<S> {
307 fn name(&self) -> &'static str {
308 "stft"
309 }
310
311 fn process(&mut self, bus: &mut AudioBus) {
312 if bus.sample_rate != self.sample_rate {
313 self.reinit(bus.sample_rate);
314 }
315 let input_len = bus.samples.len();
316 self.input_fifo.extend(bus.samples.iter().copied());
317
318 while self.input_fifo.len() >= self.need() {
319 self.process_one_hop();
320 }
321
322 bus.samples.clear();
323 for _ in 0..input_len {
324 bus.samples
325 .push(self.output_fifo.pop_front().unwrap_or(0.0));
326 }
327 }
328
329 fn latency_samples(&self) -> usize {
330 self.window_len
331 }
332}
333
334#[cfg(test)]
335#[allow(
336 clippy::needless_range_loop,
337 reason = "sample-index loops read naturally in DSP tests"
338)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn output_length_tracks_input_length() {
344 let mut stft = Stft::new(Identity, 16_000);
345 let mut samples = vec![0.1; 160];
346 let mut bus = AudioBus {
347 samples: &mut samples,
348 sample_rate: 16_000,
349 };
350 let input_len = bus.samples.len();
351 stft.process(&mut bus);
352 assert_eq!(bus.samples.len(), input_len);
353
354 samples = vec![0.05; 73];
355 let mut bus = AudioBus {
356 samples: &mut samples,
357 sample_rate: 16_000,
358 };
359 let input_len = bus.samples.len();
360 stft.process(&mut bus);
361 assert_eq!(bus.samples.len(), input_len);
362 }
363
364 #[test]
365 fn latency_is_window_length() {
366 let stft = Stft::new(Identity, 16_000);
367 assert_eq!(stft.latency_samples(), 320);
368 }
369
370 #[test]
371 fn dc_and_nyquist_bins_survive_identity() {
372 let mut identity = Identity;
373 let mut bins = vec![
374 Complex::new(1.0, 0.0),
375 Complex::new(0.5, 0.3),
376 Complex::new(0.2, 0.0),
377 ];
378 identity.process_spectrum(&mut bins, 50.0);
379 assert!(bins[0].re > 0.9);
380 assert!(bins[0].im.abs() < 0.01);
381 }
382
383 #[test]
384
385 fn identity_reconstructs_within_60db() {
386 let sample_rate = 16_000u32;
387 let duration = 0.5;
388 let total_samples = (sample_rate as f64 * duration) as usize;
389
390 let mut signal = vec![0.0; total_samples];
391 for i in 0..total_samples {
392 let t = i as f64 / sample_rate as f64;
393 let sine_440 = (2.0 * std::f64::consts::PI * 440.0 * t).sin();
394 let sine_1300 = (2.0 * std::f64::consts::PI * 1300.0 * t).sin();
395 signal[i] = (sine_440 + sine_1300) as f32 * 0.5;
396 }
397
398 let mut stft = Stft::new(Identity, sample_rate);
399
400 let mut output: Vec<f32> = Vec::new();
401 let chunk_sizes = [160, 73, 512, 41];
402 let mut chunk_idx = 0;
403 let mut pos = 0;
404
405 while pos < signal.len() {
406 let size = chunk_sizes[chunk_idx % chunk_sizes.len()].min(signal.len() - pos);
407 chunk_idx += 1;
408
409 let mut chunk = signal[pos..pos + size].to_vec();
410 let mut bus = AudioBus {
411 samples: &mut chunk,
412 sample_rate,
413 };
414 stft.process(&mut bus);
415 output.extend(bus.samples.iter());
416 pos += size;
417 }
418
419 let window_len = 320;
420 let skip = 2 * window_len;
423 if output.len() <= skip {
424 panic!("Output too short");
425 }
426 let output_delayed = &output[skip..];
427 let signal_ref = &signal[skip - window_len..skip - window_len + output_delayed.len()];
428
429 let mut error_sq = 0.0f64;
430 for i in 0..output_delayed.len() {
431 let diff = output_delayed[i] as f64 - signal_ref[i] as f64;
432 error_sq += diff * diff;
433 }
434 let error_rms = (error_sq / output_delayed.len() as f64).sqrt();
435
436 let mut signal_sq = 0.0f64;
437 for &s in signal_ref {
438 signal_sq += s as f64 * s as f64;
439 }
440 let signal_rms = (signal_sq / signal_ref.len() as f64).sqrt();
441
442 let snr_db = 20.0 * (signal_rms / error_rms).log10();
443 eprintln!("SNR: {snr_db:.1} dB");
444 assert!(snr_db > 60.0, "SNR {snr_db:.1} dB is not > 60 dB");
445 }
446
447 #[test]
448 fn spectral_floor_attenuates_stationary_noise() {
449 let sample_rate = 16_000u32;
450 let noise_duration = 1.0;
451 let noise_samples = (sample_rate as f64 * noise_duration) as usize;
452
453 let mut rng = 0u64;
454 let mut noise_signal = vec![0.0; noise_samples];
455 for i in 0..noise_samples {
456 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
457 let u = ((rng >> 16) & 0x7fff) as f32 / 32768.0;
458 noise_signal[i] = (u - 0.5) * 2.0 * 0.05;
459 }
460
461 let noise_segment_start = (noise_samples as f64 * 0.6) as usize;
462 let noise_segment_end = (noise_samples as f64 * 0.9) as usize;
463 let mut noise_input_sq = 0.0f64;
464 for i in noise_segment_start..noise_segment_end {
465 noise_input_sq += noise_signal[i] as f64 * noise_signal[i] as f64;
466 }
467 let noise_input_rms =
468 (noise_input_sq / (noise_segment_end - noise_segment_start) as f64).sqrt();
469
470 let mut stft = Stft::new(SpectralFloor::default_speech(), sample_rate);
471 let mut output: Vec<f32> = Vec::new();
472 let chunk_size = 160;
473 for chunk in noise_signal.chunks(chunk_size) {
474 let mut chunk = chunk.to_vec();
475 let mut bus = AudioBus {
476 samples: &mut chunk,
477 sample_rate,
478 };
479 stft.process(&mut bus);
480 output.extend(bus.samples.iter());
481 }
482
483 let window_len = 320;
484 let output_segment_start = noise_segment_start + window_len;
485 let output_segment_end = noise_segment_end + window_len;
486 let mut noise_output_sq = 0.0f64;
487 for i in output_segment_start..output_segment_end.min(output.len()) {
488 noise_output_sq += output[i] as f64 * output[i] as f64;
489 }
490 let noise_output_rms =
491 (noise_output_sq / (output_segment_end - output_segment_start) as f64).sqrt();
492
493 let attenuation_db = 20.0 * (noise_input_rms / (noise_output_rms + 1e-10)).log10();
494 eprintln!("Noise attenuation: {attenuation_db:.1} dB");
495 assert!(
496 attenuation_db > 6.0,
497 "Attenuation {attenuation_db:.1} dB is not > 6 dB"
498 );
499
500 let mix_samples = (sample_rate as f64 * 1.5) as usize;
506 let lead_in = (sample_rate as f64 * 0.15) as usize;
507 let burst_on = (sample_rate as f64 * 0.2) as usize;
508 let burst_off = (sample_rate as f64 * 0.1) as usize;
509 let mut mix_signal = vec![0.0f32; mix_samples];
510 let mut burst_ranges: Vec<(usize, usize)> = Vec::new();
511 rng = 0;
512 let mut i = 0usize;
513 let mut cursor = lead_in;
514 while cursor < mix_samples {
515 burst_ranges.push((cursor, (cursor + burst_on).min(mix_samples)));
516 cursor += burst_on + burst_off;
517 }
518 while i < mix_samples {
519 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
520 let u = ((rng >> 16) & 0x7fff) as f32 / 32768.0;
521 let noise = (u - 0.5) * 2.0 * 0.05;
522 let in_burst = burst_ranges.iter().any(|&(a, b)| i >= a && i < b);
523 let t = i as f64 / sample_rate as f64;
524 let sine = if in_burst {
525 (2.0 * std::f64::consts::PI * 440.0 * t).sin() as f32 * 0.2
526 } else {
527 0.0
528 };
529 mix_signal[i] = noise + sine;
530 i += 1;
531 }
532
533 let mut stft = Stft::new(SpectralFloor::default_speech(), sample_rate);
534 let mut mix_output: Vec<f32> = Vec::new();
535 for chunk in mix_signal.chunks(chunk_size) {
536 let mut chunk = chunk.to_vec();
537 let mut bus = AudioBus {
538 samples: &mut chunk,
539 sample_rate,
540 };
541 stft.process(&mut bus);
542 mix_output.extend(bus.samples.iter());
543 }
544
545 let settle = (sample_rate as f64 * 0.04) as usize;
548 let mut in_sq = 0.0f64;
549 let mut out_sq = 0.0f64;
550 let mut n = 0usize;
551 for &(a, b) in &burst_ranges {
552 for i in (a + settle)..b {
553 let o = i + window_len;
554 if o < mix_output.len() {
555 in_sq += mix_signal[i] as f64 * mix_signal[i] as f64;
556 out_sq += mix_output[o] as f64 * mix_output[o] as f64;
557 n += 1;
558 }
559 }
560 }
561 let sine_amplitude_retention =
562 (out_sq / n as f64).sqrt() / ((in_sq / n as f64).sqrt() + 1e-10);
563 eprintln!(
564 "Sine amplitude retention: {:.1}%",
565 sine_amplitude_retention * 100.0
566 );
567 assert!(
568 sine_amplitude_retention > 0.8,
569 "Sine retention {:.1}% is not > 80%",
570 sine_amplitude_retention * 100.0
571 );
572 }
573}