# iOS Audio Session Lifecycle — Integration Test Plan ## Overview Chanora uses Apple's **VoiceProcessingIO** (VPIO) AudioUnit on iOS/macOS for voice capture and playback. The audio session is configured in Swift (`AppDelegate`) with `AVAudioSession.Category.playAndRecord` and `AVAudioSession.Mode.default`. This document defines the integration tests needed to verify correct behavior across session transitions, interruptions, and route changes. ## Architecture Summary | Layer | Responsibility | |-------|---------------| | `AppDelegate.swift` | Sets `AVAudioSession` category/mode, handles route-change and interruption notifications | | `IosVoiceUnit` (Rust) | Opens VPIO AudioUnit, pins 48 kHz Int16 mono, installs render + input callbacks | | `AudioEngine::ios_restart_voice_unit` | Restarts the VPIO unit after a route change | | `AudioEngine::ios_pause_voice_unit` / `ios_resume_voice_unit` | Suspends audio during interruptions | | `route_policy.rs` | Maps `AudioRoute` to recommended `AudioProcessingConfig` (AEC/NS/AGC ownership) | ## Test Scenarios ### 1. Session Activation and Deactivation | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | S-01 | Cold start session activation | Launch app → connect to server → join voice channel | VPIO unit starts, mic input flows, audio plays through default route | | S-02 | Session deactivation on disconnect | While in voice → disconnect from server | VPIO unit stops, `AudioEngine::stop()` called, session category restored | | S-03 | Session mode verification | After activation, query `AVAudioSession.mode` | Must be `.default` (not `.voiceChat`) to avoid ducking | ### 2. Audio Interruption Handling | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | I-01 | Phone call interruption | While in voice → receive incoming call | `AVAudioSession.interruptionNotification` fires with `.began`; VPIO paused via `ios_pause_voice_unit` | | I-02 | Phone call ends | After I-01 → call ends | Interruption notification fires with `.ended`; VPIO resumed via `ios_resume_voice_unit` if session was active | | I-03 | Siri activation | While in voice → invoke Siri | Interruption `.began` → pause; Siri dismisses → `.ended` → resume | | I-04 | Alarm / timer | While in voice → alarm fires | Audio ducks (not interrupted); voice continues at reduced volume | | I-05 | Third-party audio app | While in voice → open Spotify and play music | Other audio ducks; Chanora voice remains active | | I-06 | Interruption during route change | While switching routes → phone call arrives | Both events handled; no crash, VPIO restarts cleanly after both resolve | ### 3. Route Change Handling | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | R-01 | Plug in wired headset | While on speaker → connect Lightning/USB-C headphones | Route changes to `.wiredHeadset`; `ios_restart_voice_unit` called; AEC disabled (no acoustic echo path); `route_policy.rs` returns `WiredHeadset` config | | R-02 | Unplug wired headset | While on wired headset → disconnect | Route changes to `.speaker`; VPIO restarts; AEC re-enabled via platform VPIO | | R-03 | Connect Bluetooth HFP | While on speaker → connect BT headset in HFP mode | Route changes to `.bluetoothHfp`; VPIO restarts; AEC off (headset firmware handles it) | | R-04 | Disconnect Bluetooth HFP | While on BT HFP → turn off headset | Route falls back to speaker; VPIO restarts with platform AEC | | R-05 | Switch to Bluetooth A2DP | While on speaker → connect A2DP-only device | Route changes to `.bluetoothA2dp`; transmit blocked (A2DP is output-only); playback continues | | R-06 | Toggle speaker/earpiece | Use in-app audio output picker | `overrideOutputAudioPort` called; VPIO restarts; audio actually moves (not just metadata) | | R-07 | AirPods connect/disconnect | While on speaker → AirPods connect → AirPods case closed | Route transitions handled; VPIO restarts on each change | | R-08 | Rapid route changes | Connect/disconnect headset 5 times in 10 seconds | No crash, no audio leak, VPIO restarts cleanly each time | | R-09 | Route change during mute | While muted → route changes | VPIO restarts; mute state preserved; no audio leak | ### 4. Audio Ducking Configuration | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | D-01 | Ducking disabled on startup | App launches and joins voice | `kAUVoiceIOProperty_OtherAudioDuckingConfiguration` set with `mEnableAdvancedDucking=0`, `mDuckingLevel=Min` | | D-02 | Music playback while in voice | Play music via Music app → join voice channel | Music volume is NOT heavily attenuated; voice and music coexist | | D-03 | Game audio while in voice | Play a game with audio → join voice | Game audio is NOT heavily attenuated | ### 5. VPIO Stream Format Verification | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | F-01 | Output bus format | After VPIO start, inspect bus 0 stream format | 48 kHz, Int16, mono, signed integer, packed | | F-02 | Input bus format | After VPIO start, inspect bus 1 stream format | 48 kHz, Int16, mono, signed integer, packed | | F-03 | Callback frame count | Log `num_frames` in render callback | iOS: 480 frames (10 ms); macOS: 512 frames (10.67 ms) | | F-04 | Audio quality roundtrip | Speak into mic → loopback to speaker | No distortion, no resampling artifacts, correct latency | ### 6. Route Policy Correctness | ID | Scenario | Steps | Expected Behavior | |----|----------|-------|-------------------| | P-01 | Speaker route policy | Route = Speaker | `ios_route_policy` returns: AEC=Platform, NS=Platform, AGC=Platform, backend=PlatformVoiceProcessing | | P-02 | Wired headset policy | Route = WiredHeadset | AEC=Off, NS=Conservative, AGC=Conservative, backend=Noop | | P-03 | BT HFP policy | Route = BluetoothHfp | AEC=Off, NS=Conservative, AGC=Conservative, backend=PlatformVoiceProcessing | | P-04 | A2DP policy | Route = BluetoothA2dp | All processing off, VAD disabled, transmit blocked | | P-05 | INV-009 invariant | Any route | Sonora AEC never enabled simultaneously with platform VPIO | | P-06 | User VAD preserved on route change | Set VAD=WebRTC → change route | New config keeps VAD=WebRTC and hangover timing | ## Device Requirements ### Required Devices | Device | OS | Reason | |--------|-----|--------| | iPhone (Lightning or USB-C) | iOS 16+ | Primary target; VPIO, route changes, interruptions | | iPhone with Face ID | iOS 17+ | `OtherAudioDuckingConfiguration` property availability | | AirPods (any generation) | — | Bluetooth A2DP/HFP route testing | | Bluetooth HFP headset | — | Non-Apple BT headset route testing | | Lightning/USB-C wired headset | — | Wired route testing | | iPad (optional) | iPadOS 16+ | Verify identical VPIO behavior | ### Simulator Limitations - VPIO render callback cadence differs from real hardware - Route changes are not testable on simulator - Interruption notifications are unreliable on simulator - **Recommendation**: All integration tests must run on physical devices ## Automation Approach ### Phase 1: Manual Test Matrix Execute scenarios S-01 through P-06 on physical devices using this checklist. Record pass/fail and any audio artifacts observed. ### Phase 2: XCUITest + Rust Harness ``` ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ XCUITest │────▶│ FRB bridge │────▶│ AudioEngine │ │ (Swift) │ │ test helper │ │ (Rust) │ └─────────────┘ └──────────────┘ └───────────────┘ ``` 1. **FRB test helper**: Add a `#[flutter_rust_bridge::frb]` test function that starts `AudioEngine`, runs for N seconds, and returns stats (frames_sent, frames_received, xruns, output_underruns). 2. **XCUITest**: Launches the app, connects to a test server, triggers the FRB helper, then uses `XCUIDevice` APIs to simulate: - Route changes via `XCUIDevice.shared().press(.volumeUp)` + BT pairing - Interruptions via `XCUISiriService` (Siri) or call simulation 3. **Assertions**: Verify stats counters are within expected ranges (no xruns, no output underruns, frames_sent > 0). ### Phase 3: Continuous Monitoring Add a telemetry event for each VPIO restart, pause, resume, and interruption. Track: - Restart count per session (should be ≤ number of route changes) - Pause-to-resume latency (should be < 500 ms) - Xrun count per session (should be 0 under normal conditions) ## References - `crates/chanora_audio/src/ios_voice_unit.rs` — VPIO AudioUnit setup - `crates/chanora_audio/src/engine/lifecycle.rs` — Engine start/stop/restart - `crates/chanora_audio/src/route_policy.rs` — Route-to-config policy - Apple: [Audio Session Programming Guide](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/) - Apple: [Audio Unit Hosting Guide for iOS](https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/)