Design documents for event replay, audio processing test, audio loopback test, and protocol probe tools. iOS audio session test plan with 28 scenarios and 3-phase automation approach.
9.1 KiB
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) │
└─────────────┘ └──────────────┘ └───────────────┘
-
FRB test helper: Add a
#[flutter_rust_bridge::frb]test function that startsAudioEngine, runs for N seconds, and returns stats (frames_sent, frames_received, xruns, output_underruns). -
XCUITest: Launches the app, connects to a test server, triggers the FRB helper, then uses
XCUIDeviceAPIs to simulate:- Route changes via
XCUIDevice.shared().press(.volumeUp)+ BT pairing - Interruptions via
XCUISiriService(Siri) or call simulation
- Route changes via
-
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 setupcrates/chanora_audio/src/engine/lifecycle.rs— Engine start/stop/restartcrates/chanora_audio/src/route_policy.rs— Route-to-config policy- Apple: Audio Session Programming Guide
- Apple: Audio Unit Hosting Guide for iOS