gemini_adk_fluent_rs/voice/
mod.rs1#[cfg(feature = "voice-io")]
33mod devices;
34
35#[cfg(feature = "voice-io")]
36pub use devices::{Talk, VoiceIoError};
37pub mod dsp;
38pub use dsp::{AudioBus, ChainMetrics, ChainSnapshot, DspChain, DspStage, IntStage, StageSnapshot};
39
40#[cfg(feature = "denoise")]
41mod denoise;
42
43#[cfg(feature = "denoise")]
44pub use denoise::Denoiser;
45
46pub use gemini_adk_rs::live::InputAudioProcessor;
50use gemini_adk_rs::live::{LiveEvent, LiveHandle};
51use gemini_genai_rs::prelude::{bytes_to_i16, i16_to_bytes};
52use tokio::sync::mpsc;
53use tokio::task::JoinHandle;
54
55pub const SESSION_INPUT_HZ: u32 = 16_000;
57pub const SESSION_OUTPUT_HZ: u32 = 24_000;
59
60#[derive(Debug, Clone, PartialEq)]
62pub enum Playback {
63 Chunk(Vec<i16>),
65 Flush,
68}
69
70pub struct VoicePump {
73 uplink: JoinHandle<()>,
74 downlink: JoinHandle<()>,
75}
76
77impl VoicePump {
78 pub async fn join(self) {
80 let _ = self.uplink.await;
81 let _ = self.downlink.await;
82 }
83
84 pub fn abort(&self) {
86 self.uplink.abort();
87 self.downlink.abort();
88 }
89}
90
91pub fn pump(
104 handle: &LiveHandle,
105 mic: mpsc::Receiver<Vec<i16>>,
106 mic_hz: u32,
107 speaker: mpsc::Sender<Playback>,
108 speaker_hz: u32,
109) -> VoicePump {
110 pump_processed(handle, mic, mic_hz, Vec::new(), speaker, speaker_hz)
111}
112
113pub fn pump_processed(
126 handle: &LiveHandle,
127 mut mic: mpsc::Receiver<Vec<i16>>,
128 mic_hz: u32,
129 mut processors: Vec<Box<dyn InputAudioProcessor>>,
130 speaker: mpsc::Sender<Playback>,
131 speaker_hz: u32,
132) -> VoicePump {
133 let uplink_handle = handle.clone();
134 let uplink = tokio::spawn(async move {
135 while let Some(mut frame) = mic.recv().await {
136 for processor in &mut processors {
137 processor.process_frame(&mut frame);
138 }
139 let samples = resample(&frame, mic_hz, SESSION_INPUT_HZ);
140 if uplink_handle
141 .send_audio(i16_to_bytes(&samples).to_vec())
142 .await
143 .is_err()
144 {
145 break;
146 }
147 }
148 });
149
150 let mut events = handle.events();
151 let downlink = tokio::spawn(async move {
152 loop {
153 match events.recv().await {
154 Ok(event) => match playback_of(&event, speaker_hz) {
155 Some(playback) => {
156 if speaker.send(playback).await.is_err() {
157 break;
158 }
159 }
160 None => continue,
161 },
162 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
163 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
164 }
165 }
166 });
167
168 VoicePump { uplink, downlink }
169}
170
171pub struct NoiseGate {
176 threshold_rms: f64,
177 hold_frames: u32,
178 open_for: u32,
179}
180
181impl NoiseGate {
182 pub fn new(threshold_rms: f64, hold_frames: u32) -> Self {
187 Self {
188 threshold_rms,
189 hold_frames,
190 open_for: 0,
191 }
192 }
193}
194
195impl InputAudioProcessor for NoiseGate {
196 fn process_frame(&mut self, frame: &mut Vec<i16>) {
197 if frame.is_empty() {
198 return;
199 }
200 let energy: f64 = frame.iter().map(|&s| f64::from(s) * f64::from(s)).sum();
201 let rms = (energy / frame.len() as f64).sqrt();
202 if rms >= self.threshold_rms {
203 self.open_for = self.hold_frames + 1;
204 }
205 if self.open_for > 0 {
206 self.open_for -= 1;
207 } else {
208 frame.fill(0);
209 }
210 }
211}
212
213pub(crate) fn playback_of(event: &LiveEvent, speaker_hz: u32) -> Option<Playback> {
216 match event {
217 LiveEvent::Audio(bytes) => {
218 let samples = bytes_to_i16(bytes)?;
219 Some(Playback::Chunk(resample(
220 samples,
221 SESSION_OUTPUT_HZ,
222 speaker_hz,
223 )))
224 }
225 LiveEvent::Interrupted => Some(Playback::Flush),
226 _ => None,
227 }
228}
229
230pub fn resample(input: &[i16], from_hz: u32, to_hz: u32) -> Vec<i16> {
236 if from_hz == to_hz || input.is_empty() {
237 return input.to_vec();
238 }
239 let ratio = from_hz as f64 / to_hz as f64;
240 let out_len = ((input.len() as f64) / ratio).floor() as usize;
241 let mut out = Vec::with_capacity(out_len);
242 for i in 0..out_len {
243 let pos = i as f64 * ratio;
244 let idx = pos as usize;
245 let frac = pos - idx as f64;
246 let a = input[idx] as f64;
247 let b = input[(idx + 1).min(input.len() - 1)] as f64;
248 out.push((a + (b - a) * frac).round() as i16);
249 }
250 out
251}
252
253pub fn downmix(interleaved: &[i16], channels: u16) -> Vec<i16> {
255 if channels <= 1 {
256 return interleaved.to_vec();
257 }
258 let channels = channels as usize;
259 interleaved
260 .chunks_exact(channels)
261 .map(|frame| (frame.iter().map(|&s| s as i32).sum::<i32>() / channels as i32) as i16)
262 .collect()
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use bytes::Bytes;
269
270 #[test]
271 fn resample_preserves_duration() {
272 let input = vec![1000i16; 480];
274 assert_eq!(resample(&input, 48_000, 16_000).len(), 160);
275 let output = vec![1000i16; 240];
277 assert_eq!(resample(&output, 24_000, 48_000).len(), 480);
278 }
279
280 #[test]
281 fn resample_same_rate_is_identity() {
282 let input = vec![1, -2, 3, -4];
283 assert_eq!(resample(&input, 16_000, 16_000), input);
284 }
285
286 #[test]
287 fn resample_interpolates_between_samples() {
288 let out = resample(&[0, 100], 1, 2);
290 assert_eq!(out.len(), 4);
291 assert_eq!(out[0], 0);
292 assert_eq!(out[1], 50);
293 }
294
295 #[test]
296 fn downmix_averages_channels() {
297 assert_eq!(downmix(&[100, 300, -50, 50], 2), vec![200, 0]);
299 assert_eq!(downmix(&[7, 8], 1), vec![7, 8]);
301 }
302
303 #[test]
304 fn audio_events_become_resampled_chunks() {
305 let samples = vec![500i16; 240];
307 let event = LiveEvent::Audio(Bytes::copy_from_slice(i16_to_bytes(&samples)));
308 match playback_of(&event, 48_000) {
309 Some(Playback::Chunk(chunk)) => assert_eq!(chunk.len(), 480),
310 other => panic!("expected a chunk, got {other:?}"),
311 }
312 }
313
314 #[test]
315 fn interruption_becomes_flush() {
316 assert_eq!(
317 playback_of(&LiveEvent::Interrupted, 48_000),
318 Some(Playback::Flush)
319 );
320 }
321
322 #[test]
323 fn unrelated_events_produce_no_playback() {
324 assert_eq!(playback_of(&LiveEvent::TurnComplete, 48_000), None);
325 }
326
327 #[test]
328 fn noise_gate_silences_quiet_frames_with_hangover() {
329 let mut gate = NoiseGate::new(1_000.0, 1);
330 let loud = vec![8_000i16; 160];
331 let quiet = vec![50i16; 160];
332
333 let mut frame = loud.clone();
334 gate.process_frame(&mut frame);
335 assert_eq!(frame, loud, "speech passes untouched");
336
337 let mut frame = quiet.clone();
338 gate.process_frame(&mut frame);
339 assert_eq!(frame, quiet, "hangover keeps the tail of an utterance");
340
341 let mut frame = quiet.clone();
342 gate.process_frame(&mut frame);
343 assert_eq!(frame, vec![0i16; 160], "sustained quiet is gated");
344
345 let mut frame = loud.clone();
346 gate.process_frame(&mut frame);
347 assert_eq!(frame, loud, "the gate reopens on speech");
348 }
349
350 #[test]
351 fn resample_single_sample_upsampling() {
352 let result = resample(&[100i16], 8_000, 24_000);
354 assert!(
355 !result.is_empty(),
356 "upsampling single sample should produce output"
357 );
358 assert_eq!(
359 result[0], 100,
360 "single sample upsampled should preserve value"
361 );
362 }
363
364 #[test]
365 fn resample_single_sample_downsampling() {
366 let result = resample(&[100i16], 24_000, 8_000);
368 assert!(
371 result.len() <= 1,
372 "downsampling single sample should produce at most 1 sample"
373 );
374 }
375
376 #[test]
377 fn resample_extreme_upsampling_ratio() {
378 let result = resample(&[1000i16], 100, 48_000);
380 assert!(
381 !result.is_empty(),
382 "extreme upsampling should produce output"
383 );
384 assert!(
386 result.len() >= 400,
387 "extreme upsampling should produce proportional output"
388 );
389 }
390
391 #[test]
392 fn resample_maintains_edge_indices_without_panic() {
393 for len in &[1, 2, 3, 159, 160, 161, 479, 480, 481] {
395 let input = vec![100i16; *len];
396 let down = resample(&input, 48_000, 16_000);
397 let up = resample(&input, 16_000, 48_000);
398 assert!(
399 !down.is_empty() || *len <= 4,
400 "downsampling should produce some output or be very short"
401 );
402 assert!(
404 !up.is_empty(),
405 "upsampling non-empty input should produce output"
406 );
407 }
408 }
409
410 #[test]
411 fn resample_boundary_value_clamping() {
412 let extreme = vec![i16::MIN, 0, i16::MAX];
414 let result = resample(&extreme, 16_000, 24_000);
415 for &sample in &result {
416 assert!(
417 (i16::MIN..=i16::MAX).contains(&sample),
418 "resampled sample in valid i16 range"
419 );
420 }
421 }
422
423 #[test]
424 fn noise_gate_accumulates_open_duration() {
425 let mut gate = NoiseGate::new(1_000.0, 3);
427 let loud = vec![8_000i16; 160];
428 let quiet = vec![100i16; 160];
429
430 let mut frame = loud.clone();
432 gate.process_frame(&mut frame);
433 assert_eq!(frame, loud);
434
435 let mut frame = quiet.clone();
437 gate.process_frame(&mut frame);
438 assert_eq!(frame, quiet);
439
440 let mut frame = quiet.clone();
442 gate.process_frame(&mut frame);
443 assert_eq!(frame, quiet);
444
445 let mut frame = quiet.clone();
447 gate.process_frame(&mut frame);
448 assert_eq!(frame, quiet);
449
450 let mut frame = quiet.clone();
452 gate.process_frame(&mut frame);
453 assert_eq!(frame, vec![0i16; 160]);
454 }
455}