gemini_adk_fluent_rs/telephony/
recorder.rs1use std::collections::VecDeque;
21use std::fs::File;
22use std::io::{BufWriter, Seek, SeekFrom, Write};
23use std::path::Path;
24use std::time::{Duration, Instant};
25
26use parking_lot::Mutex;
27
28pub struct CallRecorder {
30 inner: Mutex<Inner>,
31}
32
33struct Inner {
34 writer: Option<BufWriter<File>>,
35 rate: u32,
36 start: Instant,
37 written: u64,
39 caller: VecDeque<i16>,
41 agent: VecDeque<i16>,
43}
44
45impl Inner {
46 fn now_frames(&self, at: Duration) -> u64 {
47 (at.as_secs_f64() * f64::from(self.rate)) as u64
48 }
49
50 fn caller_end(&self) -> u64 {
51 self.written + self.caller.len() as u64
52 }
53
54 fn agent_end(&self) -> u64 {
55 self.written + self.agent.len() as u64
56 }
57
58 fn drain_to(&mut self, upto: u64) -> std::io::Result<()> {
60 let Some(writer) = self.writer.as_mut() else {
61 return Ok(());
62 };
63 while self.written < upto {
64 let left = self.caller.pop_front().unwrap_or(0);
65 let right = self.agent.pop_front().unwrap_or(0);
66 writer.write_all(&left.to_le_bytes())?;
67 writer.write_all(&right.to_le_bytes())?;
68 self.written += 1;
69 }
70 Ok(())
71 }
72}
73
74impl CallRecorder {
75 pub fn create(path: impl AsRef<Path>, sample_rate: u32) -> std::io::Result<Self> {
78 let mut writer = BufWriter::new(File::create(path)?);
79 writer.write_all(&wav_header(sample_rate, 0))?;
80 Ok(Self {
81 inner: Mutex::new(Inner {
82 writer: Some(writer),
83 rate: sample_rate,
84 start: Instant::now(),
85 written: 0,
86 caller: VecDeque::new(),
87 agent: VecDeque::new(),
88 }),
89 })
90 }
91
92 pub fn caller(&self, samples: &[i16]) {
94 let at = self.inner.lock().start.elapsed();
95 self.caller_at(at, samples);
96 }
97
98 pub fn agent(&self, samples: &[i16]) {
100 let at = self.inner.lock().start.elapsed();
101 self.agent_at(at, samples);
102 }
103
104 pub fn flush(&self) -> Duration {
107 let at = self.inner.lock().start.elapsed();
108 self.flush_at(at)
109 }
110
111 pub fn finish(&self) -> std::io::Result<Duration> {
114 let mut inner = self.inner.lock();
115 let at = inner.start.elapsed();
116 let now = inner.now_frames(at);
117 let end = inner.caller_end().max(inner.agent_end().min(now));
119 inner.drain_to(end)?;
120 let frames = inner.written;
121 let rate = inner.rate;
122 if let Some(mut writer) = inner.writer.take() {
123 writer.flush()?;
124 let mut file = writer
125 .into_inner()
126 .map_err(std::io::IntoInnerError::into_error)?;
127 file.seek(SeekFrom::Start(0))?;
128 file.write_all(&wav_header(rate, frames))?;
129 file.sync_all()?;
130 }
131 Ok(Duration::from_secs_f64(frames as f64 / f64::from(rate)))
132 }
133
134 pub(crate) fn caller_at(&self, at: Duration, samples: &[i16]) {
135 let mut inner = self.inner.lock();
136 let arrived = inner.now_frames(at);
137 let begins = arrived.saturating_sub(samples.len() as u64);
140 let pad = begins.saturating_sub(inner.caller_end());
141 inner.caller.extend(std::iter::repeat_n(0, pad as usize));
142 inner.caller.extend(samples);
143 let safe = inner.caller_end().min(arrived);
144 if let Err(e) = inner.drain_to(safe) {
145 tracing::warn!("call recording stopped: {e}");
146 inner.writer = None;
147 }
148 }
149
150 pub(crate) fn agent_at(&self, at: Duration, samples: &[i16]) {
151 let mut inner = self.inner.lock();
152 let now = inner.now_frames(at);
153 let pad = now.saturating_sub(inner.agent_end());
154 inner.agent.extend(std::iter::repeat_n(0, pad as usize));
155 inner.agent.extend(samples);
156 }
157
158 pub(crate) fn flush_at(&self, at: Duration) -> Duration {
159 let mut inner = self.inner.lock();
160 let now = inner.now_frames(at).max(inner.written);
161 let keep = (now - inner.written) as usize;
162 let dropped = inner.agent.len().saturating_sub(keep);
163 inner.agent.truncate(keep);
164 Duration::from_secs_f64(dropped as f64 / f64::from(inner.rate))
165 }
166}
167
168fn wav_header(rate: u32, frames: u64) -> [u8; 44] {
169 let data = u32::try_from(frames * 4).unwrap_or(u32::MAX - 36);
170 let mut h = [0u8; 44];
171 h[0..4].copy_from_slice(b"RIFF");
172 h[4..8].copy_from_slice(&(36 + data).to_le_bytes());
173 h[8..12].copy_from_slice(b"WAVE");
174 h[12..16].copy_from_slice(b"fmt ");
175 h[16..20].copy_from_slice(&16u32.to_le_bytes());
176 h[20..22].copy_from_slice(&1u16.to_le_bytes()); h[22..24].copy_from_slice(&2u16.to_le_bytes()); h[24..28].copy_from_slice(&rate.to_le_bytes());
179 h[28..32].copy_from_slice(&(rate * 4).to_le_bytes()); h[32..34].copy_from_slice(&4u16.to_le_bytes()); h[34..36].copy_from_slice(&16u16.to_le_bytes()); h[36..40].copy_from_slice(b"data");
183 h[40..44].copy_from_slice(&data.to_le_bytes());
184 h
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn ms(n: u64) -> Duration {
192 Duration::from_millis(n)
193 }
194
195 fn read(path: &Path) -> (u32, Vec<(i16, i16)>) {
196 let bytes = std::fs::read(path).unwrap();
197 assert_eq!(&bytes[0..4], b"RIFF");
198 assert_eq!(u16::from_le_bytes([bytes[22], bytes[23]]), 2);
199 let rate = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
200 let len = u32::from_le_bytes(bytes[40..44].try_into().unwrap()) as usize;
201 assert_eq!(bytes.len(), 44 + len);
202 let frames = bytes[44..]
203 .chunks_exact(4)
204 .map(|f| {
205 (
206 i16::from_le_bytes([f[0], f[1]]),
207 i16::from_le_bytes([f[2], f[3]]),
208 )
209 })
210 .collect();
211 (rate, frames)
212 }
213
214 #[test]
215 fn caller_and_agent_are_aligned_on_the_call_clock() {
216 let path = std::env::temp_dir().join(format!("rec-{}.wav", std::process::id()));
217 let rec = CallRecorder::create(&path, 8_000).unwrap();
218 rec.caller_at(ms(20), &[1; 160]);
220 rec.agent_at(ms(30), &[2; 800]);
223 rec.caller_at(ms(60), &[3; 160]);
224 let cut = rec.flush_at(ms(80));
226 assert_eq!(cut, ms(50));
227 rec.caller_at(ms(100), &[4; 160]);
228 rec.finish().unwrap();
229
230 let (rate, frames) = read(&path);
231 assert_eq!(rate, 8_000);
232 assert_eq!(frames.len(), 800); let at = |t_ms: usize| frames[t_ms * 8];
234 assert_eq!(at(10), (1, 0));
235 assert_eq!(at(30), (0, 2)); assert_eq!(at(50), (3, 2));
237 assert_eq!(at(85), (4, 0)); let _ = std::fs::remove_file(path);
239 }
240}