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:
@@ -310,14 +310,14 @@ fn open_ios_voice_backend(
|
||||
audio_processing_stats.clone(),
|
||||
) {
|
||||
Ok(unit) => {
|
||||
info!(target: "chanora_audio", "ios: RemoteIO/Sonora experimental backend selected");
|
||||
info!(target: "chanora_audio", "ios: RemoteIO/WebRTC APM backend selected");
|
||||
return Ok(IosVoiceBackend::Raw(unit));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"ios: RemoteIO/Sonora backend failed; falling back to VoiceProcessingIO"
|
||||
"ios: RemoteIO/WebRTC APM backend failed; falling back to VoiceProcessingIO"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -724,6 +724,9 @@ impl AudioEngine {
|
||||
handler: audio_handler.clone(),
|
||||
output_gain: output_gain.clone(),
|
||||
output_muted: output_muted.clone(),
|
||||
voice_activity_selector: cfg.voice_activity_selector.clone(),
|
||||
audio_processing_config: audio_processing_config.clone(),
|
||||
audio_processing_stats: audio_processing_stats.clone(),
|
||||
};
|
||||
let mut android_voice_unit =
|
||||
crate::android_voice_unit::AndroidVoiceUnit::open(&cfg_av, params).map_err(|e| {
|
||||
@@ -739,16 +742,75 @@ impl AudioEngine {
|
||||
if let Some(mut event_rx) = android_voice_unit.take_event_rx() {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
if let BackendEvent::Disconnected = event {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"android: backend disconnected event received; stream reconnect requires session restart"
|
||||
);
|
||||
match event {
|
||||
BackendEvent::Disconnected => {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"android: backend disconnected event received; stream reconnect requires session restart"
|
||||
);
|
||||
}
|
||||
BackendEvent::FocusLost => {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"android: audio focus lost permanently (SDD-109); engine should leave session"
|
||||
);
|
||||
}
|
||||
BackendEvent::FocusTransient => {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
"android: transient audio focus loss (SDD-109); pausing capture"
|
||||
);
|
||||
}
|
||||
BackendEvent::FocusTransientCanDuck => {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
"android: transient audio focus loss with ducking (SDD-109); continuing"
|
||||
);
|
||||
}
|
||||
BackendEvent::FocusGain => {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
"android: audio focus regained (SDD-109); resuming capture"
|
||||
);
|
||||
}
|
||||
BackendEvent::BluetoothScoStateChanged(s) => {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
sco_state = s,
|
||||
"android: Bluetooth SCO state changed (SDD-110)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
crate::android_voice_unit::register_global_event_sender(android_voice_unit.event_sender());
|
||||
|
||||
if crate::android_voice_unit::chanora_android_request_audio_focus() {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
"android: audio focus requested (SDD-109)"
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"android: audio focus request failed; engine operates without focus (SDD-109)"
|
||||
);
|
||||
}
|
||||
|
||||
if crate::android_voice_unit::chanora_android_start_bluetooth_sco() {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
"android: Bluetooth SCO started (SDD-110)"
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
target: "chanora_audio",
|
||||
"android: Bluetooth SCO start failed; BT HFP may not route correctly (SDD-110)"
|
||||
);
|
||||
}
|
||||
|
||||
let capture_active = true;
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
@@ -949,13 +1011,17 @@ impl AudioEngine {
|
||||
let _ = self._ios_voice_backend.lock().unwrap().take();
|
||||
}
|
||||
// SDD-115 reverse-order teardown on Android:
|
||||
// 1) close the voice unit (releases SDD-113 hardware
|
||||
// 1) stop Bluetooth SCO + abandon audio focus;
|
||||
// 2) close the voice unit (releases SDD-113 hardware
|
||||
// effects then stops + closes the Oboe streams);
|
||||
// 2) restore the prior audio mode (SDD-108 §1 on 1 → 0
|
||||
// 3) restore the prior audio mode (SDD-108 §1 on 1 → 0
|
||||
// transition);
|
||||
// 3) stop the foreground service.
|
||||
// 4) stop the foreground service.
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
crate::android_voice_unit::chanora_android_stop_bluetooth_sco();
|
||||
crate::android_voice_unit::chanora_android_abandon_audio_focus();
|
||||
crate::android_voice_unit::clear_global_event_sender();
|
||||
if let Some(mut unit) = self._android_voice_unit.lock().unwrap().take() {
|
||||
use crate::mobile_voice_backend::MobileVoiceAudioBackend;
|
||||
if let Err(e) = unit.close() {
|
||||
|
||||
Reference in New Issue
Block a user