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;
42mod resampler;
43pub use resampler::StreamResampler;
44
45#[cfg(feature = "denoise")]
46pub use denoise::Denoiser;
47
48pub use gemini_adk_rs::live::InputAudioProcessor;
52use gemini_adk_rs::live::{LiveEvent, LiveHandle};
53use gemini_genai_rs::prelude::{bytes_to_i16, i16_to_bytes};
54use tokio::sync::mpsc;
55use tokio::task::JoinHandle;
56
57pub const SESSION_INPUT_HZ: u32 = 16_000;
59pub const SESSION_OUTPUT_HZ: u32 = 24_000;
61
62#[derive(Debug, Clone, PartialEq)]
64pub enum Playback {
65 Chunk(Vec<i16>),
67 Flush,
70}
71
72pub struct VoicePump {
75 uplink: JoinHandle<()>,
76 downlink: JoinHandle<()>,
77}
78
79impl VoicePump {
80 pub async fn join(self) {
82 let _ = self.uplink.await;
83 let _ = self.downlink.await;
84 }
85
86 pub fn abort(&self) {
88 self.uplink.abort();
89 self.downlink.abort();
90 }
91}
92
93pub fn pump(
110 handle: &LiveHandle,
111 mic: mpsc::Receiver<Vec<i16>>,
112 mic_hz: u32,
113 speaker: mpsc::Sender<Playback>,
114 speaker_hz: u32,
115) -> VoicePump {
116 pump_processed(handle, mic, mic_hz, Vec::new(), speaker, speaker_hz)
117}
118
119pub fn pump_processed(
132 handle: &LiveHandle,
133 mut mic: mpsc::Receiver<Vec<i16>>,
134 mic_hz: u32,
135 mut processors: Vec<Box<dyn InputAudioProcessor>>,
136 speaker: mpsc::Sender<Playback>,
137 speaker_hz: u32,
138) -> VoicePump {
139 let uplink_handle = handle.clone();
140 let uplink = tokio::spawn(async move {
141 let mut to_session = StreamResampler::new(mic_hz, SESSION_INPUT_HZ);
143 while let Some(mut frame) = mic.recv().await {
144 for processor in &mut processors {
145 processor.process_frame(&mut frame);
146 }
147 let samples = to_session.process(&frame);
148 if uplink_handle
149 .send_audio(i16_to_bytes(&samples).to_vec())
150 .await
151 .is_err()
152 {
153 break;
154 }
155 }
156 });
157
158 let mut events = handle.events();
159 let clock = handle.playback().clone();
160 let downlink = tokio::spawn(async move {
161 let mut to_speaker = StreamResampler::new(SESSION_OUTPUT_HZ, speaker_hz);
162 loop {
163 match events.recv().await {
164 Ok(event) => match playback_of(&event, &mut to_speaker) {
165 Some(playback) => {
166 match &playback {
169 Playback::Chunk(samples) => {
170 clock.queued(std::time::Duration::from_secs_f64(
171 samples.len() as f64 / f64::from(speaker_hz.max(1)),
172 ));
173 }
174 Playback::Flush => clock.flushed(),
175 }
176 if speaker.send(playback).await.is_err() {
177 break;
178 }
179 }
180 None => continue,
181 },
182 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
183 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
184 }
185 }
186 });
187
188 VoicePump { uplink, downlink }
189}
190
191pub struct NoiseGate {
196 threshold_rms: f64,
197 hold_frames: u32,
198 open_for: u32,
199}
200
201impl NoiseGate {
202 pub fn new(threshold_rms: f64, hold_frames: u32) -> Self {
207 Self {
208 threshold_rms,
209 hold_frames,
210 open_for: 0,
211 }
212 }
213}
214
215impl InputAudioProcessor for NoiseGate {
216 fn process_frame(&mut self, frame: &mut Vec<i16>) {
217 if frame.is_empty() {
218 return;
219 }
220 let energy: f64 = frame.iter().map(|&s| f64::from(s) * f64::from(s)).sum();
221 let rms = (energy / frame.len() as f64).sqrt();
222 if rms >= self.threshold_rms {
223 self.open_for = self.hold_frames + 1;
224 }
225 if self.open_for > 0 {
226 self.open_for -= 1;
227 } else {
228 frame.fill(0);
229 }
230 }
231}
232
233pub(crate) fn playback_of(event: &LiveEvent, to_speaker: &mut StreamResampler) -> Option<Playback> {
236 match event {
237 LiveEvent::Audio(bytes) => {
238 let samples = bytes_to_i16(bytes)?;
239 Some(Playback::Chunk(to_speaker.process(samples)))
240 }
241 LiveEvent::Interrupted => {
242 to_speaker.reset();
244 Some(Playback::Flush)
245 }
246 _ => None,
247 }
248}
249
250pub fn resample(input: &[i16], from_hz: u32, to_hz: u32) -> Vec<i16> {
258 if from_hz == to_hz || input.is_empty() {
259 return input.to_vec();
260 }
261 let ratio = from_hz as f64 / to_hz as f64;
262 let out_len = ((input.len() as f64) / ratio).floor() as usize;
263 let mut out = Vec::with_capacity(out_len);
264 for i in 0..out_len {
265 let pos = i as f64 * ratio;
266 let idx = pos as usize;
267 let frac = pos - idx as f64;
268 let a = input[idx] as f64;
269 let b = input[(idx + 1).min(input.len() - 1)] as f64;
270 out.push((a + (b - a) * frac).round() as i16);
271 }
272 out
273}
274
275pub fn downmix(interleaved: &[i16], channels: u16) -> Vec<i16> {
277 if channels <= 1 {
278 return interleaved.to_vec();
279 }
280 let channels = channels as usize;
281 interleaved
282 .chunks_exact(channels)
283 .map(|frame| (frame.iter().map(|&s| s as i32).sum::<i32>() / channels as i32) as i16)
284 .collect()
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use bytes::Bytes;
291
292 #[test]
293 fn resample_preserves_duration() {
294 let input = vec![1000i16; 480];
296 assert_eq!(resample(&input, 48_000, 16_000).len(), 160);
297 let output = vec![1000i16; 240];
299 assert_eq!(resample(&output, 24_000, 48_000).len(), 480);
300 }
301
302 #[test]
303 fn resample_same_rate_is_identity() {
304 let input = vec![1, -2, 3, -4];
305 assert_eq!(resample(&input, 16_000, 16_000), input);
306 }
307
308 #[test]
309 fn resample_interpolates_between_samples() {
310 let out = resample(&[0, 100], 1, 2);
312 assert_eq!(out.len(), 4);
313 assert_eq!(out[0], 0);
314 assert_eq!(out[1], 50);
315 }
316
317 #[test]
318 fn downmix_averages_channels() {
319 assert_eq!(downmix(&[100, 300, -50, 50], 2), vec![200, 0]);
321 assert_eq!(downmix(&[7, 8], 1), vec![7, 8]);
323 }
324
325 #[test]
326 fn audio_events_become_resampled_chunks() {
327 let samples = vec![500i16; 240];
329 let event = LiveEvent::Audio(Bytes::copy_from_slice(i16_to_bytes(&samples)));
330 match playback_of(&event, &mut StreamResampler::new(SESSION_OUTPUT_HZ, 48_000)) {
331 Some(Playback::Chunk(chunk)) => assert_eq!(chunk.len(), 480),
332 other => panic!("expected a chunk, got {other:?}"),
333 }
334 }
335
336 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
337 async fn the_pump_reports_what_it_plays() {
338 use base64::Engine as _;
339 use std::time::Duration;
340 let second = serde_json::json!({ "serverContent": { "modelTurn": { "parts": [{
342 "inlineData": {
343 "mimeType": "audio/pcm;rate=24000",
344 "data": base64::engine::general_purpose::STANDARD.encode(vec![0u8; 48_000]),
345 }
346 }] } } });
347 let (transport, control) = crate::live::scripted::ScriptedServer::new()
348 .frame(second.clone())
349 .frame(second)
350 .interrupts()
351 .into_transport();
352 let handle = crate::live::Live::builder()
353 .connect_with_transport(transport)
354 .await
355 .unwrap();
356 let (_mic_tx, mic_rx) = mpsc::channel(4);
357 let (speaker_tx, mut speaker_rx) = mpsc::channel(16);
358 let _pump = pump(&handle, mic_rx, 16_000, speaker_tx, 8_000);
359 assert_eq!(handle.playback().heard(), None);
360 control.release();
361
362 let mut got = Vec::new();
363 while got.len() < 3 {
364 match tokio::time::timeout(Duration::from_secs(3), speaker_rx.recv()).await {
365 Ok(Some(Playback::Chunk(c))) if c.is_empty() => {}
366 Ok(Some(p)) => got.push(p),
367 other => panic!("pump stopped early: {other:?}"),
368 }
369 }
370 assert!(matches!(got[2], Playback::Flush));
371 let heard = handle.playback().heard().expect("the pump reports");
374 assert!(heard < Duration::from_millis(500), "{heard:?}");
375 tokio::time::sleep(Duration::from_millis(100)).await;
376 assert_eq!(handle.playback().heard(), Some(heard));
377 handle.disconnect().await.ok();
378 }
379
380 #[test]
381 fn interruption_becomes_flush() {
382 assert_eq!(
383 playback_of(
384 &LiveEvent::Interrupted,
385 &mut StreamResampler::new(SESSION_OUTPUT_HZ, 48_000)
386 ),
387 Some(Playback::Flush)
388 );
389 }
390
391 #[test]
392 fn unrelated_events_produce_no_playback() {
393 assert_eq!(
394 playback_of(
395 &LiveEvent::TurnComplete,
396 &mut StreamResampler::new(SESSION_OUTPUT_HZ, 48_000)
397 ),
398 None
399 );
400 }
401
402 #[test]
403 fn noise_gate_silences_quiet_frames_with_hangover() {
404 let mut gate = NoiseGate::new(1_000.0, 1);
405 let loud = vec![8_000i16; 160];
406 let quiet = vec![50i16; 160];
407
408 let mut frame = loud.clone();
409 gate.process_frame(&mut frame);
410 assert_eq!(frame, loud, "speech passes untouched");
411
412 let mut frame = quiet.clone();
413 gate.process_frame(&mut frame);
414 assert_eq!(frame, quiet, "hangover keeps the tail of an utterance");
415
416 let mut frame = quiet.clone();
417 gate.process_frame(&mut frame);
418 assert_eq!(frame, vec![0i16; 160], "sustained quiet is gated");
419
420 let mut frame = loud.clone();
421 gate.process_frame(&mut frame);
422 assert_eq!(frame, loud, "the gate reopens on speech");
423 }
424
425 #[test]
426 fn resample_single_sample_upsampling() {
427 let result = resample(&[100i16], 8_000, 24_000);
429 assert!(
430 !result.is_empty(),
431 "upsampling single sample should produce output"
432 );
433 assert_eq!(
434 result[0], 100,
435 "single sample upsampled should preserve value"
436 );
437 }
438
439 #[test]
440 fn resample_single_sample_downsampling() {
441 let result = resample(&[100i16], 24_000, 8_000);
443 assert!(
446 result.len() <= 1,
447 "downsampling single sample should produce at most 1 sample"
448 );
449 }
450
451 #[test]
452 fn resample_extreme_upsampling_ratio() {
453 let result = resample(&[1000i16], 100, 48_000);
455 assert!(
456 !result.is_empty(),
457 "extreme upsampling should produce output"
458 );
459 assert!(
461 result.len() >= 400,
462 "extreme upsampling should produce proportional output"
463 );
464 }
465
466 #[test]
467 fn resample_maintains_edge_indices_without_panic() {
468 for len in &[1, 2, 3, 159, 160, 161, 479, 480, 481] {
470 let input = vec![100i16; *len];
471 let down = resample(&input, 48_000, 16_000);
472 let up = resample(&input, 16_000, 48_000);
473 assert!(
474 !down.is_empty() || *len <= 4,
475 "downsampling should produce some output or be very short"
476 );
477 assert!(
479 !up.is_empty(),
480 "upsampling non-empty input should produce output"
481 );
482 }
483 }
484
485 #[test]
486 fn resample_boundary_value_clamping() {
487 let extreme = vec![i16::MIN, 0, i16::MAX];
489 let result = resample(&extreme, 16_000, 24_000);
490 for &sample in &result {
491 assert!(
492 (i16::MIN..=i16::MAX).contains(&sample),
493 "resampled sample in valid i16 range"
494 );
495 }
496 }
497
498 #[test]
499 fn noise_gate_accumulates_open_duration() {
500 let mut gate = NoiseGate::new(1_000.0, 3);
502 let loud = vec![8_000i16; 160];
503 let quiet = vec![100i16; 160];
504
505 let mut frame = loud.clone();
507 gate.process_frame(&mut frame);
508 assert_eq!(frame, loud);
509
510 let mut frame = quiet.clone();
512 gate.process_frame(&mut frame);
513 assert_eq!(frame, quiet);
514
515 let mut frame = quiet.clone();
517 gate.process_frame(&mut frame);
518 assert_eq!(frame, quiet);
519
520 let mut frame = quiet.clone();
522 gate.process_frame(&mut frame);
523 assert_eq!(frame, quiet);
524
525 let mut frame = quiet.clone();
527 gate.process_frame(&mut frame);
528 assert_eq!(frame, vec![0i16; 160]);
529 }
530}