diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cb1d512..bede16d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -974,21 +974,33 @@ Cellular generation is approximated from `getLinkDownstreamBandwidthKbps()` to a Both Android app variants support 3-way audio routing: **Earpiece → Speaker → Bluetooth SCO**. +### Audio Mode Lifecycle + +`MODE_IN_COMMUNICATION` is set by the Rust call engine (via JNI `AudioManager.setMode()`) right before Oboe streams open — NOT at app launch. Restored to `MODE_NORMAL` when the call ends. This prevents hijacking system audio routing (music, BT A2DP) before a call is active. + ### Native Kotlin App -`AudioRouteManager.kt` handles device detection (via `AudioDeviceCallback`), SCO lifecycle (`startBluetoothSco` / `stopBluetoothSco`), and auto-fallback on BT disconnect. `CallViewModel.cycleAudioRoute()` cycles through available routes. +`AudioRouteManager.kt` handles device detection (via `AudioDeviceCallback`), SCO lifecycle, and auto-fallback on BT disconnect. `CallViewModel.cycleAudioRoute()` cycles through available routes. ### Tauri Desktop App -`android_audio.rs` provides JNI bridges to `AudioManager` for speakerphone and Bluetooth SCO control. After each route change, Oboe streams are stopped and restarted via `spawn_blocking` to force AAudio to reconfigure with the new routing. +`android_audio.rs` provides JNI bridges to `AudioManager` for speakerphone and Bluetooth SCO control. After each route change, Oboe streams are stopped and restarted via `spawn_blocking`. ``` User tap ──► cycleAudioRoute() │ - ├─ Earpiece: setSpeakerphoneOn(false) + ├─ Earpiece: setSpeakerphoneOn(false) + clearCommunicationDevice() ├─ Speaker: setSpeakerphoneOn(true) - └─ BT SCO: startBluetoothSco() + setBluetoothScoOn(true) - │ + └─ BT SCO: setCommunicationDevice(bt_device) [API 31+] + │ fallback: startBluetoothSco() [API < 31] ▼ - Oboe stop + start (~60-400ms) + Oboe stop + start_bt() for BT / start() for others ``` + +### BT SCO and Oboe + +BT SCO only supports 8/16kHz. When `bt_active=1`, Oboe capture skips `setSampleRate(48000)` and `setInputPreset(VoiceCommunication)`, letting the system choose the native BT rate. Oboe's `SampleRateConversionQuality::Best` bridges to our 48kHz ring buffers. Playout uses `Usage::Media` in BT mode to avoid conflicts with the communication device routing. + +### Hangup Signal Fix + +`SignalMessage::Hangup` now carries an optional `call_id` field. The relay uses it to end only the specific call instead of broadcasting to all active calls for the user — preventing a race where a hangup for call 1 kills a newly-placed call 2. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 703f8fc..3dc469e 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -587,9 +587,19 @@ Signal messages are sent over reliable QUIC streams as length-prefixed JSON: WarzonePhone supports three audio output routes on Android: **Earpiece**, **Speaker**, and **Bluetooth SCO**. The user cycles through available routes with a single button. +### Audio mode lifecycle + +`MODE_IN_COMMUNICATION` is set **when the call engine starts** (right before Oboe `audio_start()`), not at app launch. This is critical — setting it early hijacks system audio routing (e.g. music drops from BT A2DP to earpiece). `MODE_NORMAL` is restored when the call engine stops. + +``` +App launch → MODE_NORMAL (other apps' audio unaffected) +Call start → set_audio_mode_communication() → MODE_IN_COMMUNICATION +Call end → audio_stop() → set_audio_mode_normal() → MODE_NORMAL +``` + ### Route lifecycle -1. Call starts → Earpiece (default). `AudioManager.MODE_IN_COMMUNICATION` set by `CallService`. +1. Call starts → Earpiece (default). 2. User taps route button → cycles to next available route. 3. Route change requires Oboe stream restart (~60-400ms) because AAudio silently tears down streams on some OEMs when the routing target changes mid-stream. 4. Bluetooth disconnect mid-call → `AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece or Speaker. @@ -598,11 +608,13 @@ WarzonePhone supports three audio output routes on Android: **Earpiece**, **Spea SCO (Synchronous Connection Oriented) is the correct Bluetooth profile for VoIP — it provides bidirectional mono audio at 8/16 kHz with ~30ms latency. A2DP (stereo, high-quality) is unidirectional and adds 100-200ms of buffering, making it unsuitable for real-time voice. -The deprecated `AudioManager.startBluetoothSco()` API is used because the modern `setCommunicationDevice()` requires API 31+ and our minSdk is 26. The deprecated APIs are functional on all tested devices through API 35. +On API 31+ (Android 12), we use the modern `setCommunicationDevice(AudioDeviceInfo)` API to route audio to the BT SCO device. The deprecated `startBluetoothSco()` + `setBluetoothScoOn()` path is used as fallback on older APIs. `setBluetoothScoOn()` is silently rejected on Android 12+ for non-system apps. + +BT SCO devices only support 8/16kHz sample rates, but our pipeline runs at 48kHz. When BT is active, Oboe opens in **BT mode** (`bt_active=1`): capture skips `setSampleRate(48000)` and `setInputPreset(VoiceCommunication)`, letting the system open at the device's native rate. Oboe's `SampleRateConversionQuality::Best` resamples to/from 48kHz for our ring buffers. ### Two app variants -Both the native Kotlin app (`AudioRouteManager.kt`) and the Tauri app (`android_audio.rs` JNI bridge) call the same `AudioManager` APIs. The native app uses `AudioDeviceCallback` for automatic device detection; the Tauri app queries `getDevices()` on demand. +Both the native Kotlin app (`AudioRouteManager.kt`) and the Tauri app (`android_audio.rs` JNI bridge) support BT SCO routing. The native app uses `AudioDeviceCallback` for automatic device detection; the Tauri app uses `getAvailableCommunicationDevices()` (API 31+) or `getDevices()` on demand. ## Network Change Response @@ -631,4 +643,19 @@ These thresholds are conservative. Carriers over-report bandwidth, but for VoIP - **Rust** 1.85+ (2024 edition) - **Linux**: cmake, pkg-config, libasound2-dev (for audio feature) - **macOS**: Xcode command line tools (CoreAudio included) -- **Android**: NDK r27c, cmake 3.28+ (from pip) +- **Android**: NDK 26.1 (r26b), cmake 3.25-3.28 (system package) + +### Android APK Builds + +```bash +# arm64 only (default, 25MB release APK) +./scripts/build-tauri-android.sh --init --release --arch arm64 + +# armv7 only (smaller devices) +./scripts/build-tauri-android.sh --init --release --arch armv7 + +# both architectures as separate APKs +./scripts/build-tauri-android.sh --init --release --arch all +``` + +Release APKs are signed with `android/keystore/wzp-release.jks` via `apksigner`. Per-arch builds produce separate APKs (~25MB each vs ~50MB universal) for easier sharing with testers. diff --git a/docs/PRD-bluetooth-audio.md b/docs/PRD-bluetooth-audio.md index ffb8e43..d7e839c 100644 --- a/docs/PRD-bluetooth-audio.md +++ b/docs/PRD-bluetooth-audio.md @@ -50,18 +50,23 @@ Wire Bluetooth SCO routing end-to-end through both app variants, replacing the b | File | Change | |------|--------| -| `android_audio.rs` | Added `start_bluetooth_sco()`, `stop_bluetooth_sco()`, `is_bluetooth_sco_on()`, `is_bluetooth_available()` | -| `lib.rs` | Added `set_bluetooth_sco`, `is_bluetooth_available`, `get_audio_route` Tauri commands | +| `android_audio.rs` | `setCommunicationDevice()` (API 31+) with `startBluetoothSco()` fallback; `set_audio_mode_communication/normal()` for call lifecycle | +| `lib.rs` | `set_bluetooth_sco`, `is_bluetooth_available`, `get_audio_route` Tauri commands; SCO polling + 500ms route delay | +| `wzp_native.rs` | Added `audio_start_bt()` for BT-mode Oboe (skips 48kHz + VoiceCommunication preset) | +| `oboe_bridge.cpp` | `bt_active` flag: capture skips sample rate + input preset; playout uses `Usage::Media`; both use `Shared` mode + `SampleRateConversionQuality::Best` | +| `engine.rs` | `set_audio_mode_communication()` before `audio_start()`; `set_audio_mode_normal()` after `audio_stop()` | +| `MainActivity.kt` | Removed `MODE_IN_COMMUNICATION` from app launch — deferred to call start | | `main.ts` | Replaced `speakerphoneOn` toggle with `currentAudioRoute` cycling logic | | `style.css` | Added `.bt-on` CSS class (blue-400 highlight) | ## Audio Route Lifecycle -1. **Call starts** → route defaults to Earpiece -2. **User taps route button** → cycles to next available route -3. **Route changes** → AudioManager JNI call + Oboe stream restart (~60-400ms) -4. **BT device disconnects mid-call** → `AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece/Speaker -5. **Call ends** → route reset to Earpiece, BT SCO stopped +1. **App launch** → `MODE_NORMAL` (other apps' audio unaffected — BT A2DP music keeps playing) +2. **Call starts** → `MODE_IN_COMMUNICATION` set via JNI, Oboe opens with earpiece routing +3. **User taps route button** → cycles to next available route +4. **Route changes** → `setCommunicationDevice()` (API 31+) + Oboe restart in BT mode or normal mode +5. **BT device disconnects mid-call** → `AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece/Speaker +6. **Call ends** → route reset, `MODE_NORMAL` restored ## Route Cycling Logic @@ -83,8 +88,10 @@ If BT not available: ## Known Limitations - **SCO only** — no A2DP (stereo music profile). SCO is correct for VoIP (bidirectional mono). -- **Deprecated APIs** — `startBluetoothSco()`, `isBluetoothScoOn` are deprecated in API 31+ but still functional. Modern replacement `setCommunicationDevice()` requires API 31 and more complex device enumeration. Since minSdk is 26, deprecated path is correct. -- **No auto-switch on BT connect** — when a BT device connects mid-call, `onRouteChanged` fires but we don't auto-switch. User must tap the button. +- **API 31+ required for modern path** — `setCommunicationDevice()` is the primary BT routing API. Fallback to deprecated `startBluetoothSco()` on API < 31 (untested). +- **BT SCO capture at 8/16kHz** — Oboe resamples to 48kHz via `SampleRateConversionQuality::Best`. Quality is inherently limited by the SCO codec (CVSD at 8kHz or mSBC at 16kHz). +- **No auto-switch on BT connect** — when a BT device connects mid-call, user must tap the route button. +- **500ms route switch delay** — after `setCommunicationDevice()` returns, the audio policy needs time to apply the bt-sco route. We wait 500ms before restarting Oboe. ## Testing diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index ddfd213..565b763 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -191,3 +191,26 @@ Run with `wzp-bench --all`. Representative results (Apple M-series, single core) - **Hetzner VPS**: Build script (`scripts/build-linux.sh`) tested for provisioning, building, and downloading Linux binaries - **CI**: Gitea workflow defined for amd64/arm64/armv7 builds - **Production**: Not yet deployed to production networks + +## Recent Changes (2026-04-12) + +### Bluetooth Audio Routing +- 3-way route cycling: Earpiece → Speaker → Bluetooth SCO +- `setCommunicationDevice()` API 31+ with `startBluetoothSco()` fallback +- BT-mode Oboe: capture skips 48kHz + VoiceCommunication, Oboe resamples 8/16kHz ↔ 48kHz +- `MODE_IN_COMMUNICATION` deferred to call start (was at app launch — hijacked system audio) + +### Network Change Detection +- `NetworkMonitor.kt` wraps `ConnectivityManager.NetworkCallback` +- WiFi/cellular classification via bandwidth heuristics (no READ_PHONE_STATE needed) +- Feeds `AdaptiveQualityController::signal_network_change()` via JNI → AtomicU8 → recv task + +### Hangup Signal Fix +- `SignalMessage::Hangup` now carries optional `call_id` +- Relay only ends the named call (not all calls for the user) +- Fixes race: hangup for call 1 no longer kills newly-placed call 2 + +### Per-Architecture APK Builds +- `build-tauri-android.sh --arch arm64|armv7|all` +- Separate per-arch APKs (~25MB each vs ~50MB universal) +- Release APKs signed with `wzp-release.jks` via `apksigner`