feat: Android Oboe voice backend — WebRTC APM, VAD, HW/SW toggle, BBCode welcome, link trust, foreground task

Audio engine (Rust):
- Android Oboe: WebRTC APM (AEC/NS/AGC/HPF) + TEN/Silero ONNX VAD
- Hardware effects (JNI) with software fallback per-effect
- Render reference buffer for AEC between output/capture callbacks
- Voice activity gate: suppress transmission when speaker muted (all platforms)
- Audio focus (SDD-109) + Bluetooth SCO (SDD-110) via JNI
- ONNX Runtime 1.26 via ort 2.0.0-rc.12 (down from rc.10, ndarray 0.17)
- VAD worker channel capacity 8→32, initial seq u64::MAX (warm-up fix)
- TEN VAD default backend (was Silero)
- Platform→WebrtcApm resolution after hardware binding
- oboe-rs edisonjwa fork with get_raw_session_id()

Android Kotlin:
- AndroidAudioFocusController + AndroidBluetoothScoController
- AndroidAudioLifecycleController (route changes to Flutter)
- ProGuard rules for new controllers

Flutter UI:
- VoiceSettings: Android HW/SW toggle (Platform auto / WebRTC APM)
- VoiceStatusChip: mute warning border + Speaker muted label
- BBCode welcome message parser (BbCodeText, case-insensitive)
- Welcome message foldable (expanded by default)
- Link trust dialog (domain wildcards, SharedPreferences)
- HapticFeedback on voice sheet opener
- Server name in AppBar, version v0.1.0
- Default channel (id=1) visible, serverquery clients hidden
- flutter_foreground_task integration

Config:
- ort load-dynamic on all non-iOS (Android/Linux/Windows)
- ONNX Runtime AAR 1.26.0
- ndarray moved to common deps (was Apple-only)
This commit is contained in:
Edison Jwa
2026-05-22 09:29:57 +09:00
parent 6af4ecab0f
commit bf284018e6
37 changed files with 3676 additions and 703 deletions
+17 -8
View File
@@ -13,6 +13,12 @@ publish.workspace = true
chanora_protocol = { path = "../chanora_protocol" }
thiserror.workspace = true
tracing.workspace = true
sonora = "0.1"
webrtc-vad = "0.4"
# ndarray is required by ort's tensor construction API and by
# Silero / TEN VAD ONNX inference across all platforms.
ndarray = "0.17"
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
# this crate can compile against it without going through tsclientlib.
@@ -47,19 +53,15 @@ coreaudio-rs = "0.14"
# Grand Central Dispatch bindings — used to run AudioUnit initialize/start
# on the main queue to avoid the VPIO RPC timeout on iOS simulator.
dispatch2 = "0.3"
# ndarray is required by ort's tensor construction API.
ndarray = "0.16"
[target.'cfg(target_os = "ios")'.dependencies]
# ONNX Runtime Rust binding for Silero VAD v6 (P1 VAD_002). The official
# iOS CocoaPod ships ONNX Runtime as a static framework, so iOS links it
# into chanora_bridge at build time instead of loading a dylib at runtime.
ort = { version = "2.0.0-rc.10", default-features = false, features = ["std", "ndarray"] }
ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray"] }
[target.'cfg(target_os = "macos")'.dependencies]
# macOS keeps dynamic loading so developer machines can provide ORT via
# ORT_DYLIB_PATH without forcing a bundled runtime into desktop builds.
ort = { version = "2.0.0-rc.10", default-features = false, features = ["load-dynamic", "ndarray"] }
[target.'cfg(not(target_os = "ios"))'.dependencies]
ort = { version = "2.0.0-rc.12", default-features = false, features = ["load-dynamic", "ndarray", "api-24"] }
[target.'cfg(target_os = "android")'.dependencies]
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
@@ -75,7 +77,14 @@ ndk-context = "0.1"
# shipped with `oboe-sys` 0.6 covers armv7 / aarch64 / x86 / x86_64.
# Default features keep the precompiled library + pregenerated bindings
# so we avoid the clang-sys / libclang requirement on the build host.
oboe = "0.6"
#
# Using local fork edisonjwa/oboe-rs (v0.6.2) with:
# - catch_unwind safety in callbacks
# - unwrap_or_default in enum getters (no more SessionId panic)
# - get_raw_session_id() for JNI hardware effect binding
# - deduplicated macro impls
# - PowerSavingOffloaded PerformanceMode variant
oboe = { path = "../../../oboe-rs" }
[target.'cfg(target_os = "windows")'.dependencies]
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices