pub mod filter;
pub mod mixer;
pub mod strategy;
pub mod turn;
pub mod vad;
#[cfg(feature = "filter-rnnoise")]
pub use filter::RnnoiseFilter;
pub use filter::{AudioFilter, AudioFilterProcessor, IdentityFilter};
pub use mixer::{AudioMixer, MixerProcessor, SilenceMixer, SoundfileMixer};
pub use strategy::{
AlwaysInterrupt, AlwaysMute, ExternalTurnStart, FirstSpeechMute, FunctionCallMute,
InterruptionStrategy, MinWordsInterrupt, MinWordsTurnStart, MuteStrategy,
MuteUntilFirstBotComplete, SpeechTimeoutStop, TurnAnalyzerStop, TurnStartDecision,
TurnStartStrategy, TurnStopDecision, TurnStopStrategy, VadTurnStart,
};
#[cfg(feature = "vad-ort")]
pub use turn::SmartTurn;
pub use turn::{MockTurn, TurnProcessor, TurnSilenceTracker};
#[cfg(feature = "vad-ort")]
pub use vad::SileroVad;
pub use vad::{window_volume, MockVad, VadProcessor, VadStateMachine};
use std::collections::HashMap;
use std::io::Cursor;
use std::time::{Duration, Instant};
use hound::{SampleFormat, WavSpec, WavWriter};
use crate::codec::Resampler;
use crate::error::FlowcatError;
use crate::types::AudioChunk;
struct PlacedChunk {
offset_s: f64,
chunk: AudioChunk,
}
pub struct AudioRecorder {
pub sample_rate: u32,
inbound: Vec<PlacedChunk>,
outbound: Vec<PlacedChunk>,
start: Option<Instant>,
inbound_end_s: f64,
outbound_end_s: f64,
}
impl AudioRecorder {
pub fn new(sample_rate: u32) -> Self {
Self {
sample_rate,
inbound: Vec::new(),
outbound: Vec::new(),
start: None,
inbound_end_s: 0.0,
outbound_end_s: 0.0,
}
}
fn tick(&mut self) -> Duration {
match self.start {
Some(s) => s.elapsed(),
None => {
self.start = Some(Instant::now());
Duration::ZERO
}
}
}
pub fn push_inbound(&mut self, chunk: &AudioChunk) {
let at = self.tick();
self.push_inbound_at(at, chunk);
}
pub fn push_outbound(&mut self, chunk: &AudioChunk) {
let at = self.tick();
self.push_outbound_at(at, chunk);
}
pub fn push_inbound_at(&mut self, offset: Duration, chunk: &AudioChunk) {
if let Some(p) = place(offset, chunk, self.inbound_end_s) {
self.inbound_end_s = p.offset_s + chunk_duration_s(chunk);
self.inbound.push(p);
}
}
pub fn push_outbound_at(&mut self, offset: Duration, chunk: &AudioChunk) {
if let Some(p) = place(offset, chunk, self.outbound_end_s) {
self.outbound_end_s = p.offset_s + chunk_duration_s(chunk);
self.outbound.push(p);
}
}
pub fn duration_seconds(&self) -> f64 {
self.inbound_end_s.max(self.outbound_end_s)
}
pub fn render_wav(&self) -> Result<Vec<u8>, FlowcatError> {
let caller = render_leg(&self.inbound, self.sample_rate)?;
let bot = render_leg(&self.outbound, self.sample_rate)?;
let n = caller.len().max(bot.len());
let mut mixed = Vec::with_capacity(n);
for i in 0..n {
let a = *caller.get(i).unwrap_or(&0) as i32;
let b = *bot.get(i).unwrap_or(&0) as i32;
mixed.push((a + b).clamp(i16::MIN as i32, i16::MAX as i32) as i16);
}
let spec = WavSpec {
channels: 1,
sample_rate: self.sample_rate,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut buf: Vec<u8> = Vec::new();
{
let mut writer = WavWriter::new(Cursor::new(&mut buf), spec)
.map_err(|e| FlowcatError::Codec(format!("wav header: {e}")))?;
for &s in &mixed {
writer
.write_sample(s)
.map_err(|e| FlowcatError::Codec(format!("wav sample: {e}")))?;
}
writer
.finalize()
.map_err(|e| FlowcatError::Codec(format!("wav finalize: {e}")))?;
}
Ok(buf)
}
}
fn chunk_duration_s(chunk: &AudioChunk) -> f64 {
if chunk.sample_rate == 0 {
0.0
} else {
chunk.pcm.len() as f64 / chunk.sample_rate as f64
}
}
fn place(offset: Duration, chunk: &AudioChunk, leg_end_s: f64) -> Option<PlacedChunk> {
if chunk.is_empty() {
return None;
}
Some(PlacedChunk {
offset_s: offset.as_secs_f64().max(leg_end_s),
chunk: chunk.clone(),
})
}
fn render_leg(chunks: &[PlacedChunk], target_rate: u32) -> Result<Vec<i16>, FlowcatError> {
let mut out: Vec<i16> = Vec::new();
let mut resamplers: HashMap<u32, Resampler> = HashMap::new();
for pc in chunks {
let target = (pc.offset_s * target_rate as f64).round() as usize;
if out.len() < target {
out.resize(target, 0);
}
if pc.chunk.sample_rate == target_rate {
out.extend_from_slice(&pc.chunk.pcm);
continue;
}
let rs = match resamplers.get_mut(&pc.chunk.sample_rate) {
Some(rs) => rs,
None => {
let rs = Resampler::new(pc.chunk.sample_rate, target_rate)?;
resamplers.entry(pc.chunk.sample_rate).or_insert(rs)
}
};
let res = rs.process(&pc.chunk)?;
out.extend_from_slice(&res.pcm);
}
for rs in resamplers.values_mut() {
let tail = rs.flush()?;
out.extend_from_slice(&tail.pcm);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use hound::WavReader;
fn tone(rate: u32, n: usize, freq: f32, amp: f32) -> AudioChunk {
let pcm = (0..n)
.map(|i| {
let t = i as f32 / rate as f32;
(amp * (2.0 * std::f32::consts::PI * freq * t).sin()) as i16
})
.collect();
AudioChunk::new(pcm, rate)
}
fn parse_wav(bytes: &[u8]) -> (WavSpec, Vec<i16>) {
let mut reader = WavReader::new(Cursor::new(bytes)).expect("valid WAV header");
let spec = reader.spec();
let samples: Vec<i16> = reader
.samples::<i16>()
.map(|s| s.expect("readable sample"))
.collect();
(spec, samples)
}
#[test]
fn renders_valid_wav_header_at_recorder_rate() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &tone(8000, 800, 440.0, 8000.0));
rec.push_outbound_at(Duration::ZERO, &tone(8000, 800, 660.0, 8000.0));
let bytes = rec.render_wav().unwrap();
assert!(!bytes.is_empty());
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
let (spec, samples) = parse_wav(&bytes);
assert_eq!(spec.channels, 1, "mono");
assert_eq!(spec.sample_rate, 8000);
assert_eq!(spec.bits_per_sample, 16);
assert_eq!(spec.sample_format, SampleFormat::Int);
assert_eq!(samples.len(), 800, "same-rate legs mix 1:1");
}
#[test]
fn mixes_two_legs_by_summation() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1000i16; 100], 8000));
rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![2000i16; 100], 8000));
let (_, samples) = parse_wav(&rec.render_wav().unwrap());
assert_eq!(samples.len(), 100);
assert!(samples.iter().all(|&s| s == 3000), "1000 + 2000 = 3000");
}
#[test]
fn mix_clamps_instead_of_wrapping() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![30000i16; 10], 8000));
rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![30000i16; 10], 8000));
let (_, samples) = parse_wav(&rec.render_wav().unwrap());
assert!(samples.iter().all(|&s| s == i16::MAX));
}
#[test]
fn longer_leg_determines_length_shorter_zero_padded() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![500i16; 200], 8000));
rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![500i16; 50], 8000));
let (_, samples) = parse_wav(&rec.render_wav().unwrap());
assert_eq!(samples.len(), 200, "length = longer leg");
assert_eq!(samples[0], 1000);
assert_eq!(samples[199], 500);
}
#[test]
fn resamples_legs_at_different_rates_to_common_rate() {
let mut rec = AudioRecorder::new(16000);
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();
let (spec, samples) = parse_wav(&bytes);
assert_eq!(spec.sample_rate, 16000);
assert_eq!(spec.channels, 1);
assert!(
(3000..=3700).contains(&samples.len()),
"expected ~3200 samples @16k, got {}",
samples.len()
);
}
#[test]
fn empty_recorder_renders_valid_empty_wav() {
let rec = AudioRecorder::new(8000);
let bytes = rec.render_wav().unwrap();
let (spec, samples) = parse_wav(&bytes);
assert_eq!(spec.sample_rate, 8000);
assert_eq!(samples.len(), 0);
}
#[test]
fn empty_chunks_are_ignored() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![], 8000));
rec.push_outbound_at(Duration::ZERO, &AudioChunk::new(vec![], 8000));
let (_, samples) = parse_wav(&rec.render_wav().unwrap());
assert_eq!(samples.len(), 0);
}
#[test]
fn sequential_turns_do_not_overlap_on_the_timeline() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1000i16; 100], 8000));
rec.push_outbound_at(
Duration::from_millis(500),
&AudioChunk::new(vec![2000i16; 100], 8000),
);
let (_, samples) = parse_wav(&rec.render_wav().unwrap());
assert_eq!(samples.len(), 4100, "timeline spans to the bot turn");
assert_eq!(samples[50], 1000, "user turn, bot silent");
assert_eq!(samples[4050], 2000, "bot turn, user silent");
assert!(
!samples.contains(&3000),
"sequential turns must not overlap (no summed 1000+2000)"
);
}
#[test]
fn duration_seconds_spans_to_last_audio() {
let mut rec = AudioRecorder::new(8000);
rec.push_inbound_at(Duration::ZERO, &AudioChunk::new(vec![1i16; 8000], 8000));
rec.push_outbound_at(
Duration::from_secs(2),
&AudioChunk::new(vec![1i16; 4000], 8000),
);
assert!(
(rec.duration_seconds() - 2.5).abs() < 1e-6,
"duration spans to the later leg's last audio, got {}",
rec.duration_seconds()
);
}
}