feat: stabilize voice activity and audio routing

This commit is contained in:
Edison Jwa
2026-05-25 01:19:09 +09:00
parent eb9014cd81
commit 5515ff6643
34 changed files with 3054 additions and 1751 deletions
+59 -11
View File
@@ -92,9 +92,6 @@ impl AudioBackend {
pub enum VadBackend {
/// Silero ONNX VAD. P1 schema default when model/runtime exist.
SileroOnnx,
/// TEN VAD backend. Native TEN runtime is optional; unavailable
/// builds fall back to the realtime-safe WebRTC detector.
TenVad,
/// WebRTC-style fallback VAD.
WebrtcVad,
/// Debug-only energy VAD.
@@ -108,7 +105,6 @@ impl VadBackend {
pub fn as_str(self) -> &'static str {
match self {
Self::SileroOnnx => "silero_vad_onnx",
Self::TenVad => "ten_vad",
Self::WebrtcVad => "webrtc_vad",
Self::EnergyDebug => "energy_debug",
Self::Disabled => "disabled",
@@ -168,7 +164,7 @@ impl Default for AudioProcessingConfig {
route: AudioRoute::Speaker,
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
processing_backend: AudioBackend::PlatformVoiceProcessing,
vad_backend: VadBackend::TenVad,
vad_backend: VadBackend::SileroOnnx,
aec: EffectOwner::Platform,
// iOS VPIO owns NS/AGC on the default shipping path. Software
// effects are opt-in through the experimental raw route only.
@@ -246,7 +242,7 @@ mod tests {
assert_eq!(config.aec, EffectOwner::Platform);
assert_eq!(config.ns, EffectOwner::Platform);
assert_eq!(config.agc, EffectOwner::Platform);
assert_eq!(config.vad_backend, VadBackend::TenVad);
assert_eq!(config.vad_backend, VadBackend::SileroOnnx);
}
#[test]
@@ -259,11 +255,6 @@ mod tests {
assert!(config.validate_for_ios().is_err());
}
#[test]
fn ten_vad_has_stable_debug_string() {
assert_eq!(VadBackend::TenVad.as_str(), "ten_vad");
}
#[test]
fn raw_processing_allows_full_webrtc_apm_chain() {
let config = AudioProcessingConfig {
@@ -342,6 +333,16 @@ pub struct AudioProcessingStats {
pub callback_xruns: u64,
/// Clipped sample count.
pub clipped_samples: u64,
/// Number of effectively silent processed capture frames.
pub zero_frames: u64,
/// Number of processed capture frames.
pub capture_frames: u64,
/// Number of input callbacks carrying 10 ms of audio.
pub callbacks_10ms: u64,
/// Number of input callbacks carrying 20 ms of audio.
pub callbacks_20ms: u64,
/// Number of input callbacks carrying any other size.
pub callbacks_other: u64,
/// Sonora enabled.
pub sonora_enabled: bool,
/// Platform voice processing enabled.
@@ -361,6 +362,11 @@ pub struct SharedAudioProcessingStats {
output_underruns: AtomicU64,
callback_xruns: AtomicU64,
clipped_samples: AtomicU64,
zero_frames: AtomicU64,
capture_frames: AtomicU64,
callbacks_10ms: AtomicU64,
callbacks_20ms: AtomicU64,
callbacks_other: AtomicU64,
actual_sample_rate_hz: AtomicU32,
actual_io_buffer_frames: AtomicU32,
}
@@ -379,6 +385,11 @@ impl Default for SharedAudioProcessingStats {
output_underruns: AtomicU64::new(0),
callback_xruns: AtomicU64::new(0),
clipped_samples: AtomicU64::new(0),
zero_frames: AtomicU64::new(0),
capture_frames: AtomicU64::new(0),
callbacks_10ms: AtomicU64::new(0),
callbacks_20ms: AtomicU64::new(0),
callbacks_other: AtomicU64::new(0),
actual_sample_rate_hz: AtomicU32::new(crate::frame::SAMPLE_RATE_HZ),
actual_io_buffer_frames: AtomicU32::new(crate::frame::FRAME_20MS_SAMPLES as u32),
}
@@ -412,6 +423,12 @@ impl SharedAudioProcessingStats {
.store(io_buffer_frames, Ordering::Relaxed);
}
/// Record the actual device sample rate.
pub fn set_actual_sample_rate_hz(&self, sample_rate_hz: u32) {
self.actual_sample_rate_hz
.store(sample_rate_hz, Ordering::Relaxed);
}
/// Increment output underrun count.
pub fn increment_output_underrun(&self) {
self.output_underruns.fetch_add(1, Ordering::Relaxed);
@@ -427,6 +444,32 @@ impl SharedAudioProcessingStats {
self.clipped_samples.fetch_add(count, Ordering::Relaxed);
}
/// Record one processed capture frame and whether it was effectively silent.
pub fn record_capture_frame(&self, zero_frame: bool) {
self.capture_frames.fetch_add(1, Ordering::Relaxed);
if zero_frame {
self.zero_frames.fetch_add(1, Ordering::Relaxed);
}
}
/// Bucket callback delivery sizes to diagnose timing jitter and packetization.
pub fn record_callback_frames(&self, frames: u64) {
let sample_rate_hz = self.actual_sample_rate_hz.load(Ordering::Relaxed).max(1);
let frames_10ms = (sample_rate_hz / 100) as u64;
let frames_20ms = (sample_rate_hz / 50) as u64;
match frames {
value if value == frames_10ms => {
self.callbacks_10ms.fetch_add(1, Ordering::Relaxed);
}
value if value == frames_20ms => {
self.callbacks_20ms.fetch_add(1, Ordering::Relaxed);
}
_ => {
self.callbacks_other.fetch_add(1, Ordering::Relaxed);
}
}
}
/// Store whether the selected VAD backend is currently using a fallback.
pub fn set_vad_fallback_active(&self, active: bool) {
self.vad_fallback_active.store(active, Ordering::Relaxed);
@@ -452,6 +495,11 @@ impl SharedAudioProcessingStats {
output_underruns: self.output_underruns.load(Ordering::Relaxed),
callback_xruns: self.callback_xruns.load(Ordering::Relaxed),
clipped_samples: self.clipped_samples.load(Ordering::Relaxed),
zero_frames: self.zero_frames.load(Ordering::Relaxed),
capture_frames: self.capture_frames.load(Ordering::Relaxed),
callbacks_10ms: self.callbacks_10ms.load(Ordering::Relaxed),
callbacks_20ms: self.callbacks_20ms.load(Ordering::Relaxed),
callbacks_other: self.callbacks_other.load(Ordering::Relaxed),
sonora_enabled: config.processing_backend == AudioBackend::Sonora,
platform_voice_processing_enabled: config.processing_backend
== AudioBackend::PlatformVoiceProcessing,