//! Audio input and output, and the voice detector that splits them into turns. //! //! The rule that governs this crate: **the audio callback never blocks**. cpal //! runs it on a real-time thread and any wait there is heard as a click, so it //! only copies samples to a channel (input) or drains an already filled ring //! (output). All the real work (resampling, VAD, HTTP) happens on regular //! threads on the other side. pub mod capture; pub mod playback; pub mod vad; /// Re-exported so the binary can list devices without declaring cpal /// again and risking resolving another version. pub use cpal; pub use capture::{Capture, CaptureBlock, InputFormat}; pub use playback::{detached_handle, Playback, PlaybackHandle}; pub use vad::{Gate, Segmenter, Utterance, VoiceEvent}; /// Rate Canary works at. Capture is opened directly at this rate when the /// device allows it, which takes resampling off the path. pub const ASR_SAMPLE_RATE: u32 = 16_000; /// Rate qwentts synthesizes at. pub const TTS_SAMPLE_RATE: u32 = 24_000; /// Human-readable device name. /// /// `DeviceTrait::name` is deprecated in cpal 0.17 in favour of `description`, /// which returns a whole record; only the name matters here, and having a /// single place to extract it avoids repeating the unwrapping. pub fn describe(device: &impl cpal::traits::DeviceTrait) -> String { device .description() .map(|d| d.name().to_string()) .unwrap_or_else(|_| "desconocido".into()) } /// RMS level of a block, the measure the VAD decides on. pub fn rms(samples: &[f32]) -> f32 { if samples.is_empty() { return 0.0; } let sum: f32 = samples.iter().map(|v| v * v).sum(); (sum / samples.len() as f32).sqrt() } /// Mixes down to mono and resamples linearly to `target`. /// /// Linear interpolation is enough: the device already delivers a /// band-limited signal, and a decent resampler would cost more than the /// decoding it feeds. pub fn to_mono_at(samples: &[f32], format: InputFormat, target: u32) -> Vec { let mono: Vec = if format.channels > 1 { samples .chunks(format.channels) .map(|frame| frame.iter().sum::() / format.channels as f32) .collect() } else { samples.to_vec() }; if format.sample_rate == target as usize { return mono; } let ratio = target as f64 / format.sample_rate as f64; let out_len = (mono.len() as f64 * ratio) as usize; (0..out_len) .map(|i| { let pos = i as f64 / ratio; let idx = pos as usize; let frac = (pos - idx as f64) as f32; let a = mono.get(idx).copied().unwrap_or(0.0); let b = mono.get(idx + 1).copied().unwrap_or(a); a + (b - a) * frac }) .collect() } /// Converts s16le to f32 in [-1, 1]. It is the format the TTS delivers. pub fn s16le_to_f32(bytes: &[u8], out: &mut Vec) { for pair in bytes.chunks_exact(2) { let sample = i16::from_le_bytes([pair[0], pair[1]]); out.push(sample as f32 / 32768.0); } } #[cfg(test)] mod tests { use super::*; #[test] fn rms_of_a_constant_signal_is_its_amplitude() { assert!((rms(&[0.5; 100]) - 0.5).abs() < 1e-6); assert_eq!(rms(&[]), 0.0); } #[test] fn stereo_is_mixed_to_mono_by_averaging() { let format = InputFormat { sample_rate: 16_000, channels: 2, }; let out = to_mono_at(&[1.0, 0.0, 0.5, 0.5], format, 16_000); assert_eq!(out, vec![0.5, 0.5]); } #[test] fn resampling_adjusts_the_duration() { let format = InputFormat { sample_rate: 48_000, channels: 1, }; let out = to_mono_at(&vec![0.0; 4800], format, 16_000); assert_eq!(out.len(), 1600, "48 kHz -> 16 kHz must divide by three"); } #[test] fn resampling_at_the_same_rate_changes_nothing() { let format = InputFormat { sample_rate: 16_000, channels: 1, }; let input = vec![0.1, -0.2, 0.3]; assert_eq!(to_mono_at(&input, format, 16_000), input); } #[test] fn s16le_covers_the_full_range() { let mut out = Vec::new(); s16le_to_f32(&[0x00, 0x00, 0xff, 0x7f, 0x00, 0x80], &mut out); assert_eq!(out[0], 0.0); assert!((out[1] - 1.0).abs() < 1e-4); assert!((out[2] + 1.0).abs() < 1e-6); } #[test] fn stray_byte_does_not_produce_a_partial_sample() { let mut out = Vec::new(); s16le_to_f32(&[0x00, 0x00, 0x11], &mut out); assert_eq!( out.len(), 1, "the odd byte is ignored instead of corrupting the sample" ); } }