252 lines
9.4 KiB
Rust
252 lines
9.4 KiB
Rust
//! Route-to-processing policy for P1 iOS.
|
|
//!
|
|
//! Maps the current [`AudioRoute`] to the recommended
|
|
//! [`AudioProcessingConfig`] for that route. The policy enforces
|
|
//! INV_009 (never enable platform AEC and Rust AEC simultaneously)
|
|
//! and INV_010 (never enable VoiceProcessingIO and Sonora AEC3
|
|
//! simultaneously).
|
|
//!
|
|
//! The returned config is a *recommendation*; the engine may override
|
|
//! individual fields (e.g. to keep the user's explicit VAD backend
|
|
//! choice) but must not violate the hard invariants.
|
|
|
|
use crate::audio_processing::{
|
|
AudioBackend, AudioProcessingConfig, AudioRoute, EffectOwner, IosVoiceProcessingMode,
|
|
VadBackend,
|
|
};
|
|
|
|
/// Compute the recommended [`AudioProcessingConfig`] for a given
|
|
/// iOS audio route. The returned config always satisfies the P1
|
|
/// hard invariants for iOS.
|
|
///
|
|
/// * Speaker / Earpiece → platform VPIO (AEC/NS/AGC owned by platform).
|
|
/// * Wired headset → noop AEC, conservative NS/AGC optional.
|
|
/// * Bluetooth HFP → route-managed (app-side AEC off, NS/AGC conservative).
|
|
/// * Bluetooth A2DP → invalid for duplex; transmit blocked at selector level.
|
|
/// * Unknown → safe fallback (AEC off until classified).
|
|
pub fn ios_route_policy(route: AudioRoute) -> AudioProcessingConfig {
|
|
match route {
|
|
AudioRoute::Speaker | AudioRoute::Earpiece => AudioProcessingConfig {
|
|
route,
|
|
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
|
|
processing_backend: AudioBackend::PlatformVoiceProcessing,
|
|
vad_backend: VadBackend::SileroOnnx,
|
|
aec: EffectOwner::Platform,
|
|
// VPIO owns NS and AGC on the shipping default path (IOSP_002/003).
|
|
ns: EffectOwner::Platform,
|
|
agc: EffectOwner::Platform,
|
|
hpf_enabled: true,
|
|
limiter_enabled: true,
|
|
..AudioProcessingConfig::default()
|
|
},
|
|
AudioRoute::WiredHeadset => AudioProcessingConfig {
|
|
route,
|
|
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
|
|
processing_backend: AudioBackend::Noop,
|
|
vad_backend: VadBackend::SileroOnnx,
|
|
// No AEC needed for wired headset (no acoustic echo path).
|
|
aec: EffectOwner::Off,
|
|
// Conservative NS/AGC: optional, not forced.
|
|
ns: EffectOwner::Conservative,
|
|
agc: EffectOwner::Conservative,
|
|
hpf_enabled: true,
|
|
limiter_enabled: true,
|
|
..AudioProcessingConfig::default()
|
|
},
|
|
AudioRoute::BluetoothHfp => AudioProcessingConfig {
|
|
route,
|
|
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
|
|
processing_backend: AudioBackend::PlatformVoiceProcessing,
|
|
vad_backend: VadBackend::SileroOnnx,
|
|
// BT HFP manages its own AEC in the headset firmware.
|
|
aec: EffectOwner::Off,
|
|
ns: EffectOwner::Conservative,
|
|
agc: EffectOwner::Conservative,
|
|
hpf_enabled: true,
|
|
limiter_enabled: true,
|
|
..AudioProcessingConfig::default()
|
|
},
|
|
AudioRoute::BluetoothA2dp => {
|
|
// A2DP is output-only; duplex voice is invalid on this route.
|
|
// Return a config that disables all processing and VAD.
|
|
// The transmit selector will block transmit via the route check.
|
|
AudioProcessingConfig {
|
|
route,
|
|
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
|
|
processing_backend: AudioBackend::Noop,
|
|
vad_backend: VadBackend::Disabled,
|
|
aec: EffectOwner::Off,
|
|
ns: EffectOwner::Off,
|
|
agc: EffectOwner::Off,
|
|
hpf_enabled: false,
|
|
limiter_enabled: false,
|
|
..AudioProcessingConfig::default()
|
|
}
|
|
}
|
|
AudioRoute::Unknown => AudioProcessingConfig {
|
|
route,
|
|
ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
|
|
processing_backend: AudioBackend::Noop,
|
|
vad_backend: VadBackend::SileroOnnx,
|
|
// Safe fallback: AEC off until route is classified.
|
|
aec: EffectOwner::Off,
|
|
ns: EffectOwner::Off,
|
|
agc: EffectOwner::Off,
|
|
hpf_enabled: true,
|
|
limiter_enabled: true,
|
|
..AudioProcessingConfig::default()
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Apply a route change to an existing config, preserving user-chosen
|
|
/// VAD backend, timing, and debug settings while updating the
|
|
/// route-dependent policy fields.
|
|
pub fn apply_route_change(
|
|
existing: &AudioProcessingConfig,
|
|
new_route: AudioRoute,
|
|
) -> AudioProcessingConfig {
|
|
let policy = ios_route_policy(new_route);
|
|
AudioProcessingConfig {
|
|
// Route-policy fields from the new route.
|
|
route: policy.route,
|
|
ios_mode: policy.ios_mode,
|
|
processing_backend: policy.processing_backend,
|
|
aec: policy.aec,
|
|
ns: policy.ns,
|
|
agc: policy.agc,
|
|
hpf_enabled: policy.hpf_enabled,
|
|
limiter_enabled: policy.limiter_enabled,
|
|
// Preserve user-chosen VAD backend and timing.
|
|
vad_backend: existing.vad_backend,
|
|
vad_hangover_ms: existing.vad_hangover_ms,
|
|
vad_pre_roll_ms: existing.vad_pre_roll_ms,
|
|
vad_min_tx_ms: existing.vad_min_tx_ms,
|
|
// Preserve debug settings.
|
|
debug_wav_dump_enabled: existing.debug_wav_dump_enabled,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn speaker_uses_platform_vpio() {
|
|
let cfg = ios_route_policy(AudioRoute::Speaker);
|
|
assert_eq!(
|
|
cfg.processing_backend,
|
|
AudioBackend::PlatformVoiceProcessing
|
|
);
|
|
assert_eq!(cfg.aec, EffectOwner::Platform);
|
|
// VPIO owns NS and AGC on the default path.
|
|
assert_eq!(cfg.ns, EffectOwner::Platform);
|
|
assert_eq!(cfg.agc, EffectOwner::Platform);
|
|
assert_eq!(
|
|
cfg.ios_mode,
|
|
IosVoiceProcessingMode::PlatformVoiceProcessing
|
|
);
|
|
assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
|
|
}
|
|
|
|
#[test]
|
|
fn earpiece_uses_platform_vpio() {
|
|
let cfg = ios_route_policy(AudioRoute::Earpiece);
|
|
assert_eq!(
|
|
cfg.processing_backend,
|
|
AudioBackend::PlatformVoiceProcessing
|
|
);
|
|
assert_eq!(cfg.aec, EffectOwner::Platform);
|
|
assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
|
|
}
|
|
|
|
#[test]
|
|
fn wired_headset_disables_aec() {
|
|
let cfg = ios_route_policy(AudioRoute::WiredHeadset);
|
|
assert_eq!(cfg.aec, EffectOwner::Off);
|
|
assert_eq!(cfg.processing_backend, AudioBackend::Noop);
|
|
assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
|
|
}
|
|
|
|
#[test]
|
|
fn bluetooth_hfp_disables_app_aec() {
|
|
let cfg = ios_route_policy(AudioRoute::BluetoothHfp);
|
|
assert_eq!(cfg.aec, EffectOwner::Off);
|
|
assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
|
|
}
|
|
|
|
#[test]
|
|
fn bluetooth_a2dp_disables_all_processing_and_vad() {
|
|
let cfg = ios_route_policy(AudioRoute::BluetoothA2dp);
|
|
assert_eq!(cfg.aec, EffectOwner::Off);
|
|
assert_eq!(cfg.vad_backend, VadBackend::Disabled);
|
|
assert_eq!(cfg.processing_backend, AudioBackend::Noop);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_route_safe_fallback_no_aec() {
|
|
let cfg = ios_route_policy(AudioRoute::Unknown);
|
|
assert_eq!(cfg.aec, EffectOwner::Off);
|
|
assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
|
|
}
|
|
|
|
#[test]
|
|
fn apply_route_change_preserves_vad_timing_and_debug() {
|
|
let existing = AudioProcessingConfig {
|
|
vad_backend: VadBackend::WebrtcVad,
|
|
vad_hangover_ms: 600,
|
|
vad_pre_roll_ms: 200,
|
|
vad_min_tx_ms: 300,
|
|
debug_wav_dump_enabled: true,
|
|
..AudioProcessingConfig::default()
|
|
};
|
|
let updated = apply_route_change(&existing, AudioRoute::WiredHeadset);
|
|
assert_eq!(updated.vad_backend, VadBackend::WebrtcVad);
|
|
assert_eq!(updated.vad_hangover_ms, 600);
|
|
assert_eq!(updated.vad_pre_roll_ms, 200);
|
|
assert_eq!(updated.vad_min_tx_ms, 300);
|
|
assert!(updated.debug_wav_dump_enabled);
|
|
assert_eq!(updated.route, AudioRoute::WiredHeadset);
|
|
assert_eq!(updated.aec, EffectOwner::Off);
|
|
}
|
|
|
|
#[test]
|
|
fn no_sonora_aec_in_platform_vpio_policy() {
|
|
// INV_009: AEC must never be Sonora in the VPIO path.
|
|
for route in [
|
|
AudioRoute::Speaker,
|
|
AudioRoute::Earpiece,
|
|
AudioRoute::WiredHeadset,
|
|
AudioRoute::BluetoothHfp,
|
|
AudioRoute::BluetoothA2dp,
|
|
AudioRoute::Unknown,
|
|
] {
|
|
let cfg = ios_route_policy(route);
|
|
assert_ne!(
|
|
cfg.processing_backend,
|
|
AudioBackend::Sonora,
|
|
"route {:?} must not use Sonora backend in platform policy",
|
|
route
|
|
);
|
|
assert_ne!(
|
|
cfg.aec,
|
|
EffectOwner::Sonora,
|
|
"route {:?}: AEC must not be Sonora in VPIO path (INV_009)",
|
|
route
|
|
);
|
|
assert_ne!(
|
|
cfg.ns,
|
|
EffectOwner::Sonora,
|
|
"route {:?}: NS must not be Sonora in VPIO path (IOSP_003)",
|
|
route
|
|
);
|
|
assert_ne!(
|
|
cfg.agc,
|
|
EffectOwner::Sonora,
|
|
"route {:?}: AGC must not be Sonora in VPIO path (IOSP_003)",
|
|
route
|
|
);
|
|
}
|
|
}
|
|
}
|