1pub mod filter;
33pub mod mixer;
34pub mod strategy;
35pub mod turn;
36pub mod vad;
37
38#[cfg(feature = "filter-rnnoise")]
40pub use filter::RnnoiseFilter;
41pub use filter::{AudioFilter, AudioFilterProcessor, IdentityFilter};
42pub use mixer::{AudioMixer, MixerProcessor, SilenceMixer, SoundfileMixer};
43pub use strategy::{
44 AlwaysInterrupt, AlwaysMute, ExternalTurnStart, FirstSpeechMute, FunctionCallMute,
45 InterruptionStrategy, MinWordsInterrupt, MinWordsTurnStart, MuteStrategy,
46 MuteUntilFirstBotComplete, SpeechTimeoutStop, TurnAnalyzerStop, TurnStartDecision,
47 TurnStartStrategy, TurnStopDecision, TurnStopStrategy, VadTurnStart,
48};
49#[cfg(feature = "vad-ort")]
50pub use turn::SmartTurn;
51pub use turn::{MockTurn, TurnProcessor, TurnSilenceTracker};
52#[cfg(feature = "vad-ort")]
53pub use vad::SileroVad;
54pub use vad::{window_volume, MockVad, VadProcessor, VadStateMachine};
55
56use std::collections::HashMap;
57use std::io::Cursor;
58use std::time::{Duration, Instant};
59
60use hound::{SampleFormat, WavSpec, WavWriter};
61
62use crate::codec::Resampler;
63use crate::error::FlowcatError;
64use crate::types::AudioChunk;
65
66struct PlacedChunk {
70 offset_s: f64,
71 chunk: AudioChunk,
72}
73
74pub struct AudioRecorder {
83 pub sample_rate: u32,
85 inbound: Vec<PlacedChunk>,
87 outbound: Vec<PlacedChunk>,
89 start: Option<Instant>,
92 inbound_end_s: f64,
95 outbound_end_s: f64,
96}
97
98impl AudioRecorder {
99 pub fn new(sample_rate: u32) -> Self {
101 Self {
102 sample_rate,
103 inbound: Vec::new(),
104 outbound: Vec::new(),
105 start: None,
106 inbound_end_s: 0.0,
107 outbound_end_s: 0.0,
108 }
109 }
110
111 fn tick(&mut self) -> Duration {
113 match self.start {
114 Some(s) => s.elapsed(),
115 None => {
116 self.start = Some(Instant::now());
117 Duration::ZERO
118 }
119 }
120 }
121
122 pub fn push_inbound(&mut self, chunk: &AudioChunk) {
127 let at = self.tick();
128 self.push_inbound_at(at, chunk);
129 }
130
131 pub fn push_outbound(&mut self, chunk: &AudioChunk) {
133 let at = self.tick();
134 self.push_outbound_at(at, chunk);
135 }
136
137 pub fn push_inbound_at(&mut self, offset: Duration, chunk: &AudioChunk) {
142 if let Some(p) = place(offset, chunk, self.inbound_end_s) {
143 self.inbound_end_s = p.offset_s + chunk_duration_s(chunk);
144 self.inbound.push(p);
145 }
146 }
147
148 pub fn push_outbound_at(&mut self, offset: Duration, chunk: &AudioChunk) {
150 if let Some(p) = place(offset, chunk, self.outbound_end_s) {
151 self.outbound_end_s = p.offset_s + chunk_duration_s(chunk);
152 self.outbound.push(p);
153 }
154 }
155
156 pub fn duration_seconds(&self) -> f64 {
159 self.inbound_end_s.max(self.outbound_end_s)
160 }
161
162 pub fn render_wav(&self) -> Result<Vec<u8>, FlowcatError> {
170 let caller = render_leg(&self.inbound, self.sample_rate)?;
171 let bot = render_leg(&self.outbound, self.sample_rate)?;
172
173 let n = caller.len().max(bot.len());
174 let mut mixed = Vec::with_capacity(n);
175 for i in 0..n {
176 let a = *caller.get(i).unwrap_or(&0) as i32;
177 let b = *bot.get(i).unwrap_or(&0) as i32;
178 mixed.push((a + b).clamp(i16::MIN as i32, i16::MAX as i32) as i16);
179 }
180
181 let spec = WavSpec {
182 channels: 1,
183 sample_rate: self.sample_rate,
184 bits_per_sample: 16,
185 sample_format: SampleFormat::Int,
186 };
187
188 let mut buf: Vec<u8> = Vec::new();
192 {
193 let mut writer = WavWriter::new(Cursor::new(&mut buf), spec)
194 .map_err(|e| FlowcatError::Codec(format!("wav header: {e}")))?;
195 for &s in &mixed {
196 writer
197 .write_sample(s)
198 .map_err(|e| FlowcatError::Codec(format!("wav sample: {e}")))?;
199 }
200 writer
201 .finalize()
202 .map_err(|e| FlowcatError::Codec(format!("wav finalize: {e}")))?;
203 }
204 Ok(buf)
205 }
206}
207
208fn chunk_duration_s(chunk: &AudioChunk) -> f64 {
210 if chunk.sample_rate == 0 {
211 0.0
212 } else {
213 chunk.pcm.len() as f64 / chunk.sample_rate as f64
214 }
215}
216
217fn place(offset: Duration, chunk: &AudioChunk, leg_end_s: f64) -> Option<PlacedChunk> {
221 if chunk.is_empty() {
222 return None;
223 }
224 Some(PlacedChunk {
225 offset_s: offset.as_secs_f64().max(leg_end_s),
226 chunk: chunk.clone(),
227 })
228}
229
230fn render_leg(chunks: &[PlacedChunk], target_rate: u32) -> Result<Vec<i16>, FlowcatError> {
238 let mut out: Vec<i16> = Vec::new();
239 let mut resamplers: HashMap<u32, Resampler> = HashMap::new();
240
241 for pc in chunks {
242 let target = (pc.offset_s * target_rate as f64).round() as usize;
244 if out.len() < target {
245 out.resize(target, 0);
246 }
247
248 if pc.chunk.sample_rate == target_rate {
249 out.extend_from_slice(&pc.chunk.pcm);
250 continue;
251 }
252 let rs = match resamplers.get_mut(&pc.chunk.sample_rate) {
253 Some(rs) => rs,
254 None => {
255 let rs = Resampler::new(pc.chunk.sample_rate, target_rate)?;
256 resamplers.entry(pc.chunk.sample_rate).or_insert(rs)
257 }
258 };
259 let res = rs.process(&pc.chunk)?;
260 out.extend_from_slice(&res.pcm);
261 }
262
263 for rs in resamplers.values_mut() {
265 let tail = rs.flush()?;
266 out.extend_from_slice(&tail.pcm);
267 }
268
269 Ok(out)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use hound::WavReader;
276
277 fn tone(rate: u32, n: usize, freq: f32, amp: f32) -> AudioChunk {
278 let pcm = (0..n)
279 .map(|i| {
280 let t = i as f32 / rate as f32;
281 (amp * (2.0 * std::f32::consts::PI * freq * t).sin()) as i16
282 })
283 .collect();
284 AudioChunk::new(pcm, rate)
285 }
286
287 fn parse_wav(bytes: &[u8]) -> (WavSpec, Vec<i16>) {
289 let mut reader = WavReader::new(Cursor::new(bytes)).expect("valid WAV header");
290 let spec = reader.spec();
291 let samples: Vec<i16> = reader
292 .samples::<i16>()
293 .map(|s| s.expect("readable sample"))
294 .collect();
295 (spec, samples)
296 }
297
298 #[test]
299 fn renders_valid_wav_header_at_recorder_rate() {
300 let mut rec = AudioRecorder::new(8000);
301 rec.push_inbound_at(Duration::ZERO, &tone(8000, 800, 440.0, 8000.0));
303 rec.push_outbound_at(Duration::ZERO, &tone(8000, 800, 660.0, 8000.0));
304
305 let bytes = rec.render_wav().unwrap();
306 assert!(!bytes.is_empty());
307 assert_eq!(&bytes[0..4], b"RIFF");
308 assert_eq!(&bytes[8..12], b"WAVE");
309
310 let (spec, samples) = parse_wav(&bytes);
311 assert_eq!(spec.channels, 1, "mono");
312 assert_eq!(spec.sample_rate, 8000);
313 assert_eq!(spec.bits_per_sample, 16);
314 assert_eq!(spec.sample_format, SampleFormat::Int);
315 assert_eq!(samples.len(), 800, "same-rate legs mix 1:1");
316 }
317
318 #[test]
319 fn mixes_two_legs_by_summation() {
320 let mut rec = AudioRecorder::new(8000);
321 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1000i16; 100], 8000));
323 rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![2000i16; 100], 8000));
324
325 let (_, samples) = parse_wav(&rec.render_wav().unwrap());
326 assert_eq!(samples.len(), 100);
327 assert!(samples.iter().all(|&s| s == 3000), "1000 + 2000 = 3000");
328 }
329
330 #[test]
331 fn mix_clamps_instead_of_wrapping() {
332 let mut rec = AudioRecorder::new(8000);
333 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![30000i16; 10], 8000));
334 rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![30000i16; 10], 8000));
335
336 let (_, samples) = parse_wav(&rec.render_wav().unwrap());
337 assert!(samples.iter().all(|&s| s == i16::MAX));
339 }
340
341 #[test]
342 fn longer_leg_determines_length_shorter_zero_padded() {
343 let mut rec = AudioRecorder::new(8000);
344 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![500i16; 200], 8000));
345 rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![500i16; 50], 8000));
346
347 let (_, samples) = parse_wav(&rec.render_wav().unwrap());
348 assert_eq!(samples.len(), 200, "length = longer leg");
349 assert_eq!(samples[0], 1000);
351 assert_eq!(samples[199], 500);
352 }
353
354 #[test]
355 fn resamples_legs_at_different_rates_to_common_rate() {
356 let mut rec = AudioRecorder::new(16000);
359 rec.push_inbound_at(Duration::ZERO, &tone(8000, 1600, 440.0, 8000.0)); rec.push_outbound_at(Duration::ZERO, &tone(24000, 4800, 660.0, 8000.0)); let bytes = rec.render_wav().unwrap();
363 let (spec, samples) = parse_wav(&bytes);
364 assert_eq!(spec.sample_rate, 16000);
365 assert_eq!(spec.channels, 1);
366 assert!(
371 (3000..=3700).contains(&samples.len()),
372 "expected ~3200 samples @16k, got {}",
373 samples.len()
374 );
375 }
376
377 #[test]
378 fn empty_recorder_renders_valid_empty_wav() {
379 let rec = AudioRecorder::new(8000);
380 let bytes = rec.render_wav().unwrap();
381 let (spec, samples) = parse_wav(&bytes);
382 assert_eq!(spec.sample_rate, 8000);
383 assert_eq!(samples.len(), 0);
384 }
385
386 #[test]
387 fn empty_chunks_are_ignored() {
388 let mut rec = AudioRecorder::new(8000);
389 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![], 8000));
390 rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![], 8000));
391 let (_, samples) = parse_wav(&rec.render_wav().unwrap());
392 assert_eq!(samples.len(), 0);
393 }
394
395 #[test]
396 fn sequential_turns_do_not_overlap_on_the_timeline() {
397 let mut rec = AudioRecorder::new(8000);
401 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1000i16; 100], 8000));
402 rec.push_outbound_at(
403 Duration::from_millis(500),
404 &AudioChunk::new(vec![2000i16; 100], 8000),
405 );
406
407 let (_, samples) = parse_wav(&rec.render_wav().unwrap());
408 assert_eq!(samples.len(), 4100, "timeline spans to the bot turn");
410 assert_eq!(samples[50], 1000, "user turn, bot silent");
411 assert_eq!(samples[4050], 2000, "bot turn, user silent");
412 assert!(
413 !samples.contains(&3000),
414 "sequential turns must not overlap (no summed 1000+2000)"
415 );
416 }
417
418 #[test]
419 fn duration_seconds_spans_to_last_audio() {
420 let mut rec = AudioRecorder::new(8000);
421 rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1i16; 8000], 8000));
423 rec.push_outbound_at(
424 Duration::from_secs(2),
425 &AudioChunk::new(vec![1i16; 4000], 8000),
426 );
427 assert!(
428 (rec.duration_seconds() - 2.5).abs() < 1e-6,
429 "duration spans to the later leg's last audio, got {}",
430 rec.duration_seconds()
431 );
432 }
433}