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:
@@ -18,6 +18,11 @@ import '../l10n/generated/app_localizations.dart';
|
||||
import 'voice_platform.dart';
|
||||
import '../src/rust/api.dart' as rust;
|
||||
|
||||
bool get _isAndroid {
|
||||
if (kIsWeb) return false;
|
||||
return Platform.isAndroid;
|
||||
}
|
||||
|
||||
bool get _isIos {
|
||||
if (kIsWeb) return false;
|
||||
return Platform.isIOS;
|
||||
@@ -68,6 +73,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
late rust.BridgeVadBackend _vadBackend;
|
||||
late rust.BridgeIosVoiceProcessingMode _iosMode;
|
||||
late bool _debugWavDump;
|
||||
late bool _preferHardware; // Android only: try JNI hardware effects
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -86,29 +92,59 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
: c.vadBackend;
|
||||
_iosMode = c.iosMode;
|
||||
_debugWavDump = c.debugWavDumpEnabled;
|
||||
_preferHardware = c.aec == rust.BridgeEffectOwner.platform
|
||||
|| c.ns == rust.BridgeEffectOwner.platform
|
||||
|| c.agc == rust.BridgeEffectOwner.platform;
|
||||
}
|
||||
|
||||
rust.BridgeAudioProcessingConfig _buildConfig() {
|
||||
final c = widget.initialAudioConfig;
|
||||
final isSonora =
|
||||
_iosMode == rust.BridgeIosVoiceProcessingMode.sonoraExperimental;
|
||||
// In VPIO mode, enabled effects are platform-owned. Sonora ownership is
|
||||
// reserved for the experimental raw path so config validation stays honest.
|
||||
|
||||
if (_isAndroid) {
|
||||
final owner = _preferHardware
|
||||
? rust.BridgeEffectOwner.platform
|
||||
: rust.BridgeEffectOwner.webrtcApm;
|
||||
return rust.BridgeAudioProcessingConfig(
|
||||
route: c.route,
|
||||
iosMode: _iosMode,
|
||||
processingBackend: _preferHardware
|
||||
? rust.BridgeAudioBackend.platformVoiceProcessing
|
||||
: rust.BridgeAudioBackend.webrtcApm,
|
||||
vadBackend: _vadBackend == rust.BridgeVadBackend.disabled
|
||||
? rust.BridgeVadBackend.webrtcVad
|
||||
: _vadBackend,
|
||||
aec: _aecEnabled ? owner : rust.BridgeEffectOwner.off,
|
||||
ns: _nsEnabled ? owner : rust.BridgeEffectOwner.off,
|
||||
agc: _agcEnabled ? owner : rust.BridgeEffectOwner.off,
|
||||
hpfEnabled: _hpfEnabled,
|
||||
limiterEnabled: _limiterEnabled,
|
||||
vadHangoverMs: c.vadHangoverMs,
|
||||
vadPreRollMs: c.vadPreRollMs,
|
||||
vadMinTxMs: c.vadMinTxMs,
|
||||
debugWavDumpEnabled: _debugWavDump,
|
||||
);
|
||||
}
|
||||
|
||||
// iOS / macOS: VPIO vs Sonora paths.
|
||||
// In VPIO mode, enabled effects are platform-owned. The experimental raw
|
||||
// path uses WebRTC APM ownership so config validation stays honest.
|
||||
final aecOwner = isSonora
|
||||
? (_aecEnabled
|
||||
? rust.BridgeEffectOwner.sonora
|
||||
? rust.BridgeEffectOwner.webrtcApm
|
||||
: rust.BridgeEffectOwner.off)
|
||||
: rust.BridgeEffectOwner.platform; // VPIO always owns AEC
|
||||
final nsOwner = isSonora
|
||||
? (_nsEnabled
|
||||
? rust.BridgeEffectOwner.sonora
|
||||
? rust.BridgeEffectOwner.webrtcApm
|
||||
: rust.BridgeEffectOwner.off)
|
||||
: (_nsEnabled
|
||||
? rust.BridgeEffectOwner.platform
|
||||
: rust.BridgeEffectOwner.off);
|
||||
final agcOwner = isSonora
|
||||
? (_agcEnabled
|
||||
? rust.BridgeEffectOwner.sonora
|
||||
? rust.BridgeEffectOwner.webrtcApm
|
||||
: rust.BridgeEffectOwner.off)
|
||||
: (_agcEnabled
|
||||
? rust.BridgeEffectOwner.platform
|
||||
@@ -120,7 +156,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
route: c.route,
|
||||
iosMode: _iosMode,
|
||||
processingBackend: isSonora
|
||||
? rust.BridgeAudioBackend.sonora
|
||||
? rust.BridgeAudioBackend.webrtcApm
|
||||
: rust.BridgeAudioBackend.platformVoiceProcessing,
|
||||
vadBackend: vadBackend,
|
||||
aec: aecOwner,
|
||||
@@ -244,6 +280,30 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
|
||||
// Android HW/SW selector
|
||||
if (_isAndroid) ...[
|
||||
_subHeader(theme, 'Processing backend'),
|
||||
_radioTile<bool>(
|
||||
value: true,
|
||||
groupValue: _preferHardware,
|
||||
title: const Text('Platform (auto)'),
|
||||
subtitle: _tileSubtitle(
|
||||
'Try hardware JNI effects · software fallback',
|
||||
),
|
||||
onSelected: (v) => setState(() => _preferHardware = v),
|
||||
),
|
||||
_radioTile<bool>(
|
||||
value: false,
|
||||
groupValue: _preferHardware,
|
||||
title: const Text('WebRTC APM'),
|
||||
subtitle: _tileSubtitle(
|
||||
'Software AEC3 · NS · AGC2',
|
||||
),
|
||||
onSelected: (v) => setState(() => _preferHardware = v),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
|
||||
// DSP toggles
|
||||
_subHeader(theme, 'DSP stages'),
|
||||
_switchTile(
|
||||
@@ -254,12 +314,14 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
),
|
||||
_switchTile(
|
||||
title: 'Echo cancellation (AEC3)',
|
||||
subtitle: platformVpio
|
||||
? 'Managed by platform VPIO'
|
||||
: 'Adaptive NLMS · 80 ms tail',
|
||||
subtitle: _isAndroid
|
||||
? 'WebRTC AEC3 · adaptive filter'
|
||||
: platformVpio
|
||||
? 'Managed by platform VPIO'
|
||||
: 'Adaptive NLMS · 80 ms tail',
|
||||
value: _aecEnabled,
|
||||
// AEC is always on in VPIO mode — disable the toggle.
|
||||
onSelected: platformVpio ? null : (v) => _aecEnabled = v,
|
||||
onSelected: (_isAndroid || !platformVpio) ? (v) => _aecEnabled = v : null,
|
||||
),
|
||||
_switchTile(
|
||||
title: 'Auto gain control (AGC2)',
|
||||
|
||||
Reference in New Issue
Block a user