feat(ios,p0): iOS P0 platform, audio fixes, channel UX

This commit is contained in:
Edison Jwa
2026-05-17 22:00:00 +09:00
parent a1fefc8ab6
commit 7a59f5b9a1
38 changed files with 1705 additions and 674 deletions
+83 -6
View File
@@ -3,7 +3,7 @@
**Document type:** SDD / Software Detailed Design
**Process alignment:** ASPICE SWE.3 Software Detailed Design and Unit Construction
**Version:** 0.9.2
**Version:** 0.9.7
**Status:** Baseline Candidate
**Language:** English
**Product:** Chanora
@@ -1086,7 +1086,7 @@ The registry supports consistency between architecture, detailed design, impleme
- Source SAD: SAD-075
- Verification method: Unit Test
**SDD-090**: `PttSanitizer` shall implement `tracing_subscriber::Layer` and decorate the existing `RedactingLogLayer`. Records whose field names match the SDD-077 banned list shall be dropped before `on_event` reaches the wrapped sink. The implementation shall be allocation-free on the success path (the typical "no banned field" case).
**SDD-090**: `PttSanitizer` shall implement `tracing_subscriber::Layer` and decorate the existing `RedactingLogLayer`. Records whose field names match any of the following banned field names shall be dropped before `on_event` reaches the wrapped sink: `key_code`, `scan_code`, `virtual_key`, `vk`, `keysym`, `keysym_string`, `key_sequence` (7 fields required by SAD-077) and `key_press_history`, `key_timing` (2 defensive additions for future PTT diagnostic fields that might carry raw key data). The implementation shall be allocation-free on the success path (the typical "no banned field" case).
- Status: Draft
- Type: Software Detailed Design Item
@@ -1140,7 +1140,7 @@ The registry supports consistency between architecture, detailed design, impleme
- Source SAD: SAD-083
- Verification method: Unit Test, UI Review
**SDD-096**: `chanora_audio::release_tail::ReleaseTailTimer` shall be a tokio-task-owning struct exposing `arm(&self, gate: AudioTransmitGate, tail_ms: u32)`, `key_down(&self)`, `key_up(&self)`, and `cancel(&self)`. Internally the struct shall hold a `tokio::sync::watch::Sender<bool>` plus a `JoinHandle<()>` for the pending close. `key_down` shall abort any pending close handle and shall set `transmit_active = true` via the gate; `key_up` shall abort the prior close handle and shall spawn a new task that sleeps for `tail_ms` milliseconds and then sets `transmit_active = false`. The struct shall use no `unsafe`, no mutex, and shall rely only on atomic refcounts on the watch handle. The configuration value `release_tail_ms` shall live in the identity store next to `transmit_mode`, with a default of 200 and validated range 0500 inclusive on every write.
**SDD-096**: `chanora_audio::release_tail::ReleaseTailTimer` shall be a tokio-task-owning struct exposing `arm(&self, gate: AudioTransmitGate, tail_ms: u32)`, `key_down(&self)`, `key_up(&self)`, and `cancel(&self)`. Internally the struct shall hold a `tokio::sync::watch::Sender<bool>` plus a `JoinHandle<()>` for the pending close. `key_down` shall cancel any pending close by aborting the `JoinHandle` (`cancel_pending()`) and shall set `transmit_active = true` via the gate; `key_up` shall abort the prior close handle via `JoinHandle::abort()` and shall spawn a new task that sleeps for `tail_ms` milliseconds and then sets `transmit_active = false`. Cancellation is performed by aborting the pending `JoinHandle`; the watch channel enables cooperative early exit — the spawned task races `cancel_rx.changed()` against the tail sleep so that it can exit without writing to the gate when the sender is replaced or dropped. The struct shall use no `unsafe`. No `std::sync::Mutex` — interior mutability shall use `std::sync::RwLock` for brief edge-transition writes (`arm`, `key_down`, `key_up`, `cancel`); concurrent reads are uncontended. The watch handle shall be shared via atomic refcounts. The configuration value `release_tail_ms` shall live in the identity store next to `transmit_mode`, with a default of 200 and validated range 0500 inclusive on every write.
- Status: Draft
- Type: Software Detailed Design Item
@@ -1158,15 +1158,80 @@ The registry supports consistency between architecture, detailed design, impleme
- Source SAD: SAD-081, SAD-083
- Verification method: UI Review, Widget Test
**SDD-098**: `IOSAudioSessionConfig` shall be a Swift unit that configures `AVAudioSession` with category `.playAndRecord`, mode `.default`, and options `.defaultToSpeaker | .allowBluetoothHFP | .allowBluetoothA2DP`. `setCategory` shall be called from `didFinishLaunching` in the app delegate; `setActive` shall be called from the `UIApplication.didBecomeActiveNotification` observer. The actual session state shall be read back after both calls and logged for verification.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `IOSAudioSessionConfig`
- Source SAD: SAD-061, SAD-064
- Verification method: Platform Test, Release Inspection
**SDD-099**: `IOSPermissionRequester` shall call `AVAudioSession.sharedInstance().requestRecordPermission` approximately 1 second after app launch. The permission result shall be logged. A denied permission shall surface as an audio engine failure at `voice_join` time with a user-safe message; the UI shall not crash or hang.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `IOSPermissionRequester`
- Source SAD: SAD-061
- Verification method: Platform Test, Demo
**SDD-100**: Swift shall observe `AVAudioSession.routeChangeNotification`. On route change where `reason == .oldDeviceUnavailable` or `reason == .newDeviceAvailable`, Swift shall call the Rust bridge method `handleRouteChange()`. The Rust side shall invoke the `ios_voice_unit` restart path, uninitialising then reinitialising per `ios_voice_unit.rs` comments §5. Audio shall continue after rebind.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `IOSRouteChangeHandler`, `ios_voice_unit`
- Source SAD: SAD-061, SAD-064
- Verification method: Platform Test TC-9.3
**SDD-101**: Swift shall observe `AVAudioSession.interruptionNotification`. On `.began` the audio shall pause via `VPIO.stop()`. On `.ended` with `shouldResume == true` the audio shall resume via `VPIO.start()`. A phone call shall yield the audio session; the end of the call shall resume audio without user action. The interruption state shall be communicated to Flutter via `BridgeEvent`.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `IOSInterruptionRecovery`, `BridgeEvent`
- Source SAD: SAD-061
- Verification method: Platform Test TC-9.1, TC-9.2
> **Design note (shouldResume == false):** When iOS signals interruption end with `shouldResume == false`, the VPIO unit remains paused and the `BridgeEvent_InterruptionState` carries `shouldResume: false` to Flutter. The Flutter UI surfaces a snackbar indicating the interruption. The user must manually rejoin the voice channel or the app must implement a "resume audio" action in a future release. This is a known limitation for P0.
>
> **Design note (lifecycle interleaving):** The current iOS audio lifecycle handlers (route change, interruption began/ended) are individually serialized by the session mutex and `_ios_voice_unit` mutex, but do not track an explicit lifecycle state (e.g., `Running`, `Interrupted`, `Restarting`). This means that a route change arriving during an active interruption will restart the VPIO unit even though the app should remain paused. A future release should introduce an explicit iOS audio lifecycle state model that gates restart/resume decisions accordingly. For P0, the risk is mitigated by iOS serializing these notifications on the main thread and the low probability of interleaving in practice.
**SDD-102**: `Info.plist` shall contain `UIBackgroundModes = [audio]`. The VPIO unit shall continue running when the app backgrounds. iOS shall show the red microphone indicator in the status bar while background audio is active. `AVAudioSession` shall remain active. On iOS lock screen, audio shall continue if the network remains available.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `Info.plist`, `VPIOUnit`
- Source SAD: SAD-061
- Verification method: Platform Test TC-8
**SDD-103**: iOS P0 shall use `FocusedPttBackend` only. There shall be no `DesktopPttBackend` ladder on iOS. `PttCapabilityLevel` shall always be `L0Focused`. The UI shall render an on-screen PTT button. A capability badge shall explain the iOS limitation. Users shall bind a key through the in-app dialog only when the app is in the foreground.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `FocusedPttBackend`, `PttCapabilityLevel`, `FocusedPttBindingDialog`
- Source SAD: SAD-061, SAD-071
- Verification method: Platform Test TC-3, TC-12
**SDD-104**: `VoiceProcessingIO` AudioUnit shall provide hardware AEC, AGC, and noise suppression. These shall always be engaged and shall not be user-disableable. The Flutter audio processing settings UI shall display these as hardware-enabled with switches disabled and a "Hardware-enabled" label.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: `VoiceProcessingIO`, `AudioProcessingSettings` (Flutter)
- Source SAD: SAD-061, SAD-065
- Verification method: Platform Test, Audio Test
## 11. Updated SAD-to-SDD Coverage Matrix
| SAD Range | SDD Coverage |
|---|---|
| SAD-001 through SAD-060 | Covered by inherited SDD baseline `SDD-001` through `SDD-070` |
| SAD-061 through SAD-070 | Covered by `SDD-071` through `SDD-080` |
| SAD-071 through SAD-079 | Covered by `SDD-081` through `SDD-092` |
| SAD-080 | Covered by `SDD-093` |
| SAD-081 through SAD-083 | Covered by `SDD-094` through `SDD-097` |
| SAD-061 through SAD-070 | Covered by `SDD-098` through `SDD-104` (iOS platform additions) |
## Baseline Candidate 0.9.1 Update
@@ -1202,3 +1267,15 @@ The registry supports consistency between architecture, detailed design, impleme
| Version | Date | Description |
|---|---|---|
| 0.9.5 | 2026-05-15 | Added v1 audio + PTT lifecycle detailed design SDD-094 through SDD-097 sourced from SAD-081..083: bridge surface drops `start_audio` / `stop_audio` and exposes `chanora_bridge::voice::voice_join` / `voice_leave` with idempotent `AudioEngine::ensure_running` and `shutdown_if_idle`, `chanora_audio::TransmitMode` as `#[repr(u8)]` enum persisted via `chanora_storage::IdentityStore::{set,get}_transmit_mode` and mirrored across FRB as `BridgeTransmitMode`, `chanora_audio::release_tail::ReleaseTailTimer` as a tokio-task-owning adapter on the watch handle (no `unsafe`, no mutex; configurable 0500 ms via `release_tail_ms`), and the new `apps/chanora_flutter/lib/widgets/voice_bar.dart` + `voice_settings.dart` surfaces replacing the legacy `_AudioControls` widget. Strict layered sourcing preserved (`SDD -> SAD` only). |
## Baseline Candidate 0.9.6 Update
| Version | Date | Description |
|---|---|---|
| 0.9.6 | 2026-05-17 | Added iOS P0 platform SDD-098 through SDD-104 to close traceability gaps identified in the P0 audit: `IOSAudioSessionConfig` (AVAudioSession `.playAndRecord` configuration), `IOSPermissionRequester` (microphone permission flow), `IOSRouteChangeHandler` (route change observation and Rust bridge call), `IOSInterruptionRecovery` (audio interruption handling), `Info.plist` / `VPIOUnit` (background audio), `FocusedPttBackend` only (iOS no global PTT), and `VoiceProcessingIO` (hardware AEC/AGC/NS always engaged). Sourced from SAD-061, SAD-064, SAD-065, SAD-071. Strict layered sourcing preserved (`SDD -> SAD` only). |
## Baseline Candidate 0.9.7 Update
| Version | Date | Description |
|---|---|---|
| 0.9.7 | 2026-05-17 | Added design notes to SDD-101: (1) `shouldResume == false` behavior for iOS audio interruption recovery — VPIO remains paused, `BridgeEvent_InterruptionState` carries `shouldResume: false` to Flutter, snackbar surfaces the interruption, manual rejoin required; (2) iOS audio lifecycle interleaving concern — handlers are individually serialized but lack explicit lifecycle state, route-change-during-interruption risk is mitigated by iOS main-thread serialization for P0. |
+9 -9
View File
@@ -14,7 +14,7 @@ build from rc.8.
Source: this checklist mirrors the macOS, Linux, and Windows
acceptance documents. Auto-test sign-off lives in this document's
"Auto-test sign-off" section. SDD references: SDD-094..097 (v1
"Auto-test sign-off" section. SDD references: SDD-094..097, SDD-098..104 (v1
audio + PTT lifecycle), DEC-025 (target environment), DEC-027
(diagnostic privacy invariant), SRS-197 (iOS audio routing
contract), SRS-198 (honest capability advertising under runtime
@@ -48,7 +48,7 @@ session-level event tap analogue.
|---|---|---|
| 1.1 | Enter a reachable TS3 server address, a nickname, blank password | Connect button enabled |
| 1.2 | Tap Connect | Server snapshot appears |
| 1.3 | Quit (background) + relaunch with the same nickname | Server-visible UID is the same (identity persisted in `Documents/identity.tskey`) |
| 1.3 | Quit (background) + relaunch with the same nickname | Server-visible UID is the same (identity persisted in the app-private support directory as `identity.tskey`) |
## TC-2: Voice channel join (SRS-204, SDD-094)
@@ -59,7 +59,7 @@ session-level event tap analogue.
| 2.2 | Tap a different voice channel | Smooth move; no audio dropout. |
## TC-3: PTT press / release (Focused only — iOS limitation)
## TC-3: PTT press / release (Focused only — iOS limitation, SDD-103)
| # | Step | Expected |
|---|---|---|
@@ -68,7 +68,7 @@ session-level event tap analogue.
| 3.3 | Touch and hold the PTT button | Mic indicator turns "Mic on" while held; releases on touch-up. |
## TC-4: Release tail (SDD-097)
## TC-4: Release tail (SDD-096)
| # | Step | Expected |
|---|---|---|
@@ -97,7 +97,7 @@ session-level event tap analogue.
| 7.1 | Speaker mute | Other clients silenced locally. |
## TC-8: Background audio (UIBackgroundModes = audio)
## TC-8: Background audio (UIBackgroundModes = audio, SDD-102)
| # | Step | Expected |
|---|---|---|
@@ -106,13 +106,13 @@ session-level event tap analogue.
| 8.3 | Lock the iPhone for 30 s, then unlock | Session still intact (provided you stayed in the channel and on cellular/wifi network). |
## TC-9: AVAudioSession routing (SRS-197)
## TC-9: AVAudioSession routing (SDD-098, SDD-100, SDD-101)
| # | Step | Expected |
|---|---|---|
| 9.1 | With voice connected, place an iPhone phone call to yourself (or have someone call). | Chanora's audio session yields to the phone call (iOS owns the audio focus). |
| 9.2 | End the phone call | Chanora resumes audio without manual reconnect. |
| 9.3 | Connect AirPods / Bluetooth headset; talk on Chanora | Audio routes to the headset (`.allowBluetooth` + `.allowBluetoothA2DP` are set in `AppDelegate.swift`). |
| 9.3 | Connect AirPods / Bluetooth headset; talk on Chanora | Audio routes to the headset (`.allowBluetoothHFP` + `.allowBluetoothA2DP` are set in `AppDelegate.swift`). |
## TC-10: Reconnect
@@ -129,7 +129,7 @@ session-level event tap analogue.
| 11.1 | Export a diagnostic bundle | Allow-listed fields only; no raw key labels, no audio bytes. |
## TC-12: Capability badge labels
## TC-12: Capability badge labels (SDD-103)
| # | Step | Expected |
|---|---|---|
@@ -146,7 +146,7 @@ These rows do not require human interaction.
| `cargo test --workspace --lib` on macOS host | unchanged (iOS-specific Rust code is `#[cfg(target_os = "ios")]`-gated and not exercised in host tests) |
| `cargo build --release --target aarch64-apple-ios -p chanora_bridge` | clean — produces `libchanora_bridge.a` |
| `flutter build ios --release` | clean — produces `Runner.app` |
| `~/chanora/apps/chanora_flutter/ios/Runner/AppDelegate.swift::application(_:didFinishLaunchingWithOptions:)` configures AVAudioSession before Flutter starts | Log line `chanora_flutter: AVAudioSession configured (playAndRecord/voiceChat)` in device console (Xcode → Window → Devices and Simulators). |
| `AppDelegate.swift` registers `AVAudioSession.routeChangeNotification` and `AVAudioSession.interruptionNotification` observers (SDD-100, SDD-101) | Log lines `chanora_flutter: route change reason=…` and `chanora_flutter: audio interruption began/ended` in device console. |
## Sign-off form