//! Canonical P1 voice frame helpers. //! //! The network contract remains 48 kHz mono, 20 ms Opus frames. P1 //! processing works internally on 10 ms f32 frames so VAD and future //! processors can share a stable frame size without changing the //! transport layer. /// P1 sample rate in Hz. pub const SAMPLE_RATE_HZ: u32 = 48_000; /// Network frame duration in milliseconds. pub const NETWORK_FRAME_MS: u32 = 20; /// Processing frame duration in milliseconds. pub const PROCESSING_FRAME_MS: u32 = 10; /// Samples in one 10 ms mono frame at 48 kHz. pub const FRAME_10MS_SAMPLES: usize = 480; /// Samples in one 20 ms mono frame at 48 kHz. pub const FRAME_20MS_SAMPLES: usize = 960; /// 10 ms, 48 kHz, mono f32 processing frame. #[derive(Debug, Clone, PartialEq)] pub struct AudioFrame10ms { /// Samples normalized to `[-1.0, 1.0]`. pub samples: [f32; FRAME_10MS_SAMPLES], } /// 20 ms, 48 kHz, mono f32 network-frame-sized buffer. #[derive(Debug, Clone, PartialEq)] pub struct AudioFrame20ms { /// Samples normalized to `[-1.0, 1.0]`. pub samples: [f32; FRAME_20MS_SAMPLES], } impl AudioFrame20ms { /// Convert one 20 ms frame into two 10 ms processing frames. pub fn split(&self) -> (AudioFrame10ms, AudioFrame10ms) { let mut first = [0.0; FRAME_10MS_SAMPLES]; let mut second = [0.0; FRAME_10MS_SAMPLES]; first.copy_from_slice(&self.samples[..FRAME_10MS_SAMPLES]); second.copy_from_slice(&self.samples[FRAME_10MS_SAMPLES..]); ( AudioFrame10ms { samples: first }, AudioFrame10ms { samples: second }, ) } } impl AudioFrame10ms { /// Merge two 10 ms processing frames back into the 20 ms network /// cadence used by the existing Opus path. pub fn merge(first: &Self, second: &Self) -> AudioFrame20ms { let mut samples = [0.0; FRAME_20MS_SAMPLES]; samples[..FRAME_10MS_SAMPLES].copy_from_slice(&first.samples); samples[FRAME_10MS_SAMPLES..].copy_from_slice(&second.samples); AudioFrame20ms { samples } } /// Compute RMS dBFS for diagnostics and fallback VAD. pub fn dbfs(&self) -> f32 { dbfs(&self.samples) } } /// Convert i16 PCM to normalized f32 PCM. pub fn i16_to_f32(sample: i16) -> f32 { sample as f32 / i16::MAX as f32 } /// Convert normalized f32 PCM to saturated i16 PCM. pub fn f32_to_i16(sample: f32) -> i16 { (sample.clamp(-1.0, 1.0) * i16::MAX as f32) as i16 } /// RMS dBFS for a normalized f32 slice. Silence returns `-120.0`. pub fn dbfs(samples: &[f32]) -> f32 { if samples.is_empty() { return -120.0; } let sum = samples.iter().map(|s| s * s).sum::(); let rms = (sum / samples.len() as f32).sqrt(); if rms <= 0.000_001 { -120.0 } else { 20.0 * rms.log10() } } #[cfg(test)] mod tests { use super::*; #[test] fn split_merge_preserves_samples() { let mut samples = [0.0; FRAME_20MS_SAMPLES]; for (i, s) in samples.iter_mut().enumerate() { *s = i as f32 / FRAME_20MS_SAMPLES as f32; } let original = AudioFrame20ms { samples }; let (a, b) = original.split(); assert_eq!(AudioFrame10ms::merge(&a, &b), original); } }