From 5515ff664339109c8d711f828ea4ddc7308852cf Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Mon, 25 May 2026 01:19:09 +0900 Subject: [PATCH] feat: stabilize voice activity and audio routing --- .../AndroidAudioLifecycleController.kt | 29 +- .../AndroidAudioOutputController.kt | 20 + .../AndroidBluetoothScoController.kt | 142 ++++- .../assets/models/ten_vad.onnx | Bin 315449 -> 0 bytes .../lib/design/platform_capabilities.dart | 271 ++++----- apps/chanora_flutter/lib/main.dart | 215 +++++-- apps/chanora_flutter/lib/src/rust/api.dart | 84 ++- .../lib/src/rust/api.freezed.dart | 97 +++- .../lib/src/rust/frb_generated.dart | 131 +++-- .../lib/widgets/audio_debug_stats_panel.dart | 4 +- .../lib/widgets/audio_device_list_tile.dart | 63 +- .../audio_processing_config_state.dart | 180 ++++-- .../lib/widgets/chat_views.dart | 453 ++++++++++++++- .../lib/widgets/voice_compact.dart | 199 ++++--- .../lib/widgets/voice_settings.dart | 144 +++-- .../lib/widgets/voice_settings_controls.dart | 27 +- apps/chanora_flutter/pubspec.yaml | 1 - .../widgets/audio_device_list_tile_test.dart | 90 +++ .../audio_processing_config_state_test.dart | 163 +++++- .../test/widgets/chat_views_test.dart | 209 +++++++ .../widgets/voice_settings_controls_test.dart | 14 +- core/chanora_core/src/lib.rs | 171 +++--- crates/chanora_audio/Cargo.toml | 2 +- .../chanora_audio/src/android_voice_unit.rs | 288 ++++++---- crates/chanora_audio/src/audio_processing.rs | 70 ++- crates/chanora_audio/src/engine.rs | 536 ++++++++++++++---- crates/chanora_audio/src/ios_raw_unit.rs | 151 ++--- crates/chanora_audio/src/ios_voice_unit.rs | 155 +++-- .../chanora_audio/src/mobile_voice_backend.rs | 20 +- crates/chanora_audio/src/vad/mod.rs | 74 --- crates/chanora_audio/src/vad/ten_onnx.rs | 452 --------------- crates/chanora_bridge/src/api.rs | 108 +++- crates/chanora_bridge/src/frb_generated.rs | 144 ++--- crates/chanora_protocol/src/adapter.rs | 98 ++-- 34 files changed, 3054 insertions(+), 1751 deletions(-) delete mode 100644 apps/chanora_flutter/assets/models/ten_vad.onnx delete mode 100644 crates/chanora_audio/src/vad/ten_onnx.rs diff --git a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioLifecycleController.kt b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioLifecycleController.kt index 2dd91d4..8e63ad3 100644 --- a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioLifecycleController.kt +++ b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioLifecycleController.kt @@ -50,7 +50,7 @@ internal class AndroidAudioLifecycleController( private val mainHandler = Handler(Looper.getMainLooper()) private var callbackRegistered = false - private var currentRouteType: String? = null + private var currentRouteFingerprint: String? = null private val audioDeviceCallback = object : AudioDeviceCallback() { override fun onAudioDevicesAdded(addedDevices: Array) { @@ -121,17 +121,38 @@ internal class AndroidAudioLifecycleController( device: AudioDeviceInfo?, ) { val routeType = classifyCurrentRoute(device) - if (routeType == currentRouteType) { + val routeFingerprint = buildRouteFingerprint(routeType) + if (routeFingerprint == currentRouteFingerprint) { return } - currentRouteType = routeType - Log.i(TAG, "route changed to: $routeType") + currentRouteFingerprint = routeFingerprint + Log.i(TAG, "route changed to: $routeType fingerprint=$routeFingerprint") channel?.invokeMethod( "handleRouteChange", mapOf("routeType" to routeType), ) } + private fun buildRouteFingerprint(routeType: String): String { + val selectedCommunicationId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.communicationDevice?.id?.toString().orEmpty() + } else { + "" + } + val inputs = audioManager + .getDevices(AudioManager.GET_DEVICES_INPUTS) + .map { "${it.id}:${it.type}:${it.productName?.toString()?.trim().orEmpty()}" } + .sorted() + .joinToString("|") + val outputs = audioManager + .getDevices(AudioManager.GET_DEVICES_OUTPUTS) + .map { "${it.id}:${it.type}:${it.productName?.toString()?.trim().orEmpty()}" } + .sorted() + .joinToString("|") + return "$routeType#$selectedCommunicationId#$inputs#$outputs" + } + /** * Classify the current audio output route into a stable string * matching the iOS route-classification schema so the Dart-side diff --git a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioOutputController.kt b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioOutputController.kt index b979f29..8a604fe 100644 --- a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioOutputController.kt +++ b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidAudioOutputController.kt @@ -1,6 +1,8 @@ package app.chanora.chanora_flutter import android.content.Context +import android.Manifest +import android.content.pm.PackageManager import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager @@ -8,6 +10,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log +import androidx.core.content.ContextCompat import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -121,6 +124,18 @@ internal class AndroidAudioOutputController(context: Context) : ) return false } + if (target.requiresBluetoothConnectPermission() && + ContextCompat.checkSelfPermission( + appContext, + Manifest.permission.BLUETOOTH_CONNECT, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w( + TAG, + "setCommunicationDevice blocked for bluetooth target=${target.logLabel()}: BLUETOOTH_CONNECT not granted", + ) + return false + } val changed = audioManager.setCommunicationDevice(target) Log.i(TAG, "setCommunicationDevice device=${target.logLabel()} changed=$changed selected=${audioManager.communicationDevice?.logLabel()}") return changed @@ -230,6 +245,11 @@ internal class AndroidAudioOutputController(context: Context) : type == AudioDeviceInfo.TYPE_BLE_BROADCAST) } + private fun AudioDeviceInfo.requiresBluetoothConnectPermission(): Boolean = + normalizedType() == "bluetoothA2dp" || + normalizedType() == "bluetoothSco" || + normalizedType() == "bluetoothLe" + private fun AudioDeviceInfo.logLabel(): String = "id=$id type=${normalizedType()} product=${productName?.toString()?.trim().orEmpty()}" diff --git a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidBluetoothScoController.kt b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidBluetoothScoController.kt index 9da82e0..4eb88db 100644 --- a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidBluetoothScoController.kt +++ b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/AndroidBluetoothScoController.kt @@ -2,44 +2,50 @@ package app.chanora.chanora_flutter import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothProfile +import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager +import android.media.AudioDeviceInfo import android.media.AudioManager +import android.os.Build import android.util.Log +import androidx.core.content.ContextCompat /** - * Manages Android Bluetooth SCO (Synchronous Connection-Oriented) audio - * for the Chanora voice session. + * Manages Android Bluetooth voice routing for the Chanora voice session. * * Trace: SDD-110 (Android Bluetooth SCO) * - * Bluetooth SCO is the low-latency, monaural audio path used by Bluetooth - * headsets for phone calls. Without SCO, voice audio may route through - * A2DP which is stereo, high-latency, and lacks the codec support for - * two-way communication. On Android we must explicitly start/stop SCO - * when a Bluetooth headset is present; the platform does not auto-manage - * this for VoIP apps. + * On Android 13 / API 33 and newer, VoIP apps are expected to select a + * Bluetooth communication route with `AudioManager.setCommunicationDevice()` + * so BLE audio headsets are supported. On older Android releases we fall + * back to legacy SCO start / stop management. * * ## Lifecycle * * 1. [start] — called by the Rust audio engine (via JNI) after the Oboe - * voice streams are opened. Calls `AudioManager.startBluetoothSco()` + * voice streams are opened. On API 33+ this prefers + * `AudioManager.setCommunicationDevice()` for Bluetooth communication + * devices. On older releases it calls `AudioManager.startBluetoothSco()` * if a Bluetooth SCO-capable device is connected and registers a * `BroadcastReceiver` for `ACTION_SCO_AUDIO_STATE_UPDATED`. - * 2. SCO state changes are forwarded to the Rust engine via + * 2. Legacy SCO state changes, or synthetic connected/disconnected state + * changes for the API 33+ path, are forwarded to the Rust engine via * `publishScoStateChange(int)`, a JNI function declared in * `crates/chanora_audio/src/android_voice_unit.rs`. - * 3. [stop] — called by the Rust engine on voice stop. Calls - * `AudioManager.stopBluetoothSco()` and unregisters the receiver. + * 3. [stop] — called by the Rust engine on voice stop. Clears the selected + * communication device on API 33+ when it is one we selected, otherwise + * falls back to `AudioManager.stopBluetoothSco()` and receiver teardown. * * ## Thread model * * `start` / `stop` are called from a tokio worker thread (via JNI). - * `AudioManager.startBluetoothSco` is asynchronous — the platform - * responds with `ACTION_SCO_AUDIO_STATE_UPDATED` which arrives on the - * main thread via the `BroadcastReceiver`. + * `AudioManager.startBluetoothSco` is asynchronous on legacy devices — + * the platform responds with `ACTION_SCO_AUDIO_STATE_UPDATED` which arrives + * on the main thread via the `BroadcastReceiver`. */ internal class AndroidBluetoothScoController { @@ -72,6 +78,16 @@ internal class AndroidBluetoothScoController { return } scoStarted = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (trySelectBluetoothCommunicationDevice(appContext)) { + return + } + Log.i( + TAG, + "No selectable Bluetooth communication device on API 33+; legacy SCO fallback is skipped", + ) + return + } registerScoReceiver(appContext) tryStartSco(appContext) } @@ -86,12 +102,17 @@ internal class AndroidBluetoothScoController { fun stop(context: Context) { val appContext = context.applicationContext scoStarted = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + clearSelectedBluetoothCommunicationDevice(appContext) + return + } unregisterScoReceiver(appContext) tryStopSco(appContext) } private var scoStarted: Boolean = false private var receiverRegistered: Boolean = false + private var selectedCommunicationDeviceId: Int? = null private val scoReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -173,6 +194,15 @@ internal class AndroidBluetoothScoController { Log.e(TAG, "AudioManager unavailable; cannot start SCO") return } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "BLUETOOTH_CONNECT not granted; skipping SCO start") + return + } if (isBluetoothScoOn(am)) { Log.i(TAG, "SCO already on; no-op") @@ -212,6 +242,69 @@ internal class AndroidBluetoothScoController { } } + private fun trySelectBluetoothCommunicationDevice(context: Context): Boolean { + val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + if (am == null) { + Log.e(TAG, "AudioManager unavailable; cannot select communication device") + return false + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return false + } + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT, + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "BLUETOOTH_CONNECT not granted; skipping bluetooth communication device selection") + return false + } + val target = am.availableCommunicationDevices.firstOrNull { it.isBluetoothCommunicationDevice() } + if (target == null) { + Log.i(TAG, "No Bluetooth communication device available on API 33+") + return false + } + val current = am.communicationDevice + if (current?.id == target.id) { + selectedCommunicationDeviceId = target.id + publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_CONNECTED) + Log.i(TAG, "Bluetooth communication device already selected: ${target.logLabel()}") + return true + } + val changed = am.setCommunicationDevice(target) + Log.i( + TAG, + "setCommunicationDevice bluetooth target=${target.logLabel()} changed=$changed selected=${am.communicationDevice?.logLabel()}", + ) + if (changed) { + selectedCommunicationDeviceId = target.id + publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_CONNECTED) + } + return changed + } + + private fun clearSelectedBluetoothCommunicationDevice(context: Context) { + val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + if (am == null) { + Log.e(TAG, "AudioManager unavailable; cannot clear communication device") + return + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return + } + val selectedId = selectedCommunicationDeviceId + val current = am.communicationDevice + if (selectedId == null || current?.id != selectedId) { + selectedCommunicationDeviceId = null + Log.d(TAG, "No app-selected Bluetooth communication device to clear") + return + } + am.clearCommunicationDevice() + selectedCommunicationDeviceId = null + publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED) + Log.i(TAG, "clearCommunicationDevice() dispatched for bluetooth route") + } + private fun tryStopSco(context: Context) { val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager if (am == null) { @@ -230,6 +323,25 @@ internal class AndroidBluetoothScoController { } } + private fun publishSyntheticScoState(state: Int) { + try { + publishScoStateChange(state) + } catch (t: Throwable) { + Log.w(TAG, "publishScoStateChange JNI failed: ${t.message}", t) + } + } + + private fun AudioDeviceInfo.isBluetoothCommunicationDevice(): Boolean = + type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + (type == AudioDeviceInfo.TYPE_BLE_HEADSET || + type == AudioDeviceInfo.TYPE_BLE_SPEAKER || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + type == AudioDeviceInfo.TYPE_BLE_BROADCAST))) + + private fun AudioDeviceInfo.logLabel(): String = + "id=$id type=$type product=${productName?.toString()?.trim().orEmpty()}" + private fun scoStateName(state: Int): String = when (state) { AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> "DISCONNECTED" AudioManager.SCO_AUDIO_STATE_CONNECTED -> "CONNECTED" diff --git a/apps/chanora_flutter/assets/models/ten_vad.onnx b/apps/chanora_flutter/assets/models/ten_vad.onnx deleted file mode 100644 index 07b281610bc97bbaaa919942ac1d306756219119..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315449 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≷$+zS0}T((v^td_0;KgT$zlRX8Q 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 VlaZ8M=V!Qm+qON1Tervx{V$1J!nOba diff --git a/apps/chanora_flutter/lib/design/platform_capabilities.dart b/apps/chanora_flutter/lib/design/platform_capabilities.dart index e5cbcbb..5182578 100644 --- a/apps/chanora_flutter/lib/design/platform_capabilities.dart +++ b/apps/chanora_flutter/lib/design/platform_capabilities.dart @@ -51,146 +51,147 @@ List currentPlatformCapabilities() { } List _androidCapabilities() => [ - const PlatformCapability( - feature: 'Voice capture', - description: 'Oboe AAudio backend; Bluetooth SCO supported.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Voice processing', - description: 'WebRTC AEC3/NS/AGC2, VAD via Silero ONNX.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Foreground service', - description: 'Persistent notification during active connection.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Global PTT hotkey', - description: 'Not available on Android.', - tier: CapabilityTier.unavailable, - ), - const PlatformCapability( - feature: 'Audio device selection', - description: 'System-managed; wired/BT/SCO automatic routing.', - tier: CapabilityTier.degraded, - ), - const PlatformCapability( - feature: 'Push-to-talk button', - description: 'Media button / volume-key binding supported.', - tier: CapabilityTier.supported, - ), - ]; + const PlatformCapability( + feature: 'Voice capture', + description: 'Oboe AAudio backend; Bluetooth SCO supported.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Voice processing', + description: 'WebRTC AEC3/NS/AGC2, VAD via Silero ONNX.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Foreground service', + description: 'Persistent notification during active connection.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Global PTT hotkey', + description: 'Not available on Android.', + tier: CapabilityTier.unavailable, + ), + const PlatformCapability( + feature: 'Audio device selection', + description: 'System-managed; wired/BT/SCO automatic routing.', + tier: CapabilityTier.degraded, + ), + const PlatformCapability( + feature: 'Push-to-talk button', + description: 'Media button / volume-key binding supported.', + tier: CapabilityTier.supported, + ), +]; List _iosCapabilities() => [ - const PlatformCapability( - feature: 'Voice capture', - description: 'VoiceProcessingIO AudioUnit (default); Sonora raw mode optional.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Voice processing', - description: 'Platform AEC/NS/AGC via VPIO; optional Rust AEC3/NS/AGC2 via Sonora.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Audio route switching', - description: 'Handles speaker/earpiece/Bluetooth route changes and interruptions.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Global PTT hotkey', - description: 'Not available on iOS.', - tier: CapabilityTier.unavailable, - ), - const PlatformCapability( - feature: 'Background audio', - description: 'Supported via AVAudioSession background mode.', - tier: CapabilityTier.supported, - ), - ]; + const PlatformCapability( + feature: 'Voice capture', + description: 'VoiceProcessingIO AudioUnit (default).', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Voice processing', + description: 'Platform AEC/NS/AGC via VPIO.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Audio route switching', + description: + 'Handles speaker/earpiece/Bluetooth route changes and interruptions.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Global PTT hotkey', + description: 'Not available on iOS.', + tier: CapabilityTier.unavailable, + ), + const PlatformCapability( + feature: 'Background audio', + description: 'Supported via AVAudioSession background mode.', + tier: CapabilityTier.supported, + ), +]; List _macosCapabilities() => [ - const PlatformCapability( - feature: 'Voice capture', - description: 'cpal device enumeration; VoiceProcessingIO optional.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Voice processing', - description: 'WebRTC AEC3/NS/AGC2; VPIO available on macOS.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Global PTT hotkey', - description: 'Supported via platform-global key-binding API.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Secure storage', - description: 'macOS Keychain.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Audio device selection', - description: 'System audio output route picker.', - tier: CapabilityTier.supported, - ), - ]; + const PlatformCapability( + feature: 'Voice capture', + description: 'cpal device enumeration; VoiceProcessingIO optional.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Voice processing', + description: 'WebRTC AEC3/NS/AGC2; VPIO available on macOS.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Global PTT hotkey', + description: 'Supported via platform-global key-binding API.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Secure storage', + description: 'macOS Keychain.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Audio device selection', + description: 'System audio output route picker.', + tier: CapabilityTier.supported, + ), +]; List _windowsCapabilities() => [ - const PlatformCapability( - feature: 'Voice capture', - description: 'WASAPI via cpal.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Voice processing', - description: 'WebRTC AEC3/NS/AGC2.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Global PTT hotkey', - description: 'Supported via platform-global hotkey binding.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Secure storage', - description: 'Windows Credential Manager / DPAPI.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Installer', - description: 'MSIX packaging not yet available in beta.', - tier: CapabilityTier.unavailable, - ), - ]; + const PlatformCapability( + feature: 'Voice capture', + description: 'WASAPI via cpal.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Voice processing', + description: 'WebRTC AEC3/NS/AGC2 with Silero VAD.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Global PTT hotkey', + description: 'Supported via platform-global hotkey binding.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Secure storage', + description: 'Windows Credential Manager / DPAPI.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Installer', + description: 'MSIX packaging not yet available in beta.', + tier: CapabilityTier.unavailable, + ), +]; List _linuxCapabilities() => [ - const PlatformCapability( - feature: 'Voice capture', - description: 'PulseAudio/ALSA via cpal.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Voice processing', - description: 'WebRTC AEC3/NS/AGC2; no platform VPIO.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Global PTT hotkey', - description: 'Supported via X11/Wayland global key-binding. ', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Secure storage', - description: 'Secret Service / libsecret.', - tier: CapabilityTier.supported, - ), - const PlatformCapability( - feature: 'Desktop environment', - description: 'DE-specific behaviour: screen locker may suspend audio.', - tier: CapabilityTier.degraded, - ), - ]; + const PlatformCapability( + feature: 'Voice capture', + description: 'PulseAudio/ALSA via cpal.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Voice processing', + description: 'WebRTC AEC3/NS/AGC2 with Silero VAD; no platform VPIO.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Global PTT hotkey', + description: 'Supported via X11/Wayland global key-binding. ', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Secure storage', + description: 'Secret Service / libsecret.', + tier: CapabilityTier.supported, + ), + const PlatformCapability( + feature: 'Desktop environment', + description: 'DE-specific behaviour: screen locker may suspend audio.', + tier: CapabilityTier.degraded, + ), +]; diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 7d5f1c3..81ecdcf 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -76,12 +76,18 @@ String _kAppVersion = appSemverBaseline; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await RustLib.init(); - _kAppVersion = await resolveAppVersion(); unawaited(wireStorage()); unawaited(wireConnectivity()); wireAudioLifecycle(); await configureBundledVadModels(); runApp(const ChanoraApp()); + unawaited(_finishDeferredStartup()); +} + +Future _finishDeferredStartup() async { + try { + _kAppVersion = await resolveAppVersion(); + } catch (_) {} } class ChanoraApp extends StatelessWidget { @@ -149,7 +155,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { rust.BridgeAudioStats? _audioStats; Timer? _statsTimer; int _statsTick = 0; - bool _voiceStatusRefreshInFlight = false; + bool _snapshotRefreshInFlight = false; + bool _snapshotRefreshQueued = false; + bool _snapshotRefreshQueuedRecordActivity = false; + bool _snapshotRefreshQueuedReportErrors = false; StreamSubscription? _eventsSub; // v1 voice subsystem state (SDD-094/095/096/097). Driven by @@ -164,6 +173,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { BigInt? _currentVoiceChannelId; BigInt? _pendingVoiceChannelId; bool _canJoinVoiceChannel = true; + bool _voiceStateInitialized = false; String? _lostReason; int? _reconnectAttempt; @@ -193,13 +203,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { List _bookmarks = const []; final List _chatMessages = []; + final ValueNotifier _chatFeedRevision = ValueNotifier(0); int _chatUnread = 0; bool _chatOpen = false; final ValueNotifier> _pokeSnackBarPokes = ValueNotifier( const [], ); bool _pokeSnackBarVisible = false; - IconData? _audioRoute; // SDD-106 / SRS-209: Android RECORD_AUDIO runtime permission service. // Constructed at startup so cold-launch state is captured before the @@ -421,6 +431,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { Future _reloadBookmarks() async { try { + await wireStorage(); final list = await rust.listBookmarks(); if (!mounted) return; setState(() => _bookmarks = list); @@ -467,7 +478,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _phase = ConnectionPhase.connected; } }); - unawaited(_onRefresh()); + unawaited(_refreshSnapshot(recordActivity: true, reportErrors: true)); case rust.BridgeEvent_PttCapability( :final level, :final backendId, @@ -491,6 +502,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { :final canJoin, ): setState(() { + _voiceStateInitialized = true; _inChannel = inChannel; _transmitMode = transmitMode; _hardMute = mute; @@ -565,7 +577,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { final isPoke = target is rust.BridgeMessageTarget_Poke; final receivedAt = DateTime.now(); setState(() { - _chatMessages.add( + _appendChatEntryUnlocked( ChatEntry( senderId: senderId, senderName: senderName, @@ -575,12 +587,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { timestamp: receivedAt, ), ); - if (_chatMessages.length > 200) { - _chatMessages.removeRange(0, _chatMessages.length - 200); - } - if (!_chatOpen && !isPoke) { - _chatUnread++; - } }); if (isPoke) { _showPokeSnackBar( @@ -601,8 +607,21 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { ); }()); } - case rust.BridgeEvent_AudioRouteChanged(:final route): - setState(() => _audioRoute = _routeIcon(route)); + case rust.BridgeEvent_ServerActivity(:final message): + setState(() { + _appendChatEntryUnlocked( + ChatEntry( + senderId: BigInt.zero, + senderName: 'Server', + message: message, + target: const rust.BridgeMessageTarget.server(), + timestamp: DateTime.now(), + countsTowardUnread: false, + ), + ); + }); + case rust.BridgeEvent_AudioRouteChanged(): + break; } } @@ -617,13 +636,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { void _ensureStatsTimer() { if (_statsTimer != null) return; - _statsTimer = Timer.periodic(const Duration(milliseconds: 80), (_) async { + _statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async { try { final s = await rust.audioStats(); if (!mounted) return; setState(() => _audioStats = s); _statsTick += 1; - if (_statsTick % 5 == 0) { + if (_statsTick % 4 == 0) { unawaited(_refreshSnapshotForVoiceStatus()); } } catch (_) {} @@ -631,18 +650,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } Future _refreshSnapshotForVoiceStatus() async { - if (_voiceStatusRefreshInFlight) return; - _voiceStatusRefreshInFlight = true; - try { - final snap = await rust.snapshot(); - if (!mounted) return; - setState(() => _applySnapshot(snap)); - } catch (_) { - // Best-effort visual refresh only. Connection/loss paths still - // surface via the normal bridge events and explicit refreshes. - } finally { - _voiceStatusRefreshInFlight = false; - } + await _refreshSnapshot(recordActivity: true, reportErrors: false); } @override @@ -669,10 +677,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey); _eventsSub?.cancel(); _statsTimer?.cancel(); - _voiceStatusRefreshInFlight = false; + _snapshotRefreshInFlight = false; + _snapshotRefreshQueued = false; + _snapshotRefreshQueuedRecordActivity = false; + _snapshotRefreshQueuedReportErrors = false; _hostCtl.dispose(); _nickCtl.dispose(); _passwordCtl.dispose(); + _chatFeedRevision.dispose(); _pokeSnackBarPokes.dispose(); _androidPermissions.recordAudioState.removeListener( _onRecordAudioPermissionChanged, @@ -880,9 +892,15 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { }); } } - await configureBundledVadModels(); await rust.voiceJoin(channelId: ch.id, password: password ?? ''); if (!mounted) return; + setState(() { + _currentVoiceChannelId = ch.id; + _pendingVoiceChannelId = null; + _inChannel = true; + _canJoinVoiceChannel = true; + _voiceStateInitialized = true; + }); unawaited(_onRefresh()); } catch (e) { if (!mounted) return; @@ -959,10 +977,25 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { try { return await rust.getAudioProcessingConfig(); } catch (_) { - return defaultAudioProcessingConfig; + return defaultAudioProcessingConfig(); } } + void _appendChatEntryUnlocked(ChatEntry entry) { + _chatMessages.add(entry); + if (_chatMessages.length > 200) { + _chatMessages.removeRange(0, _chatMessages.length - 200); + } + _notifyChatFeedChanged(); + if (!_chatOpen && !entry.isPoke && entry.countsTowardUnread) { + _chatUnread++; + } + } + + void _notifyChatFeedChanged() { + _chatFeedRevision.value++; + } + /// Narrow-mode voice controls modal sheet (Plan E status chip /// trigger). On mobile this is the **single** voice-controls /// surface: route picker + inline mode radio + inline release-tail @@ -1061,13 +1094,65 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } Future _onRefresh() async { + await _refreshSnapshot(recordActivity: true, reportErrors: true); + } + + Future _refreshSnapshot({ + required bool recordActivity, + required bool reportErrors, + }) async { + if (_snapshotRefreshInFlight) { + _snapshotRefreshQueued = true; + _snapshotRefreshQueuedRecordActivity = + _snapshotRefreshQueuedRecordActivity || recordActivity; + _snapshotRefreshQueuedReportErrors = + _snapshotRefreshQueuedReportErrors || reportErrors; + return; + } + + _snapshotRefreshInFlight = true; + var nextRecordActivity = recordActivity; + var nextReportErrors = reportErrors; + try { - final snap = await rust.snapshot(); - if (!mounted) return; - setState(() => _applySnapshot(snap)); - } catch (e) { - if (!mounted) return; - _showUiError('refresh snapshot', e); + while (mounted) { + _snapshotRefreshQueued = false; + _snapshotRefreshQueuedRecordActivity = false; + _snapshotRefreshQueuedReportErrors = false; + + final previousSnapshot = _snapshot; + try { + final snap = await rust.snapshot(); + if (!mounted) return; + final activityEntries = nextRecordActivity && previousSnapshot != null + ? buildServerActivityEntries( + previous: previousSnapshot, + current: snap, + ) + : const []; + setState(() { + _applySnapshot(snap); + for (final entry in activityEntries) { + _appendChatEntryUnlocked(entry); + } + }); + } catch (e) { + if (nextReportErrors && mounted) { + _showUiError('refresh snapshot', e); + } + } + + if (!_snapshotRefreshQueued) { + break; + } + nextRecordActivity = _snapshotRefreshQueuedRecordActivity; + nextReportErrors = _snapshotRefreshQueuedReportErrors; + } + } finally { + _snapshotRefreshInFlight = false; + _snapshotRefreshQueued = false; + _snapshotRefreshQueuedRecordActivity = false; + _snapshotRefreshQueuedReportErrors = false; } } @@ -1109,7 +1194,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _statsTimer?.cancel(); _statsTimer = null; _statsTick = 0; - _voiceStatusRefreshInFlight = false; + _snapshotRefreshInFlight = false; + _snapshotRefreshQueued = false; + _snapshotRefreshQueuedRecordActivity = false; + _snapshotRefreshQueuedReportErrors = false; try { await rust.disconnect(); } catch (_) {} @@ -1131,34 +1219,21 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _currentVoiceChannelId = null; _pendingVoiceChannelId = null; _canJoinVoiceChannel = true; + _voiceStateInitialized = false; _lostReason = null; _reconnectAttempt = null; _reconnectDelay = null; _chatMessages.clear(); _chatUnread = 0; _chatOpen = false; - } - - IconData _routeIcon(rust.BridgeAudioRoute r) { - switch (r) { - case rust.BridgeAudioRoute.earpiece: - return Icons.phone_android; - case rust.BridgeAudioRoute.speaker: - return Icons.volume_up; - case rust.BridgeAudioRoute.wiredHeadset: - return Icons.headset; - case rust.BridgeAudioRoute.bluetoothHfp: - case rust.BridgeAudioRoute.bluetoothA2Dp: - return Icons.bluetooth; - case rust.BridgeAudioRoute.unknown: - return Icons.help_outline; - } + _notifyChatFeedChanged(); } Future _onOpenChat({ rust.BridgeMessageTarget? target, String clientName = '', }) async { + final initialSnapshot = _snapshot!; setState(() { _chatUnread = 0; _chatOpen = true; @@ -1167,7 +1242,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { MaterialPageRoute( builder: (_) => ChatPage( messages: _chatMessages, - snapshot: _snapshot!, + snapshot: initialSnapshot, + messagesSource: () => _chatMessages, + snapshotSource: () => _snapshot ?? initialSnapshot, + refreshListenable: _chatFeedRevision, initialTarget: target ?? resolveInitialChatTarget( @@ -1312,12 +1390,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _snapshot = snap; final own = ownClientSnapshotState(snap); if (own == null) return; - _currentVoiceChannelId = own.channelId; _inputMuted = own.inputMuted; _outputMuted = own.outputMuted; - _pendingVoiceChannelId = null; - _inChannel = true; - _canJoinVoiceChannel = true; + if (!_voiceStateInitialized || _currentVoiceChannelId == null) { + _currentVoiceChannelId = own.channelId; + _pendingVoiceChannelId = null; + _inChannel = true; + _canJoinVoiceChannel = true; + _voiceStateInitialized = true; + } else if (_pendingVoiceChannelId == null) { + _currentVoiceChannelId = own.channelId; + } + _notifyChatFeedChanged(); if (!own.talkPowerOk && !_hardMuteByTalkPower) { _hardMuteByTalkPower = true; @@ -1525,6 +1609,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (name == null || name.trim().isEmpty) return; if (!mounted) return; try { + await wireStorage(); await rust.addBookmark( b: rust.BridgeBookmark( id: 0, @@ -1543,6 +1628,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { Future _onDeleteBookmark(rust.BridgeBookmark b) async { try { + await wireStorage(); await rust.deleteBookmark(id: b.id); await _reloadBookmarks(); } catch (e) { @@ -1580,6 +1666,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } try { + await wireStorage(); await rust.addBookmark( b: rust.BridgeBookmark( id: 0, @@ -1652,7 +1739,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { final appBarTitle = _serverReachable ? Text( _snapshot?.serverName ?? l10n.appTitle, - style: theme.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), ) : headerTitle; @@ -1701,10 +1792,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { ), ], ), - if (_serverReachable && _audioRoute != null) ...[ - const SizedBox(width: 8), - Icon(_audioRoute, size: 16, color: theme.colorScheme.tertiary), - ], if (_lostReason != null || _reconnectAttempt != null) ...[ const SizedBox(height: 8), Container( @@ -1925,6 +2012,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { onPressed: _onConfirmDisconnect, ) : null, + leadingWidth: _phase.canDisconnect ? 44 : null, + titleSpacing: _phase.canDisconnect ? 4 : null, title: appBarTitle, actions: headerActions, ), diff --git a/apps/chanora_flutter/lib/src/rust/api.dart b/apps/chanora_flutter/lib/src/rust/api.dart index 5de105f..8160f52 100644 --- a/apps/chanora_flutter/lib/src/rust/api.dart +++ b/apps/chanora_flutter/lib/src/rust/api.dart @@ -270,23 +270,19 @@ Future audioProcessingStats() => Future listAudioDevices() => RustLib.instance.api.crateApiListAudioDevices(); -/// Set the preferred input device by name. Takes effect on next +/// Set the preferred input device by id. Takes effect on next /// `start_audio`. -Future setInputDevice({String? name}) => - RustLib.instance.api.crateApiSetInputDevice(name: name); +Future setInputDevice({String? id}) => + RustLib.instance.api.crateApiSetInputDevice(id: id); -/// Set the preferred output device by name. -Future setOutputDevice({String? name}) => - RustLib.instance.api.crateApiSetOutputDevice(name: name); +/// Set the preferred output device by id. +Future setOutputDevice({String? id}) => + RustLib.instance.api.crateApiSetOutputDevice(id: id); /// Configure the VAD model path. Future setVadModelPath({required String path}) => RustLib.instance.api.crateApiSetVadModelPath(path: path); -/// Configure the TEN VAD ONNX model path. -Future setTenVadModelPath({required String path}) => - RustLib.instance.api.crateApiSetTenVadModelPath(path: path); - /// Enable or disable audio debug WAV dumping. Future enableAudioDebugWavDump({required bool enabled}) => RustLib.instance.api.crateApiEnableAudioDebugWavDump(enabled: enabled); @@ -321,24 +317,47 @@ enum BridgeAudioBackend { /// Audio device info from the platform. class BridgeAudioDevice { + /// Stable platform-reported device identifier. + final String id; + /// Human-readable device name. final String name; + /// Additional device details useful for disambiguation. + final String details; + /// True if the OS reports this as the default device. final bool isDefault; - const BridgeAudioDevice({required this.name, required this.isDefault}); + /// True if Chanora currently has this device pinned. + final bool isSelected; + + const BridgeAudioDevice({ + required this.id, + required this.name, + required this.details, + required this.isDefault, + required this.isSelected, + }); @override - int get hashCode => name.hashCode ^ isDefault.hashCode; + int get hashCode => + id.hashCode ^ + name.hashCode ^ + details.hashCode ^ + isDefault.hashCode ^ + isSelected.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is BridgeAudioDevice && runtimeType == other.runtimeType && + id == other.id && name == other.name && - isDefault == other.isDefault; + details == other.details && + isDefault == other.isDefault && + isSelected == other.isSelected; } /// List of available audio devices. @@ -512,6 +531,21 @@ class BridgeAudioProcessingStats { /// Clipped samples. final BigInt clippedSamples; + /// Number of effectively silent processed capture frames. + final BigInt zeroFrames; + + /// Number of processed capture frames. + final BigInt captureFrames; + + /// Input callbacks carrying 10 ms of audio. + final BigInt callbacks10Ms; + + /// Input callbacks carrying 20 ms of audio. + final BigInt callbacks20Ms; + + /// Input callbacks carrying other sizes. + final BigInt callbacksOther; + /// Sonora enabled. final bool sonoraEnabled; @@ -536,6 +570,11 @@ class BridgeAudioProcessingStats { required this.outputUnderruns, required this.callbackXruns, required this.clippedSamples, + required this.zeroFrames, + required this.captureFrames, + required this.callbacks10Ms, + required this.callbacks20Ms, + required this.callbacksOther, required this.sonoraEnabled, required this.platformVoiceProcessingEnabled, }); @@ -559,6 +598,11 @@ class BridgeAudioProcessingStats { outputUnderruns.hashCode ^ callbackXruns.hashCode ^ clippedSamples.hashCode ^ + zeroFrames.hashCode ^ + captureFrames.hashCode ^ + callbacks10Ms.hashCode ^ + callbacks20Ms.hashCode ^ + callbacksOther.hashCode ^ sonoraEnabled.hashCode ^ platformVoiceProcessingEnabled.hashCode; @@ -584,6 +628,11 @@ class BridgeAudioProcessingStats { outputUnderruns == other.outputUnderruns && callbackXruns == other.callbackXruns && clippedSamples == other.clippedSamples && + zeroFrames == other.zeroFrames && + captureFrames == other.captureFrames && + callbacks10Ms == other.callbacks10Ms && + callbacks20Ms == other.callbacks20Ms && + callbacksOther == other.callbacksOther && sonoraEnabled == other.sonoraEnabled && platformVoiceProcessingEnabled == other.platformVoiceProcessingEnabled; @@ -976,6 +1025,12 @@ sealed class BridgeEvent with _$BridgeEvent { required BridgeMessageTarget target, }) = BridgeEvent_ChatMessage; + /// Human-readable server activity surfaced from protocol bookkeeping events. + const factory BridgeEvent.serverActivity({ + /// TeamSpeak-style activity line. + required String message, + }) = BridgeEvent_ServerActivity; + /// Audio route changed (speaker/earpiece/BT/wired). const factory BridgeEvent.audioRouteChanged({ /// The new audio route. @@ -1169,9 +1224,6 @@ enum BridgeVadBackend { /// Silero ONNX VAD. sileroOnnx, - /// TEN VAD. - tenVad, - /// WebRTC fallback VAD. webrtcVad, diff --git a/apps/chanora_flutter/lib/src/rust/api.freezed.dart b/apps/chanora_flutter/lib/src/rust/api.freezed.dart index bf123b7..0735ae0 100644 --- a/apps/chanora_flutter/lib/src/rust/api.freezed.dart +++ b/apps/chanora_flutter/lib/src/rust/api.freezed.dart @@ -55,7 +55,7 @@ extension BridgeEventPatterns on BridgeEvent { /// } /// ``` -@optionalTypeArgs TResult maybeMap({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,TResult Function( BridgeEvent_InterruptionState value)? interruptionState,TResult Function( BridgeEvent_PermissionState value)? permissionState,TResult Function( BridgeEvent_ChatMessage value)? chatMessage,TResult Function( BridgeEvent_AudioRouteChanged value)? audioRouteChanged,required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,TResult Function( BridgeEvent_InterruptionState value)? interruptionState,TResult Function( BridgeEvent_PermissionState value)? permissionState,TResult Function( BridgeEvent_ChatMessage value)? chatMessage,TResult Function( BridgeEvent_ServerActivity value)? serverActivity,TResult Function( BridgeEvent_AudioRouteChanged value)? audioRouteChanged,required TResult orElse(),}){ final _that = this; switch (_that) { case BridgeEvent_Connected() when connected != null: @@ -70,7 +70,8 @@ return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != nul return voiceState(_that);case BridgeEvent_InterruptionState() when interruptionState != null: return interruptionState(_that);case BridgeEvent_PermissionState() when permissionState != null: return permissionState(_that);case BridgeEvent_ChatMessage() when chatMessage != null: -return chatMessage(_that);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: +return chatMessage(_that);case BridgeEvent_ServerActivity() when serverActivity != null: +return serverActivity(_that);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return audioRouteChanged(_that);case _: return orElse(); @@ -89,7 +90,7 @@ return audioRouteChanged(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,required TResult Function( BridgeEvent_InterruptionState value) interruptionState,required TResult Function( BridgeEvent_PermissionState value) permissionState,required TResult Function( BridgeEvent_ChatMessage value) chatMessage,required TResult Function( BridgeEvent_AudioRouteChanged value) audioRouteChanged,}){ +@optionalTypeArgs TResult map({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,required TResult Function( BridgeEvent_InterruptionState value) interruptionState,required TResult Function( BridgeEvent_PermissionState value) permissionState,required TResult Function( BridgeEvent_ChatMessage value) chatMessage,required TResult Function( BridgeEvent_ServerActivity value) serverActivity,required TResult Function( BridgeEvent_AudioRouteChanged value) audioRouteChanged,}){ final _that = this; switch (_that) { case BridgeEvent_Connected(): @@ -104,7 +105,8 @@ return pttCapability(_that);case BridgeEvent_VoiceState(): return voiceState(_that);case BridgeEvent_InterruptionState(): return interruptionState(_that);case BridgeEvent_PermissionState(): return permissionState(_that);case BridgeEvent_ChatMessage(): -return chatMessage(_that);case BridgeEvent_AudioRouteChanged(): +return chatMessage(_that);case BridgeEvent_ServerActivity(): +return serverActivity(_that);case BridgeEvent_AudioRouteChanged(): return audioRouteChanged(_that);} } /// A variant of `map` that fallback to returning `null`. @@ -119,7 +121,7 @@ return audioRouteChanged(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,TResult? Function( BridgeEvent_InterruptionState value)? interruptionState,TResult? Function( BridgeEvent_PermissionState value)? permissionState,TResult? Function( BridgeEvent_ChatMessage value)? chatMessage,TResult? Function( BridgeEvent_AudioRouteChanged value)? audioRouteChanged,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,TResult? Function( BridgeEvent_InterruptionState value)? interruptionState,TResult? Function( BridgeEvent_PermissionState value)? permissionState,TResult? Function( BridgeEvent_ChatMessage value)? chatMessage,TResult? Function( BridgeEvent_ServerActivity value)? serverActivity,TResult? Function( BridgeEvent_AudioRouteChanged value)? audioRouteChanged,}){ final _that = this; switch (_that) { case BridgeEvent_Connected() when connected != null: @@ -134,7 +136,8 @@ return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != nul return voiceState(_that);case BridgeEvent_InterruptionState() when interruptionState != null: return interruptionState(_that);case BridgeEvent_PermissionState() when permissionState != null: return permissionState(_that);case BridgeEvent_ChatMessage() when chatMessage != null: -return chatMessage(_that);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: +return chatMessage(_that);case BridgeEvent_ServerActivity() when serverActivity != null: +return serverActivity(_that);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return audioRouteChanged(_that);case _: return null; @@ -152,7 +155,7 @@ return audioRouteChanged(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult Function( BridgeAudioRoute route)? audioRouteChanged,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult Function( String message)? serverActivity,TResult Function( BridgeAudioRoute route)? audioRouteChanged,required TResult orElse(),}) {final _that = this; switch (_that) { case BridgeEvent_Connected() when connected != null: return connected(_that.serverName);case BridgeEvent_Lost() when lost != null: @@ -166,7 +169,8 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null: return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null: return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null: -return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: +return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null: +return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return audioRouteChanged(_that.route);case _: return orElse(); @@ -185,7 +189,7 @@ return audioRouteChanged(_that.route);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target) chatMessage,required TResult Function( BridgeAudioRoute route) audioRouteChanged,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target) chatMessage,required TResult Function( String message) serverActivity,required TResult Function( BridgeAudioRoute route) audioRouteChanged,}) {final _that = this; switch (_that) { case BridgeEvent_Connected(): return connected(_that.serverName);case BridgeEvent_Lost(): @@ -199,7 +203,8 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState(): return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState(): return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage(): -return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_AudioRouteChanged(): +return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity(): +return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged(): return audioRouteChanged(_that.route);} } /// A variant of `when` that fallback to returning `null` @@ -214,7 +219,7 @@ return audioRouteChanged(_that.route);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult? Function( String message)? serverActivity,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,}) {final _that = this; switch (_that) { case BridgeEvent_Connected() when connected != null: return connected(_that.serverName);case BridgeEvent_Lost() when lost != null: @@ -228,7 +233,8 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null: return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null: return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null: -return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: +return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null: +return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return audioRouteChanged(_that.route);case _: return null; @@ -1041,6 +1047,73 @@ $BridgeMessageTargetCopyWith<$Res> get target { /// @nodoc +class BridgeEvent_ServerActivity extends BridgeEvent { + const BridgeEvent_ServerActivity({required this.message}): super._(); + + +/// TeamSpeak-style activity line. + final String message; + +/// Create a copy of BridgeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BridgeEvent_ServerActivityCopyWith get copyWith => _$BridgeEvent_ServerActivityCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_ServerActivity&&(identical(other.message, message) || other.message == message)); +} + + +@override +int get hashCode => Object.hash(runtimeType,message); + +@override +String toString() { + return 'BridgeEvent.serverActivity(message: $message)'; +} + + +} + +/// @nodoc +abstract mixin class $BridgeEvent_ServerActivityCopyWith<$Res> implements $BridgeEventCopyWith<$Res> { + factory $BridgeEvent_ServerActivityCopyWith(BridgeEvent_ServerActivity value, $Res Function(BridgeEvent_ServerActivity) _then) = _$BridgeEvent_ServerActivityCopyWithImpl; +@useResult +$Res call({ + String message +}); + + + + +} +/// @nodoc +class _$BridgeEvent_ServerActivityCopyWithImpl<$Res> + implements $BridgeEvent_ServerActivityCopyWith<$Res> { + _$BridgeEvent_ServerActivityCopyWithImpl(this._self, this._then); + + final BridgeEvent_ServerActivity _self; + final $Res Function(BridgeEvent_ServerActivity) _then; + +/// Create a copy of BridgeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? message = null,}) { + return _then(BridgeEvent_ServerActivity( +message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + class BridgeEvent_AudioRouteChanged extends BridgeEvent { const BridgeEvent_AudioRouteChanged({required this.route}): super._(); diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.dart index 130f739..894c8fd 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.dart @@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 1433826599; + int get rustContentHash => -560177922; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -156,7 +156,7 @@ abstract class RustLibApi extends BaseApi { Future crateApiSetHardMute({required bool muted}); - Future crateApiSetInputDevice({String? name}); + Future crateApiSetInputDevice({String? id}); Future crateApiSetInputMuted({required bool muted}); @@ -166,7 +166,7 @@ abstract class RustLibApi extends BaseApi { void crateApiSetNetworkState({required BridgeNetworkState state}); - Future crateApiSetOutputDevice({String? name}); + Future crateApiSetOutputDevice({String? id}); Future crateApiSetOutputGain({required double gain}); @@ -181,8 +181,6 @@ abstract class RustLibApi extends BaseApi { Future crateApiSetReleaseTailMs({required int ms}); - Future crateApiSetTenVadModelPath({required String path}); - Future crateApiSetTransmitMode({required BridgeTransmitMode mode}); Future crateApiSetVadModelPath({required String path}); @@ -772,7 +770,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }, codec: SseCodec( decodeSuccessData: sse_decode_bridge_audio_device_list, - decodeErrorData: null, + decodeErrorData: sse_decode_bridge_error, ), constMeta: kCrateApiListAudioDevicesConstMeta, argValues: [], @@ -1079,12 +1077,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "set_hard_mute", argNames: ["muted"]); @override - Future crateApiSetInputDevice({String? name}) { + Future crateApiSetInputDevice({String? id}) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_opt_String(name, serializer); + sse_encode_opt_String(id, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -1097,14 +1095,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_bridge_error, ), constMeta: kCrateApiSetInputDeviceConstMeta, - argValues: [name], + argValues: [id], apiImpl: this, ), ); } TaskConstMeta get kCrateApiSetInputDeviceConstMeta => - const TaskConstMeta(debugName: "set_input_device", argNames: ["name"]); + const TaskConstMeta(debugName: "set_input_device", argNames: ["id"]); @override Future crateApiSetInputMuted({required bool muted}) { @@ -1191,12 +1189,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "set_network_state", argNames: ["state"]); @override - Future crateApiSetOutputDevice({String? name}) { + Future crateApiSetOutputDevice({String? id}) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_opt_String(name, serializer); + sse_encode_opt_String(id, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -1209,14 +1207,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_bridge_error, ), constMeta: kCrateApiSetOutputDeviceConstMeta, - argValues: [name], + argValues: [id], apiImpl: this, ), ); } TaskConstMeta get kCrateApiSetOutputDeviceConstMeta => - const TaskConstMeta(debugName: "set_output_device", argNames: ["name"]); + const TaskConstMeta(debugName: "set_output_device", argNames: ["id"]); @override Future crateApiSetOutputGain({required double gain}) { @@ -1364,36 +1362,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiSetReleaseTailMsConstMeta => const TaskConstMeta(debugName: "set_release_tail_ms", argNames: ["ms"]); - @override - Future crateApiSetTenVadModelPath({required String path}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(path, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 42, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_bridge_error, - ), - constMeta: kCrateApiSetTenVadModelPathConstMeta, - argValues: [path], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiSetTenVadModelPathConstMeta => const TaskConstMeta( - debugName: "set_ten_vad_model_path", - argNames: ["path"], - ); - @override Future crateApiSetTransmitMode({required BridgeTransmitMode mode}) { return handler.executeNormal( @@ -1404,7 +1372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 42, port: port_, ); }, @@ -1432,7 +1400,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 43, port: port_, ); }, @@ -1459,7 +1427,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 44, port: port_, ); }, @@ -1487,7 +1455,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 45, port: port_, ); }, @@ -1519,7 +1487,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 46, port: port_, ); }, @@ -1548,7 +1516,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 47, port: port_, ); }, @@ -1643,11 +1611,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { BridgeAudioDevice dco_decode_bridge_audio_device(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 2) - throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return BridgeAudioDevice( - name: dco_decode_String(arr[0]), - isDefault: dco_decode_bool(arr[1]), + id: dco_decode_String(arr[0]), + name: dco_decode_String(arr[1]), + details: dco_decode_String(arr[2]), + isDefault: dco_decode_bool(arr[3]), + isSelected: dco_decode_bool(arr[4]), ); } @@ -1694,8 +1665,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 19) - throw Exception('unexpected arr length: expect 19 but see ${arr.length}'); + if (arr.length != 24) + throw Exception('unexpected arr length: expect 24 but see ${arr.length}'); return BridgeAudioProcessingStats( inputDbfs: dco_decode_f_32(arr[0]), renderDbfs: dco_decode_f_32(arr[1]), @@ -1716,8 +1687,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { outputUnderruns: dco_decode_u_64(arr[14]), callbackXruns: dco_decode_u_64(arr[15]), clippedSamples: dco_decode_u_64(arr[16]), - sonoraEnabled: dco_decode_bool(arr[17]), - platformVoiceProcessingEnabled: dco_decode_bool(arr[18]), + zeroFrames: dco_decode_u_64(arr[17]), + captureFrames: dco_decode_u_64(arr[18]), + callbacks10Ms: dco_decode_u_64(arr[19]), + callbacks20Ms: dco_decode_u_64(arr[20]), + callbacksOther: dco_decode_u_64(arr[21]), + sonoraEnabled: dco_decode_bool(arr[22]), + platformVoiceProcessingEnabled: dco_decode_bool(arr[23]), ); } @@ -1887,6 +1863,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { target: dco_decode_box_autoadd_bridge_message_target(raw[4]), ); case 12: + return BridgeEvent_ServerActivity(message: dco_decode_String(raw[1])); + case 13: return BridgeEvent_AudioRouteChanged( route: dco_decode_bridge_audio_route(raw[1]), ); @@ -2194,9 +2172,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_String(deserializer); var var_name = sse_decode_String(deserializer); + var var_details = sse_decode_String(deserializer); var var_isDefault = sse_decode_bool(deserializer); - return BridgeAudioDevice(name: var_name, isDefault: var_isDefault); + var var_isSelected = sse_decode_bool(deserializer); + return BridgeAudioDevice( + id: var_id, + name: var_name, + details: var_details, + isDefault: var_isDefault, + isSelected: var_isSelected, + ); } @protected @@ -2270,6 +2257,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_outputUnderruns = sse_decode_u_64(deserializer); var var_callbackXruns = sse_decode_u_64(deserializer); var var_clippedSamples = sse_decode_u_64(deserializer); + var var_zeroFrames = sse_decode_u_64(deserializer); + var var_captureFrames = sse_decode_u_64(deserializer); + var var_callbacks10Ms = sse_decode_u_64(deserializer); + var var_callbacks20Ms = sse_decode_u_64(deserializer); + var var_callbacksOther = sse_decode_u_64(deserializer); var var_sonoraEnabled = sse_decode_bool(deserializer); var var_platformVoiceProcessingEnabled = sse_decode_bool(deserializer); return BridgeAudioProcessingStats( @@ -2290,6 +2282,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { outputUnderruns: var_outputUnderruns, callbackXruns: var_callbackXruns, clippedSamples: var_clippedSamples, + zeroFrames: var_zeroFrames, + captureFrames: var_captureFrames, + callbacks10Ms: var_callbacks10Ms, + callbacks20Ms: var_callbacks20Ms, + callbacksOther: var_callbacksOther, sonoraEnabled: var_sonoraEnabled, platformVoiceProcessingEnabled: var_platformVoiceProcessingEnabled, ); @@ -2519,6 +2516,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { target: var_target, ); case 12: + var var_message = sse_decode_String(deserializer); + return BridgeEvent_ServerActivity(message: var_message); + case 13: var var_route = sse_decode_bridge_audio_route(deserializer); return BridgeEvent_AudioRouteChanged(route: var_route); default: @@ -2917,8 +2917,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.id, serializer); sse_encode_String(self.name, serializer); + sse_encode_String(self.details, serializer); sse_encode_bool(self.isDefault, serializer); + sse_encode_bool(self.isSelected, serializer); } @protected @@ -2978,6 +2981,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self.outputUnderruns, serializer); sse_encode_u_64(self.callbackXruns, serializer); sse_encode_u_64(self.clippedSamples, serializer); + sse_encode_u_64(self.zeroFrames, serializer); + sse_encode_u_64(self.captureFrames, serializer); + sse_encode_u_64(self.callbacks10Ms, serializer); + sse_encode_u_64(self.callbacks20Ms, serializer); + sse_encode_u_64(self.callbacksOther, serializer); sse_encode_bool(self.sonoraEnabled, serializer); sse_encode_bool(self.platformVoiceProcessingEnabled, serializer); } @@ -3168,8 +3176,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(senderName, serializer); sse_encode_String(message, serializer); sse_encode_box_autoadd_bridge_message_target(target, serializer); - case BridgeEvent_AudioRouteChanged(route: final route): + case BridgeEvent_ServerActivity(message: final message): sse_encode_i_32(12, serializer); + sse_encode_String(message, serializer); + case BridgeEvent_AudioRouteChanged(route: final route): + sse_encode_i_32(13, serializer); sse_encode_bridge_audio_route(route, serializer); } } diff --git a/apps/chanora_flutter/lib/widgets/audio_debug_stats_panel.dart b/apps/chanora_flutter/lib/widgets/audio_debug_stats_panel.dart index a0ebc7b..1a9fd5a 100644 --- a/apps/chanora_flutter/lib/widgets/audio_debug_stats_panel.dart +++ b/apps/chanora_flutter/lib/widgets/audio_debug_stats_panel.dart @@ -108,7 +108,8 @@ class _AudioDebugStatsPanelState extends State { s.platformVoiceProcessingEnabled ? 'on' : 'off', Colors.white, ), - _row('sonora', s.sonoraEnabled ? 'on' : 'off', Colors.white), + if (s.processingBackend == BridgeAudioBackend.sonora) + _row('sonora', s.sonoraEnabled ? 'on' : 'off', Colors.white), const SizedBox(height: 4), _row( 'mic in', @@ -184,7 +185,6 @@ class _AudioDebugStatsPanelState extends State { String _vadBackendLabel(BridgeVadBackend backend) => switch (backend) { BridgeVadBackend.webrtcVad => 'webrtc', BridgeVadBackend.sileroOnnx => 'silero', - BridgeVadBackend.tenVad => 'ten', BridgeVadBackend.energyDebug => 'energy', BridgeVadBackend.disabled => 'off', }; diff --git a/apps/chanora_flutter/lib/widgets/audio_device_list_tile.dart b/apps/chanora_flutter/lib/widgets/audio_device_list_tile.dart index 255d7aa..70497a5 100644 --- a/apps/chanora_flutter/lib/widgets/audio_device_list_tile.dart +++ b/apps/chanora_flutter/lib/widgets/audio_device_list_tile.dart @@ -5,8 +5,8 @@ import '../src/rust/api.dart' as rust; /// Loads available input/output audio devices. typedef AudioDeviceListLoader = Future Function(); -/// Persists a selected audio device name. -typedef AudioDeviceSetter = Future Function({String? name}); +/// Persists a selected audio device id. +typedef AudioDeviceSetter = Future Function({String? id}); /// Which desktop audio device group this tile manages. enum AudioDeviceKind { @@ -52,6 +52,7 @@ class AudioDeviceListTile extends StatefulWidget { class _AudioDeviceListTileState extends State { List _devices = []; + String? _selectedDeviceId; bool _loaded = false; @override @@ -68,28 +69,56 @@ class _AudioDeviceListTileState extends State { AudioDeviceKind.input => list.inputDevices, AudioDeviceKind.output => list.outputDevices, }; + _selectedDeviceId = _devices + .where((device) => device.isSelected) + .firstOrNull + ?.id; _loaded = true; }); } - Future _selectDevice(rust.BridgeAudioDevice device) async { + Future _selectDevice(String? deviceId) async { switch (widget.kind) { case AudioDeviceKind.input: - await widget.setInputDevice(name: device.name); + await widget.setInputDevice(id: deviceId); case AudioDeviceKind.output: - await widget.setOutputDevice(name: device.name); + await widget.setOutputDevice(id: deviceId); } if (!mounted) return; + setState(() { + _selectedDeviceId = deviceId; + }); + + final selectedName = _selectedDevice?.name ?? 'System default'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${widget.label} set to ${device.name}'), + content: Text('${widget.label} set to $selectedName'), duration: const Duration(seconds: 2), ), ); } + rust.BridgeAudioDevice? get _selectedDevice { + if (_selectedDeviceId == null) { + return null; + } + for (final device in _devices) { + if (device.id == _selectedDeviceId) { + return device; + } + } + return null; + } + @override Widget build(BuildContext context) { + final selectedDevice = _selectedDevice; + final subtitleText = selectedDevice?.name ?? 'System default'; + final leadingIcon = switch (widget.kind) { + AudioDeviceKind.input => Icons.mic_none, + AudioDeviceKind.output => Icons.headphones, + }; + if (!_loaded) { return ListTile( title: Text(widget.label), @@ -110,17 +139,31 @@ class _AudioDeviceListTileState extends State { } return ExpansionTile( title: Text(widget.label), - subtitle: Text('${_devices.length} available'), - leading: const Icon(Icons.headphones, size: 18), + subtitle: Text(subtitleText), + leading: Icon(leadingIcon, size: 18), children: [ + ListTile( + dense: true, + title: const Text('System default', style: TextStyle(fontSize: 13)), + subtitle: const Text('Use the OS default device'), + trailing: _selectedDeviceId == null + ? const Icon(Icons.check, size: 16, color: Colors.green) + : null, + onTap: _selectedDeviceId == null ? null : () => _selectDevice(null), + ), for (final device in _devices) ListTile( dense: true, title: Text(device.name, style: const TextStyle(fontSize: 13)), - trailing: device.isDefault + subtitle: device.details.isEmpty ? null : Text(device.details), + trailing: device.id == _selectedDeviceId ? const Icon(Icons.check, size: 16, color: Colors.green) + : device.isDefault + ? const Icon(Icons.radio_button_checked, size: 16) : null, - onTap: device.isDefault ? null : () => _selectDevice(device), + onTap: device.id == _selectedDeviceId + ? null + : () => _selectDevice(device.id), ), ], ); diff --git a/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart b/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart index 53eb4b3..1ed4a77 100644 --- a/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart +++ b/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart @@ -3,21 +3,32 @@ import 'dart:io' show Platform; import '../src/rust/api.dart' as rust; /// Fallback audio-processing config used before the bridge can report one. -const defaultAudioProcessingConfig = rust.BridgeAudioProcessingConfig( - route: rust.BridgeAudioRoute.unknown, - iosMode: rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, - processingBackend: rust.BridgeAudioBackend.platformVoiceProcessing, - vadBackend: rust.BridgeVadBackend.sileroOnnx, - aec: rust.BridgeEffectOwner.platform, - ns: rust.BridgeEffectOwner.platform, - agc: rust.BridgeEffectOwner.platform, - hpfEnabled: true, - limiterEnabled: true, - vadHangoverMs: 500, - vadPreRollMs: 160, - vadMinTxMs: 200, - debugWavDumpEnabled: false, -); +rust.BridgeAudioProcessingConfig defaultAudioProcessingConfig() { + final desktopWebrtcApm = Platform.isWindows || Platform.isLinux; + return rust.BridgeAudioProcessingConfig( + route: rust.BridgeAudioRoute.unknown, + iosMode: rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, + processingBackend: desktopWebrtcApm + ? rust.BridgeAudioBackend.webrtcApm + : rust.BridgeAudioBackend.platformVoiceProcessing, + vadBackend: rust.BridgeVadBackend.sileroOnnx, + aec: desktopWebrtcApm + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + ns: desktopWebrtcApm + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + agc: desktopWebrtcApm + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + hpfEnabled: true, + limiterEnabled: true, + vadHangoverMs: 500, + vadPreRollMs: 160, + vadMinTxMs: 200, + debugWavDumpEnabled: false, + ); +} /// Mutable UI state for audio-processing controls. class AudioProcessingConfigState { @@ -31,7 +42,7 @@ class AudioProcessingConfigState { debugWavDump = config.debugWavDumpEnabled, preferHardware = _usesPlatformEffects(config), vadBackend = normalizedVadBackend(config.vadBackend), - iosMode = config.iosMode; + iosMode = normalizedIosProcessingMode(config.iosMode); /// Noise suppression toggle. bool nsEnabled; @@ -64,9 +75,23 @@ class AudioProcessingConfigState { rust.BridgeAudioProcessingConfig buildConfig({ required rust.BridgeAudioProcessingConfig base, bool? isAndroid, + bool? isIos, + bool? isMacOS, + bool? isWindows, + bool? isLinux, }) { final android = isAndroid ?? Platform.isAndroid; - final vad = normalizedVadBackend(vadBackend); + final ios = isIos ?? Platform.isIOS; + final macOS = isMacOS ?? Platform.isMacOS; + final windows = isWindows ?? Platform.isWindows; + final linux = isLinux ?? Platform.isLinux; + final appleVoiceProcessing = ios || macOS; + final desktopWebrtcApm = windows || linux; + final vad = normalizedVadBackend( + vadBackend, + isWindows: windows, + isLinux: linux, + ); if (android) { final owner = preferHardware @@ -74,7 +99,7 @@ class AudioProcessingConfigState { : rust.BridgeEffectOwner.webrtcApm; return rust.BridgeAudioProcessingConfig( route: base.route, - iosMode: iosMode, + iosMode: normalizedIosProcessingMode(iosMode), processingBackend: preferHardware ? rust.BridgeAudioBackend.platformVoiceProcessing : rust.BridgeAudioBackend.webrtcApm, @@ -91,38 +116,55 @@ class AudioProcessingConfigState { ); } - final isSonora = - iosMode == rust.BridgeIosVoiceProcessingMode.sonoraExperimental; - final aecOwner = isSonora - ? (aecEnabled - ? rust.BridgeEffectOwner.webrtcApm - : rust.BridgeEffectOwner.off) - : rust.BridgeEffectOwner.platform; - final nsOwner = isSonora - ? (nsEnabled - ? rust.BridgeEffectOwner.webrtcApm - : rust.BridgeEffectOwner.off) - : (nsEnabled - ? rust.BridgeEffectOwner.platform - : rust.BridgeEffectOwner.off); - final agcOwner = isSonora - ? (agcEnabled - ? rust.BridgeEffectOwner.webrtcApm - : rust.BridgeEffectOwner.off) - : (agcEnabled - ? rust.BridgeEffectOwner.platform - : rust.BridgeEffectOwner.off); + if (appleVoiceProcessing) { + return rust.BridgeAudioProcessingConfig( + route: base.route, + iosMode: normalizedIosProcessingMode(iosMode), + processingBackend: rust.BridgeAudioBackend.platformVoiceProcessing, + vadBackend: vad, + aec: rust.BridgeEffectOwner.platform, + ns: rust.BridgeEffectOwner.platform, + agc: rust.BridgeEffectOwner.platform, + hpfEnabled: hpfEnabled, + limiterEnabled: limiterEnabled, + vadHangoverMs: base.vadHangoverMs, + vadPreRollMs: base.vadPreRollMs, + vadMinTxMs: base.vadMinTxMs, + debugWavDumpEnabled: debugWavDump, + ); + } + + if (desktopWebrtcApm) { + final owner = rust.BridgeEffectOwner.webrtcApm; + return rust.BridgeAudioProcessingConfig( + route: base.route, + iosMode: normalizedIosProcessingMode(iosMode), + processingBackend: rust.BridgeAudioBackend.webrtcApm, + vadBackend: vad, + aec: aecEnabled ? owner : rust.BridgeEffectOwner.off, + ns: nsEnabled ? owner : rust.BridgeEffectOwner.off, + agc: agcEnabled ? owner : rust.BridgeEffectOwner.off, + hpfEnabled: hpfEnabled, + limiterEnabled: limiterEnabled, + vadHangoverMs: base.vadHangoverMs, + vadPreRollMs: base.vadPreRollMs, + vadMinTxMs: base.vadMinTxMs, + debugWavDumpEnabled: debugWavDump, + ); + } return rust.BridgeAudioProcessingConfig( route: base.route, - iosMode: iosMode, - processingBackend: isSonora - ? rust.BridgeAudioBackend.webrtcApm - : rust.BridgeAudioBackend.platformVoiceProcessing, + iosMode: normalizedIosProcessingMode(iosMode), + processingBackend: rust.BridgeAudioBackend.platformVoiceProcessing, vadBackend: vad, - aec: aecOwner, - ns: nsOwner, - agc: agcOwner, + aec: rust.BridgeEffectOwner.platform, + ns: nsEnabled + ? rust.BridgeEffectOwner.platform + : rust.BridgeEffectOwner.off, + agc: agcEnabled + ? rust.BridgeEffectOwner.platform + : rust.BridgeEffectOwner.off, hpfEnabled: hpfEnabled, limiterEnabled: limiterEnabled, vadHangoverMs: base.vadHangoverMs, @@ -133,13 +175,55 @@ class AudioProcessingConfigState { } } +bool androidUsesHardwareProcessing(AudioProcessingConfigState state) { + return state.preferHardware; +} + +bool androidShowsNsControl(AudioProcessingConfigState state) { + return true; +} + +bool androidShowsAecControl(AudioProcessingConfigState state) { + return true; +} + +bool androidShowsAgcControl(AudioProcessingConfigState state) { + return true; +} + +bool androidShowsHpfControl(AudioProcessingConfigState state) { + return true; +} + +bool androidShowsLimiterControl(AudioProcessingConfigState state) { + return false; +} + /// Never leave the UI on the hidden disabled backend. -rust.BridgeVadBackend normalizedVadBackend(rust.BridgeVadBackend backend) { +/// +/// Windows and Linux use Silero as the primary VAD; WebRTC is still +/// available internally as a runtime fallback. +rust.BridgeVadBackend normalizedVadBackend( + rust.BridgeVadBackend backend, { + bool? isWindows, + bool? isLinux, +}) { + final desktop = + (isWindows ?? Platform.isWindows) || (isLinux ?? Platform.isLinux); + if (desktop) return rust.BridgeVadBackend.sileroOnnx; return backend == rust.BridgeVadBackend.disabled - ? rust.BridgeVadBackend.webrtcVad + ? rust.BridgeVadBackend.sileroOnnx : backend; } +rust.BridgeIosVoiceProcessingMode normalizedIosProcessingMode( + rust.BridgeIosVoiceProcessingMode mode, +) { + return mode == rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing + ? mode + : rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing; +} + bool _usesPlatformEffects(rust.BridgeAudioProcessingConfig config) { return config.aec == rust.BridgeEffectOwner.platform || config.ns == rust.BridgeEffectOwner.platform || diff --git a/apps/chanora_flutter/lib/widgets/chat_views.dart b/apps/chanora_flutter/lib/widgets/chat_views.dart index 27f477c..953a205 100644 --- a/apps/chanora_flutter/lib/widgets/chat_views.dart +++ b/apps/chanora_flutter/lib/widgets/chat_views.dart @@ -10,6 +10,9 @@ import '../services/ts3_server_link.dart'; import '../src/rust/api.dart' as rust; import 'bbcode_text.dart'; +const double _chatSidebarTileExtent = 92; +const Color _chatSidebarSelectedTileColor = Color(0xFF415366); + /// One chat/activity message shown in the chat hub. class ChatEntry { /// Construct a chat entry. @@ -20,6 +23,7 @@ class ChatEntry { required this.target, this.isSelf = false, this.timestamp, + this.countsTowardUnread = true, }); /// Sender client id. @@ -40,11 +44,18 @@ class ChatEntry { /// Local time when this message was received or sent. final DateTime? timestamp; + /// Whether this entry should increment the message unread badge. + final bool countsTowardUnread; + /// True for direct-message targets. bool get isPrivate => target is rust.BridgeMessageTarget_Client; /// True for poke targets. bool get isPoke => target is rust.BridgeMessageTarget_Poke; + + /// True for synthetic or protocol-driven server activity rows. + bool get isServerActivity => + target is rust.BridgeMessageTarget_Server && !countsTowardUnread; } String chatTimeLabel(DateTime? timestamp) { @@ -69,6 +80,162 @@ String pokeHistoryLine(AppL10n l10n, ChatEntry entry) { return l10n.pokeHistoryIncomingWithMessage(time, peerName, message); } +enum _ActivityTone { + positive, + transition, + warning, + plain, + username, + channel, + adminActor, + automationActor, + group, +} + +sealed class _ActivitySegment { + const _ActivitySegment(this.text, this.tone, {this.bold = false}); + + final String text; + final _ActivityTone tone; + final bool bold; +} + +class _ActivityTextSegment extends _ActivitySegment { + const _ActivityTextSegment(super.text, super.tone, {super.bold = false}); +} + +List<_ActivitySegment> _activitySegments(String message) { + final quotedMatches = RegExp(r'"[^"]+"').allMatches(message).toList(); + if (quotedMatches.isEmpty) { + return [_ActivityTextSegment(message, _activityActionTone(message))]; + } + + final segments = <_ActivitySegment>[]; + var cursor = 0; + for (var i = 0; i < quotedMatches.length; i++) { + final match = quotedMatches[i]; + if (match.start > cursor) { + final plain = message.substring(cursor, match.start); + if (plain.isNotEmpty) { + segments.add( + _ActivityTextSegment(plain, _activityActionTone(plain.trim())), + ); + } + } + final quoted = match.group(0)!; + segments.add(_activityEntitySegment(quoted, message, i)); + cursor = match.end; + } + if (cursor < message.length) { + final tail = message.substring(cursor); + if (tail.isNotEmpty) { + segments.add( + _ActivityTextSegment(tail, _activityActionTone(tail.trim())), + ); + } + } + return segments; +} + +_ActivitySegment _activityEntitySegment( + String quoted, + String fullMessage, + int quotedIndex, +) { + final lower = fullMessage.toLowerCase(); + final after = fullMessage + .substring(fullMessage.indexOf(quoted) + quoted.length) + .toLowerCase(); + final value = quoted.substring(1, quoted.length - 1); + + if (lower.contains('channel group ')) { + if (quotedIndex == 0) { + return _ActivityTextSegment(quoted, _ActivityTone.group); + } + return _activityActorSegment(value, quoted); + } + if (lower.contains('server group ')) { + if (quotedIndex == 0) { + return _ActivityTextSegment(quoted, _ActivityTone.group); + } + return _activityActorSegment(value, quoted); + } + if (after.startsWith(' connected to channel') || + after.startsWith(' switched from channel') || + after.startsWith(' disconnected') || + after.startsWith(' was moved from channel') || + after.startsWith(' is now ')) { + return _activityActorSegment(value, quoted); + } + if (lower.contains('channel ') && + (after.startsWith('.') || + after.startsWith(' to') || + after.startsWith(' from') || + after.startsWith(' by') || + after.isEmpty)) { + return _ActivityTextSegment(quoted, _ActivityTone.channel, bold: true); + } + if (lower.contains('channel ') && + (quotedIndex == 1 || quotedIndex == 2 || quotedIndex == 3)) { + return _ActivityTextSegment(quoted, _ActivityTone.channel, bold: true); + } + return _activityActorSegment(value, quoted); +} + +_ActivitySegment _activityActorSegment(String value, String quoted) { + final lower = value.toLowerCase(); + if (lower.contains('auto') || + lower.contains('automation') || + lower.contains('bot')) { + return _ActivityTextSegment( + quoted, + _ActivityTone.automationActor, + bold: true, + ); + } + if (lower.contains('server') || + lower.contains('admin') || + lower.contains('vigorous pro')) { + return _ActivityTextSegment(quoted, _ActivityTone.adminActor, bold: true); + } + return _ActivityTextSegment(quoted, _ActivityTone.username, bold: true); +} + +_ActivityTone _activityActionTone(String text) { + final lower = text.toLowerCase(); + if (lower.contains('connected')) return _ActivityTone.positive; + if (lower.contains('disconnected') || + lower.contains('dropped') || + lower.contains('lost') || + lower.contains('shutdown')) { + return _ActivityTone.warning; + } + if (lower.contains('switched') || + lower.contains('moved') || + lower.contains('created') || + lower.contains('deleted') || + lower.contains('renamed') || + lower.contains('assigned') || + lower.contains('removed')) { + return _ActivityTone.transition; + } + return _ActivityTone.plain; +} + +Color _activityColor(BuildContext context, _ActivityTone tone) { + return switch (tone) { + _ActivityTone.positive => const Color(0xFF2F7D57), + _ActivityTone.transition => const Color(0xFF5A7394), + _ActivityTone.warning => const Color(0xFFC06A2B), + _ActivityTone.plain => const Color(0xFF6C7C8F), + _ActivityTone.username => const Color(0xFFC64A4A), + _ActivityTone.channel => const Color(0xFF3B6EA8), + _ActivityTone.adminActor => const Color(0xFF426B9A), + _ActivityTone.automationActor => const Color(0xFFB64C4C), + _ActivityTone.group => const Color(0xFF8B7A68), + }; +} + /// Resolve the best chat target to show when opening the chat page. /// /// Returns null to show the chat hub. @@ -76,15 +243,126 @@ rust.BridgeMessageTarget? resolveInitialChatTarget({ required List messages, required BigInt? currentVoiceChannelId, }) { + bool hasServerActivity = false; for (final message in messages.reversed) { if (message.isPrivate) return message.target; + if (message.target is rust.BridgeMessageTarget_Server) { + hasServerActivity = true; + } } if (currentVoiceChannelId != null) { return const rust.BridgeMessageTarget.channel(); } + if (hasServerActivity) { + return const rust.BridgeMessageTarget.server(); + } return null; } +/// Build server-activity entries from a fresh snapshot diff. +List buildServerActivityEntries({ + required rust.BridgeSnapshot previous, + required rust.BridgeSnapshot current, + DateTime? timestamp, +}) { + final now = timestamp ?? DateTime.now(); + final previousClients = { + for (final client in previous.clients) client.id: client, + }; + final currentClients = { + for (final client in current.clients) client.id: client, + }; + final previousChannels = { + for (final channel in previous.channels) channel.id: channel, + }; + final currentChannels = { + for (final channel in current.channels) channel.id: channel, + }; + final entries = []; + + String clientLabel(rust.BridgeClient client, rust.BridgeSnapshot snapshot) { + if (client.id == snapshot.ownClientId) return 'You'; + final name = client.name.isNotEmpty ? client.name : 'Unknown'; + return '"$name"'; + } + + String channelLabel(rust.BridgeSnapshot snapshot, BigInt channelId) { + final name = snapshotChannelName(snapshot, channelId); + if (name.isNotEmpty) return 'channel "$name"'; + return 'channel ${channelId.toString()}'; + } + + void addEntry(String message) { + entries.add( + ChatEntry( + senderId: BigInt.zero, + senderName: 'Server', + message: message, + target: const rust.BridgeMessageTarget.server(), + timestamp: now, + countsTowardUnread: false, + ), + ); + } + + for (final client in current.clients) { + if (client.isServerQuery) continue; + final previousClient = previousClients[client.id]; + if (previousClient == null) { + addEntry( + '${clientLabel(client, current)} connected to ${channelLabel(current, client.channel)}', + ); + continue; + } + if (previousClient.channel != client.channel) { + final fromChannel = channelLabel(previous, previousClient.channel); + final toChannel = channelLabel(current, client.channel); + addEntry( + '${clientLabel(client, current)} switched from $fromChannel to $toChannel', + ); + continue; + } + if (previousClient.name != client.name && client.name.isNotEmpty) { + final previousName = previousClient.id == current.ownClientId + ? 'You' + : (previousClient.name.isNotEmpty ? previousClient.name : 'Unknown'); + final currentName = clientLabel(client, current); + addEntry('$previousName is now $currentName'); + } + } + + for (final client in previous.clients) { + if (client.isServerQuery) continue; + if (currentClients.containsKey(client.id)) continue; + addEntry( + '${clientLabel(client, previous)} disconnected from ${channelLabel(previous, client.channel)}', + ); + } + + for (final channel in current.channels) { + final previousChannel = previousChannels[channel.id]; + if (previousChannel == null) { + addEntry('Channel created: ${channelLabel(current, channel.id)}'); + continue; + } + if (previousChannel.name != channel.name) { + addEntry('Channel renamed to ${channelLabel(current, channel.id)}'); + continue; + } + if (previousChannel.parent != channel.parent || + previousChannel.order != channel.order) { + addEntry('Channel moved: ${channelLabel(current, channel.id)}'); + } + } + + for (final channel in previous.channels) { + if (currentChannels.containsKey(channel.id)) continue; + addEntry('Channel deleted: ${channelLabel(previous, channel.id)}'); + } + + return entries; +} + class ChatClientGroups { const ChatClientGroups({ required this.clientsByChannel, @@ -255,6 +533,9 @@ class ChatPage extends StatefulWidget { super.key, required this.messages, required this.snapshot, + this.messagesSource, + this.snapshotSource, + this.refreshListenable, this.initialTarget, this.initialClientName = '', this.onTs3ServerLink, @@ -266,6 +547,15 @@ class ChatPage extends StatefulWidget { /// Latest TeamSpeak snapshot. final rust.BridgeSnapshot snapshot; + /// Optional live source for messages while this route stays open. + final List Function()? messagesSource; + + /// Optional live source for snapshots while this route stays open. + final rust.BridgeSnapshot Function()? snapshotSource; + + /// Triggers a rebuild when the backing chat state changes. + final Listenable? refreshListenable; + /// Non-null to open a specific target directly; null for hub. final rust.BridgeMessageTarget? initialTarget; @@ -284,13 +574,19 @@ class _ChatPageState extends State { String _selectedClientName = ''; final Set _closedPrivateChats = {}; + List get _messages => + widget.messagesSource?.call() ?? widget.messages; + + rust.BridgeSnapshot get _snapshot => + widget.snapshotSource?.call() ?? widget.snapshot; + @override void initState() { super.initState(); _selectedTarget = widget.initialTarget ?? resolveInitialChatTarget( - messages: widget.messages, + messages: _messages, currentVoiceChannelId: _currentChannelId, ) ?? const rust.BridgeMessageTarget.server(); @@ -298,16 +594,28 @@ class _ChatPageState extends State { } BigInt? get _currentChannelId { - return ownClientSnapshotState(widget.snapshot)?.channelId; + return ownClientSnapshotState(_snapshot)?.channelId; } @override Widget build(BuildContext context) { + if (widget.refreshListenable != null) { + return ListenableBuilder( + listenable: widget.refreshListenable!, + builder: (context, _) => _buildScaffold(context), + ); + } + return _buildScaffold(context); + } + + Widget _buildScaffold(BuildContext context) { + final snapshot = _snapshot; + final messages = _messages; final currentChannelId = _currentChannelId; - final channelName = snapshotChannelName(widget.snapshot, currentChannelId); + final channelName = snapshotChannelName(snapshot, currentChannelId); return Scaffold( appBar: AppBar( - title: Text('Chat — ${widget.snapshot.serverName}'), + title: Text('Chat — ${snapshot.serverName}'), actions: [ IconButton( tooltip: 'Close chat', @@ -334,8 +642,8 @@ class _ChatPageState extends State { child: _ChatDetailView( target: _selectedTarget, clientName: _selectedClientName, - snapshot: widget.snapshot, - messages: widget.messages, + snapshot: snapshot, + messages: messages, currentChannelId: currentChannelId, channelName: channelName, onTs3ServerLink: widget.onTs3ServerLink, @@ -353,7 +661,7 @@ class _ChatPageState extends State { List<_PrivateChatItem> get _privateChats { final chats = {}; - for (final message in widget.messages) { + for (final message in _messages) { final target = message.target; if (target is! rust.BridgeMessageTarget_Client) continue; final id = target.field0; @@ -378,7 +686,7 @@ class _ChatPageState extends State { } String _privateChatName(BigInt id, String fallback) { - for (final client in widget.snapshot.clients) { + for (final client in _snapshot.clients) { if (client.id == id && client.name.isNotEmpty) return client.name; } return fallback.isNotEmpty && fallback != 'You' ? fallback : 'Direct'; @@ -443,7 +751,7 @@ class _ChatSidebar extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - width: 148, + width: _chatSidebarTileExtent, child: Column( children: [ _ChatSidebarItem( @@ -513,30 +821,45 @@ class _ChatSidebarItem extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final bg = selected - ? theme.colorScheme.primaryContainer - : Colors.transparent; - final fg = selected - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurface; + final fg = selected ? Colors.white : theme.colorScheme.onSurface; return Material( - color: bg, + color: Colors.transparent, child: InkWell( onTap: onTap, - child: SizedBox( - height: 44, + borderRadius: BorderRadius.circular(8), + child: Ink( + width: _chatSidebarTileExtent, + height: _chatSidebarTileExtent, + decoration: BoxDecoration( + color: selected + ? _chatSidebarSelectedTileColor + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 18, color: fg), - const SizedBox(width: 8), - Expanded( - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith(color: fg), + SizedBox( + width: 24, + height: 24, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [Icon(icon, size: 18, color: fg)], + ), + ), + const SizedBox(height: 8), + Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: theme.textTheme.labelSmall?.copyWith( + color: fg, + height: 1.1, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), ), ], @@ -693,6 +1016,8 @@ class _ChatDetailView extends StatefulWidget { class _ChatDetailViewState extends State<_ChatDetailView> { final _textCtl = TextEditingController(); final _scrollCtl = ScrollController(); + int _lastRenderedMessageCount = -1; + rust.BridgeMessageTarget? _lastRenderedTarget; Iterable get _filtered { if (widget.target is rust.BridgeMessageTarget_Channel) { @@ -758,10 +1083,22 @@ class _ChatDetailViewState extends State<_ChatDetailView> { } } + void _scheduleScrollIfNeeded(int messageCount) { + final targetChanged = _lastRenderedTarget != widget.target; + final countChanged = _lastRenderedMessageCount != messageCount; + _lastRenderedTarget = widget.target; + _lastRenderedMessageCount = messageCount; + if (!targetChanged && !countChanged) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _scrollToBottom(); + }); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); final msgs = _filtered.toList(); + _scheduleScrollIfNeeded(msgs.length); final placeholder = chatInputPlaceholder( widget.target, channelName: widget.channelName, @@ -882,10 +1219,13 @@ class _MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); if (entry.isPoke) { return _PokeHistoryRow(entry: entry); } + if (entry.isServerActivity) { + return _ServerActivityRow(entry: entry); + } + final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( @@ -947,6 +1287,61 @@ class _MessageBubble extends StatelessWidget { } } +class _ServerActivityRow extends StatelessWidget { + const _ServerActivityRow({required this.entry}); + + final ChatEntry entry; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final timestampStyle = theme.textTheme.bodySmall?.copyWith( + color: const Color(0xFF8A98A8), + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w400, + letterSpacing: 0.15, + fontFeatures: const [FontFeature.tabularFigures()], + ); + final baseStyle = theme.textTheme.bodySmall?.copyWith( + color: const Color(0xFF6C7C8F), + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.45, + letterSpacing: 0.05, + ); + final spans = [ + TextSpan( + text: '<${chatTimeLabel(entry.timestamp)}> ', + style: timestampStyle, + ), + ]; + for (final segment in _activitySegments(entry.message)) { + spans.add( + TextSpan( + text: segment.text, + style: baseStyle?.copyWith( + color: _activityColor(context, segment.tone), + fontWeight: segment.bold ? FontWeight.w700 : FontWeight.w400, + ), + ), + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 3), + child: DecoratedBox( + decoration: BoxDecoration( + color: const Color(0xFFF7F9FC), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text.rich(TextSpan(children: spans)), + ), + ), + ); + } +} + class _PokeHistoryRow extends StatelessWidget { const _PokeHistoryRow({required this.entry}); diff --git a/apps/chanora_flutter/lib/widgets/voice_compact.dart b/apps/chanora_flutter/lib/widgets/voice_compact.dart index e2797a1..601c46e 100644 --- a/apps/chanora_flutter/lib/widgets/voice_compact.dart +++ b/apps/chanora_flutter/lib/widgets/voice_compact.dart @@ -26,6 +26,21 @@ import 'voice_settings_controls.dart'; import 'voice_status_summary.dart'; import '../src/rust/api.dart' as rust; +bool get _isIos { + if (kIsWeb) return false; + return Platform.isIOS; +} + +bool get _isMacOS { + if (kIsWeb) return false; + return Platform.isMacOS; +} + +bool get _isDesktopSileroVadHost { + if (kIsWeb) return false; + return Platform.isWindows || Platform.isLinux; +} + /// Two-line status chip that summarises the current voice state. /// Tap to open the voice details modal. class VoiceStatusChip extends StatelessWidget { @@ -448,17 +463,17 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> { widget.initialAudioConfig, ); - // Poll audio stats at 80 ms so TX/RX counters and the level meter + // Poll audio stats at 250 ms so TX/RX counters and the level meter // update in real time while the sheet is open, independent of the parent. - _statsTimer = Timer.periodic(const Duration(milliseconds: 80), (_) async { + _statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async { try { final s = await rust.audioStats(); if (!mounted) return; setState(() { _stats = s; _rateTickCount++; - // Compute rates every ~960 ms (12 × 80 ms). - if (_rateTickCount >= 12) { + // Compute rates every ~1 s (4 × 250 ms). + if (_rateTickCount >= 4) { _txRate = s.framesSent - _prevSent; _rxRate = s.framesReceived - _prevReceived; _prevSent = s.framesSent; @@ -657,103 +672,99 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> { _notifyAudioConfig(); }, ), + const SizedBox(height: 4), + Text( + _audioProcessing.preferHardware + ? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.' + : 'Software mode applies the full WebRTC APM stage set.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), ], - AudioProcessingToggleRow( - dense: true, - label: 'Noise suppression', - subtitle: 'Wiener filter', - value: _audioProcessing.nsEnabled, - onChanged: (v) { - setState(() => _audioProcessing.nsEnabled = v); - _notifyAudioConfig(); - }, - ), - AudioProcessingToggleRow( - dense: true, - label: 'Echo cancellation', - subtitle: () { - if (Platform.isAndroid) { - return 'WebRTC AEC3 · adaptive filter'; - } - return widget.initialAudioConfig.iosMode == - rust - .BridgeIosVoiceProcessingMode - .platformVoiceProcessing - ? 'Always on · managed by platform VPIO' - : 'AEC3 adaptive filter · 80 ms tail'; - }(), - value: () { - if (Platform.isAndroid) return _audioProcessing.aecEnabled; - return widget.initialAudioConfig.iosMode == - rust - .BridgeIosVoiceProcessingMode - .platformVoiceProcessing - ? true - : _audioProcessing.aecEnabled; - }(), - onChanged: () { - if (Platform.isAndroid) { - return (v) { - setState(() => _audioProcessing.aecEnabled = v); - _notifyAudioConfig(); - }; - } - return widget.initialAudioConfig.iosMode == - rust - .BridgeIosVoiceProcessingMode - .platformVoiceProcessing + if (_isIos) ...[ + Text( + 'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are ' + 'hidden here; only settings that still affect the shipping ' + 'iOS path are shown.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + ], + if (!_isIos && + (!Platform.isAndroid || + androidShowsNsControl(_audioProcessing))) + AudioProcessingToggleRow( + dense: true, + label: 'Noise suppression', + subtitle: 'Wiener filter', + value: _audioProcessing.nsEnabled, + onChanged: (v) { + setState(() => _audioProcessing.nsEnabled = v); + _notifyAudioConfig(); + }, + ), + if (!_isIos && + (!Platform.isAndroid || + androidShowsAecControl(_audioProcessing))) + AudioProcessingToggleRow( + dense: true, + label: 'Echo cancellation', + subtitle: Platform.isAndroid + ? (_audioProcessing.preferHardware + ? 'Prefers device/OS effect; falls back to WebRTC AEC3' + : 'WebRTC AEC3 · adaptive filter') + : (_isMacOS + ? 'Managed by platform VPIO' + : 'WebRTC AEC3 · adaptive filter'), + value: _isMacOS ? true : _audioProcessing.aecEnabled, + onChanged: _isMacOS ? null : (v) { setState(() => _audioProcessing.aecEnabled = v); _notifyAudioConfig(); - }; - }(), - ), - AudioProcessingToggleRow( - dense: true, - label: 'Auto gain control', - subtitle: 'AGC2 · −18 dBFS target', - value: _audioProcessing.agcEnabled, - onChanged: (v) { - setState(() => _audioProcessing.agcEnabled = v); - _notifyAudioConfig(); - }, - ), - AudioProcessingToggleRow( - dense: true, - label: 'High-pass filter', - subtitle: '80 Hz · DC removal', - value: _audioProcessing.hpfEnabled, - onChanged: (v) { - setState(() => _audioProcessing.hpfEnabled = v); - _notifyAudioConfig(); - }, - ), - AudioProcessingToggleRow( - dense: true, - label: 'Peak limiter', - subtitle: '−1 dBFS soft-knee · 2 ms look-ahead', - value: _audioProcessing.limiterEnabled, - onChanged: (v) { - setState(() => _audioProcessing.limiterEnabled = v); - _notifyAudioConfig(); - }, - ), - - // iOS mode selector. - if (Platform.isIOS) ...[ - const VoiceSubHeader('Processing backend'), - SegmentedButton( - style: voiceSegmentedButtonStyle(theme), - segments: iosProcessingSegments, - selected: {_audioProcessing.iosMode}, - onSelectionChanged: (s) { - setState(() => _audioProcessing.iosMode = s.first); + }, + ), + if (!_isIos && + (!Platform.isAndroid || + androidShowsAgcControl(_audioProcessing))) + AudioProcessingToggleRow( + dense: true, + label: 'Auto gain control', + subtitle: 'AGC2 · -18 dBFS target', + value: _audioProcessing.agcEnabled, + onChanged: (v) { + setState(() => _audioProcessing.agcEnabled = v); + _notifyAudioConfig(); + }, + ), + if (!Platform.isAndroid || androidShowsHpfControl(_audioProcessing)) + AudioProcessingToggleRow( + dense: true, + label: 'High-pass filter', + subtitle: '80 Hz · DC removal', + value: _audioProcessing.hpfEnabled, + onChanged: (v) { + setState(() => _audioProcessing.hpfEnabled = v); + _notifyAudioConfig(); + }, + ), + if (!_isIos && + (!Platform.isAndroid || + androidShowsLimiterControl(_audioProcessing))) + AudioProcessingToggleRow( + dense: true, + label: 'Peak limiter', + subtitle: '-1 dBFS soft-knee · 2 ms look-ahead', + value: _audioProcessing.limiterEnabled, + onChanged: (v) { + setState(() => _audioProcessing.limiterEnabled = v); _notifyAudioConfig(); }, ), - ], // VAD backend. const SizedBox(height: 8), @@ -766,7 +777,9 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> { const SizedBox(height: 2), SegmentedButton( style: voiceSegmentedButtonStyle(theme), - segments: vadBackendSegments, + segments: _isDesktopSileroVadHost + ? desktopVadBackendSegments + : vadBackendSegments, selected: {_audioProcessing.vadBackend}, onSelectionChanged: (s) { setState(() => _audioProcessing.vadBackend = s.first); diff --git a/apps/chanora_flutter/lib/widgets/voice_settings.dart b/apps/chanora_flutter/lib/widgets/voice_settings.dart index 8fc3bfe..45cd4be 100644 --- a/apps/chanora_flutter/lib/widgets/voice_settings.dart +++ b/apps/chanora_flutter/lib/widgets/voice_settings.dart @@ -5,7 +5,7 @@ // - Automatic gain control (AGC2) // - High-pass filter (HPF) // - VAD backend -// - iOS voice processing mode +// - platform audio-processing mode selection where available import 'dart:io' show Platform; @@ -32,6 +32,16 @@ bool get _isIos { return Platform.isIOS; } +bool get _isMacOS { + if (kIsWeb) return false; + return Platform.isMacOS; +} + +bool get _isDesktopSileroVadHost { + if (kIsWeb) return false; + return Platform.isWindows || Platform.isLinux; +} + /// Result returned by [VoiceSettingsDialog]. class VoiceSettingsResult { const VoiceSettingsResult({ @@ -104,9 +114,6 @@ class _VoiceSettingsDialogState extends State { Widget build(BuildContext context) { final l10n = AppL10n.of(context); final theme = Theme.of(context); - final platformVpio = - _audioProcessing.iosMode == - rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing; return AlertDialog( title: Text(l10n.voiceSettingsTitle), contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), @@ -177,19 +184,6 @@ class _VoiceSettingsDialogState extends State { const Divider(height: 24), const VoiceSectionHeader('Audio processing'), - // iOS mode selector (iOS only) - if (_isIos) ...[ - const VoiceSubHeader('Processing backend'), - SegmentedButton( - style: voiceSegmentedButtonStyle(theme), - segments: iosProcessingSegments, - selected: {_audioProcessing.iosMode}, - onSelectionChanged: (s) => - setState(() => _audioProcessing.iosMode = s.first), - ), - const SizedBox(height: 4), - ], - // Android HW/SW selector if (_isAndroid) ...[ const VoiceSubHeader('Processing backend'), @@ -201,51 +195,81 @@ class _VoiceSettingsDialogState extends State { setState(() => _audioProcessing.preferHardware = s.first), ), const SizedBox(height: 4), + Text( + _audioProcessing.preferHardware + ? 'Android hardware mode still falls back to WebRTC APM per stage when device effects are missing, so these controls remain available.' + : 'Android software mode applies the full WebRTC APM control set.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), ], // DSP toggles const VoiceSubHeader('DSP stages'), - AudioProcessingToggleRow( - label: 'Noise suppression (NS)', - subtitle: 'Wiener filter · stationary noise', - value: _audioProcessing.nsEnabled, - onChanged: (v) => - setState(() => _audioProcessing.nsEnabled = v), - ), - AudioProcessingToggleRow( - label: 'Echo cancellation (AEC3)', - subtitle: _isAndroid - ? 'WebRTC AEC3 · adaptive filter' - : platformVpio - ? 'Managed by platform VPIO' - : 'Adaptive NLMS · 80 ms tail', - value: _audioProcessing.aecEnabled, - // AEC is always on in VPIO mode — disable the toggle. - onChanged: (_isAndroid || !platformVpio) - ? (v) => setState(() => _audioProcessing.aecEnabled = v) - : null, - ), - AudioProcessingToggleRow( - label: 'Auto gain control (AGC2)', - subtitle: 'RNN VAD-gated · −18 dBFS target', - value: _audioProcessing.agcEnabled, - onChanged: (v) => - setState(() => _audioProcessing.agcEnabled = v), - ), - AudioProcessingToggleRow( - label: 'High-pass filter (HPF)', - subtitle: '80 Hz Butterworth · DC removal', - value: _audioProcessing.hpfEnabled, - onChanged: (v) => - setState(() => _audioProcessing.hpfEnabled = v), - ), - AudioProcessingToggleRow( - label: 'Peak limiter', - subtitle: '−1 dBFS soft-knee · 2 ms look-ahead', - value: _audioProcessing.limiterEnabled, - onChanged: (v) => - setState(() => _audioProcessing.limiterEnabled = v), - ), + if (_isIos) ...[ + Text( + 'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are ' + 'hidden here; only settings that still affect the shipping ' + 'iOS path are shown.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + ], + if (!_isIos && + (!_isAndroid || androidShowsNsControl(_audioProcessing))) + AudioProcessingToggleRow( + label: 'Noise suppression (NS)', + subtitle: 'Wiener filter · stationary noise', + value: _audioProcessing.nsEnabled, + onChanged: (v) => + setState(() => _audioProcessing.nsEnabled = v), + ), + if (!_isIos && + (!_isAndroid || androidShowsAecControl(_audioProcessing))) + AudioProcessingToggleRow( + label: 'Echo cancellation (AEC3)', + subtitle: _isAndroid + ? (_audioProcessing.preferHardware + ? 'Prefers device/OS effect; WebRTC AEC3 fallback when binding is unavailable' + : 'WebRTC AEC3 · adaptive filter') + : (_isMacOS + ? 'Managed by platform VPIO' + : 'WebRTC AEC3 · adaptive filter'), + value: _isMacOS ? true : _audioProcessing.aecEnabled, + onChanged: _isMacOS + ? null + : (v) => setState(() => _audioProcessing.aecEnabled = v), + ), + if (!_isIos && + (!_isAndroid || androidShowsAgcControl(_audioProcessing))) + AudioProcessingToggleRow( + label: 'Auto gain control (AGC2)', + subtitle: 'RNN VAD-gated · -18 dBFS target', + value: _audioProcessing.agcEnabled, + onChanged: (v) => + setState(() => _audioProcessing.agcEnabled = v), + ), + if (!_isAndroid || androidShowsHpfControl(_audioProcessing)) + AudioProcessingToggleRow( + label: 'High-pass filter (HPF)', + subtitle: '80 Hz Butterworth · DC removal', + value: _audioProcessing.hpfEnabled, + onChanged: (v) => + setState(() => _audioProcessing.hpfEnabled = v), + ), + if (!_isIos && + (!_isAndroid || androidShowsLimiterControl(_audioProcessing))) + AudioProcessingToggleRow( + label: 'Peak limiter', + subtitle: '-1 dBFS soft-knee · 2 ms look-ahead', + value: _audioProcessing.limiterEnabled, + onChanged: (v) => + setState(() => _audioProcessing.limiterEnabled = v), + ), if (isTalkPowerBlocked( talkPower: widget.talkPower, @@ -267,7 +291,9 @@ class _VoiceSettingsDialogState extends State { const VoiceSubHeader('Backend'), SegmentedButton( style: voiceSegmentedButtonStyle(theme), - segments: vadBackendSegments, + segments: _isDesktopSileroVadHost + ? desktopVadBackendSegments + : vadBackendSegments, selected: {_audioProcessing.vadBackend}, onSelectionChanged: (s) => setState(() => _audioProcessing.vadBackend = s.first), diff --git a/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart b/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart index e4df35c..7c15441 100644 --- a/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart +++ b/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart @@ -43,20 +43,6 @@ const androidProcessingSegments = [ ), ]; -/// iOS VPIO/Sonora selector segments. -const iosProcessingSegments = [ - ButtonSegment( - value: rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, - label: Text('VPIO'), - icon: Icon(Icons.phone_iphone, size: 14), - ), - ButtonSegment( - value: rust.BridgeIosVoiceProcessingMode.sonoraExperimental, - label: Text('Sonora'), - icon: Icon(Icons.science_outlined, size: 14), - ), -]; - /// Voice activity detector selector segments. const vadBackendSegments = [ ButtonSegment( @@ -69,10 +55,17 @@ const vadBackendSegments = [ label: Text('Silero'), icon: Icon(Icons.psychology, size: 14), ), +]; + +/// Desktop VAD selector segments. +/// +/// Windows and Linux use Silero as the primary VAD. WebRTC remains an +/// internal runtime fallback when the model/runtime is unavailable. +const desktopVadBackendSegments = [ ButtonSegment( - value: rust.BridgeVadBackend.tenVad, - label: Text('TEN'), - icon: Icon(Icons.graphic_eq, size: 14), + value: rust.BridgeVadBackend.sileroOnnx, + label: Text('Silero'), + icon: Icon(Icons.psychology, size: 14), ), ]; diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index 554940c..c513ed9 100644 --- a/apps/chanora_flutter/pubspec.yaml +++ b/apps/chanora_flutter/pubspec.yaml @@ -111,7 +111,6 @@ flutter: assets: - assets/models/silero_vad.onnx - - assets/models/ten_vad.onnx # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/apps/chanora_flutter/test/widgets/audio_device_list_tile_test.dart b/apps/chanora_flutter/test/widgets/audio_device_list_tile_test.dart index 48793b6..d7f9567 100644 --- a/apps/chanora_flutter/test/widgets/audio_device_list_tile_test.dart +++ b/apps/chanora_flutter/test/widgets/audio_device_list_tile_test.dart @@ -29,4 +29,94 @@ void main() { expect(find.byType(CircularProgressIndicator), findsOneWidget); }, ); + + testWidgets('audio device tile shows selected device and details', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AudioDeviceListTile( + label: 'Output', + kind: AudioDeviceKind.output, + loadDevices: () async => const rust.BridgeAudioDeviceList( + inputDevices: [], + outputDevices: [ + rust.BridgeAudioDevice( + id: 'default-speakers', + name: 'Speakers', + details: 'Realtek · Speaker · id=1111', + isDefault: true, + isSelected: false, + ), + rust.BridgeAudioDevice( + id: 'usb-headset', + name: 'USB Headset', + details: 'SteelSeries · Headset · id=2222', + isDefault: false, + isSelected: true, + ), + ], + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('USB Headset'), findsOneWidget); + expect(find.text('SteelSeries · Headset · id=2222'), findsNothing); + + await tester.tap(find.text('Output')); + await tester.pumpAndSettle(); + + expect(find.text('SteelSeries · Headset · id=2222'), findsOneWidget); + expect(find.text('System default'), findsOneWidget); + }); + + testWidgets('audio device tile selects by id', (tester) async { + String? selectedId = 'device-a'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AudioDeviceListTile( + label: 'Input', + kind: AudioDeviceKind.input, + loadDevices: () async => const rust.BridgeAudioDeviceList( + inputDevices: [ + rust.BridgeAudioDevice( + id: 'device-a', + name: 'Built-in Mic', + details: 'Realtek · Microphone · id=aaaa', + isDefault: true, + isSelected: true, + ), + rust.BridgeAudioDevice( + id: 'device-b', + name: 'USB Mic', + details: 'Shure · Microphone · id=bbbb', + isDefault: false, + isSelected: false, + ), + ], + outputDevices: [], + ), + setInputDevice: ({String? id}) async { + selectedId = id; + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + await tester.tap(find.text('Input')); + await tester.pumpAndSettle(); + await tester.tap(find.text('USB Mic')); + await tester.pump(); + + expect(selectedId, 'device-b'); + }); } diff --git a/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart b/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart index 2aaeca5..bc3053a 100644 --- a/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart +++ b/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'dart:io' show Platform; import 'package:chanora_flutter/src/rust/api.dart' as rust; import 'package:chanora_flutter/widgets/audio_processing_config_state.dart'; @@ -20,28 +21,64 @@ void main() { debugWavDumpEnabled: true, ); - test('normalizes hidden disabled VAD backend for UI state', () { + test('normalizes hidden disabled VAD backend to Silero for UI state', () { final state = AudioProcessingConfigState.fromConfig(baseConfig); - expect(state.vadBackend, rust.BridgeVadBackend.webrtcVad); + expect(state.vadBackend, rust.BridgeVadBackend.sileroOnnx); expect(state.preferHardware, isTrue); expect(state.nsEnabled, isFalse); expect(state.aecEnabled, isTrue); expect(state.agcEnabled, isTrue); }); - test('default config uses platform processing and Silero VAD', () { + test('normalizes desktop VAD backend to Silero', () { expect( - defaultAudioProcessingConfig.processingBackend, - rust.BridgeAudioBackend.platformVoiceProcessing, - ); - expect( - defaultAudioProcessingConfig.vadBackend, + normalizedVadBackend( + rust.BridgeVadBackend.webrtcVad, + isWindows: true, + isLinux: false, + ), rust.BridgeVadBackend.sileroOnnx, ); - expect(defaultAudioProcessingConfig.aec, rust.BridgeEffectOwner.platform); - expect(defaultAudioProcessingConfig.ns, rust.BridgeEffectOwner.platform); - expect(defaultAudioProcessingConfig.agc, rust.BridgeEffectOwner.platform); + expect( + normalizedVadBackend( + rust.BridgeVadBackend.webrtcVad, + isWindows: false, + isLinux: true, + ), + rust.BridgeVadBackend.sileroOnnx, + ); + }); + + test('default config matches the current platform fallback', () { + final fallback = defaultAudioProcessingConfig(); + final desktop = Platform.isWindows || Platform.isLinux; + + expect( + fallback.processingBackend, + desktop + ? rust.BridgeAudioBackend.webrtcApm + : rust.BridgeAudioBackend.platformVoiceProcessing, + ); + expect(fallback.vadBackend, rust.BridgeVadBackend.sileroOnnx); + expect( + fallback.aec, + desktop + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + ); + expect( + fallback.ns, + desktop + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + ); + expect( + fallback.agc, + desktop + ? rust.BridgeEffectOwner.webrtcApm + : rust.BridgeEffectOwner.platform, + ); }); test('builds Android hardware config consistently', () { @@ -57,25 +94,113 @@ void main() { config.processingBackend, rust.BridgeAudioBackend.platformVoiceProcessing, ); - expect(config.vadBackend, rust.BridgeVadBackend.webrtcVad); + expect(config.vadBackend, rust.BridgeVadBackend.sileroOnnx); expect(config.aec, rust.BridgeEffectOwner.off); expect(config.ns, rust.BridgeEffectOwner.platform); expect(config.agc, rust.BridgeEffectOwner.platform); expect(config.debugWavDumpEnabled, isTrue); }); - test('builds Sonora config with WebRTC-owned enabled effects', () { + test('builds Windows/Linux software WebRTC APM config consistently', () { final state = AudioProcessingConfigState.fromConfig(baseConfig) - ..iosMode = rust.BridgeIosVoiceProcessingMode.sonoraExperimental ..nsEnabled = true ..aecEnabled = false ..agcEnabled = true; - final config = state.buildConfig(base: baseConfig, isAndroid: false); + final windowsConfig = state.buildConfig( + base: baseConfig, + isAndroid: false, + isIos: false, + isMacOS: false, + isWindows: true, + isLinux: false, + ); - expect(config.processingBackend, rust.BridgeAudioBackend.webrtcApm); - expect(config.aec, rust.BridgeEffectOwner.off); - expect(config.ns, rust.BridgeEffectOwner.webrtcApm); - expect(config.agc, rust.BridgeEffectOwner.webrtcApm); + final linuxConfig = state.buildConfig( + base: baseConfig, + isAndroid: false, + isIos: false, + isMacOS: false, + isWindows: false, + isLinux: true, + ); + + for (final config in [windowsConfig, linuxConfig]) { + expect(config.processingBackend, rust.BridgeAudioBackend.webrtcApm); + expect(config.vadBackend, rust.BridgeVadBackend.sileroOnnx); + expect(config.aec, rust.BridgeEffectOwner.off); + expect(config.ns, rust.BridgeEffectOwner.webrtcApm); + expect(config.agc, rust.BridgeEffectOwner.webrtcApm); + expect(config.debugWavDumpEnabled, isTrue); + } + }); + + test('normalizes hidden iOS sonora config back to platform VPIO', () { + const sonoraConfig = rust.BridgeAudioProcessingConfig( + route: rust.BridgeAudioRoute.unknown, + iosMode: rust.BridgeIosVoiceProcessingMode.sonoraExperimental, + processingBackend: rust.BridgeAudioBackend.webrtcApm, + vadBackend: rust.BridgeVadBackend.disabled, + aec: rust.BridgeEffectOwner.webrtcApm, + ns: rust.BridgeEffectOwner.webrtcApm, + agc: rust.BridgeEffectOwner.webrtcApm, + hpfEnabled: true, + limiterEnabled: false, + vadHangoverMs: 500, + vadPreRollMs: 160, + vadMinTxMs: 200, + debugWavDumpEnabled: true, + ); + + final state = AudioProcessingConfigState.fromConfig(sonoraConfig); + final config = state.buildConfig( + base: sonoraConfig, + isAndroid: false, + isIos: true, + isMacOS: false, + isWindows: false, + isLinux: false, + ); + + expect( + state.iosMode, + rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, + ); + expect( + config.iosMode, + rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, + ); + expect( + config.processingBackend, + rust.BridgeAudioBackend.platformVoiceProcessing, + ); + expect(config.aec, rust.BridgeEffectOwner.platform); + expect(config.ns, rust.BridgeEffectOwner.platform); + expect(config.agc, rust.BridgeEffectOwner.platform); + }); + + test( + 'Android hardware mode keeps controls visible because stages can fall back to WebRTC', + () { + final state = AudioProcessingConfigState.fromConfig(baseConfig) + ..preferHardware = true; + + expect(androidShowsNsControl(state), isTrue); + expect(androidShowsAecControl(state), isTrue); + expect(androidShowsAgcControl(state), isTrue); + expect(androidShowsHpfControl(state), isTrue); + expect(androidShowsLimiterControl(state), isFalse); + }, + ); + + test('Android software mode shows effective WebRTC APM controls', () { + final state = AudioProcessingConfigState.fromConfig(baseConfig) + ..preferHardware = false; + + expect(androidShowsNsControl(state), isTrue); + expect(androidShowsAecControl(state), isTrue); + expect(androidShowsAgcControl(state), isTrue); + expect(androidShowsHpfControl(state), isTrue); + expect(androidShowsLimiterControl(state), isFalse); }); } diff --git a/apps/chanora_flutter/test/widgets/chat_views_test.dart b/apps/chanora_flutter/test/widgets/chat_views_test.dart index 88224cb..ed4f141 100644 --- a/apps/chanora_flutter/test/widgets/chat_views_test.dart +++ b/apps/chanora_flutter/test/widgets/chat_views_test.dart @@ -113,6 +113,64 @@ void main() { expect(target, isNull); }); + test('initial chat target falls back to server activity when present', () { + final target = resolveInitialChatTarget( + messages: [ + entry(const rust.BridgeMessageTarget.server()), + entry(const rust.BridgeMessageTarget.channel()), + ], + currentVoiceChannelId: null, + ); + + expect(target, const rust.BridgeMessageTarget.server()); + }); + + test('builds server activity entries for join, move, and disconnect', () { + final lobbyId = BigInt.from(10); + final quietId = BigInt.from(20); + final previous = snapshot( + channels: [ + channel(lobbyId, 'Default Channel'), + channel(quietId, 'Quiet Zone'), + ], + clients: [ + client(id: BigInt.one, name: 'Me', channelId: quietId), + client(id: BigInt.from(2), name: 'Alice', channelId: lobbyId), + client(id: BigInt.from(3), name: 'Bob', channelId: lobbyId), + ], + ); + final current = snapshot( + channels: [ + channel(lobbyId, 'Default Channel'), + channel(quietId, 'Quiet Zone'), + ], + clients: [ + client(id: BigInt.one, name: 'Me', channelId: quietId), + client(id: BigInt.from(2), name: 'Alice', channelId: quietId), + client(id: BigInt.from(4), name: 'Carol', channelId: lobbyId), + ], + ); + + final entries = buildServerActivityEntries( + previous: previous, + current: current, + timestamp: DateTime(2026, 5, 24, 11, 40, 39), + ); + + expect(entries.map((entry) => entry.message), [ + '"Alice" switched from channel "Default Channel" to channel "Quiet Zone"', + '"Carol" connected to channel "Default Channel"', + '"Bob" disconnected from channel "Default Channel"', + ]); + expect(entries.every((entry) => !entry.countsTowardUnread), isTrue); + expect( + entries.every( + (entry) => entry.target == const rust.BridgeMessageTarget.server(), + ), + isTrue, + ); + }); + test( 'groups chat picker clients by channel and excludes own/query clients', () { @@ -244,6 +302,76 @@ void main() { expect(groups.single.channelName, 'Lobby'); }); + test('builds server activity entries from snapshot deltas', () { + final before = snapshot( + channels: [ + channel(BigInt.from(10), 'Lobby'), + channel(BigInt.from(20), 'AFK'), + ], + clients: [ + client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)), + client(id: BigInt.from(2), name: 'Alice', channelId: BigInt.from(10)), + client(id: BigInt.from(3), name: 'Bob', channelId: BigInt.from(20)), + ], + ); + final after = snapshot( + channels: [ + channel(BigInt.from(10), 'Lobby'), + channel(BigInt.from(20), 'AFK'), + ], + clients: [ + client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)), + client(id: BigInt.from(2), name: 'Alice', channelId: BigInt.from(20)), + client(id: BigInt.from(4), name: 'Carol', channelId: BigInt.from(10)), + ], + ); + + final entries = buildServerActivityEntries( + previous: before, + current: after, + timestamp: DateTime(2026, 1, 2, 3, 4, 5), + ); + + expect(entries.map((entry) => entry.message), [ + '"Alice" switched from channel "Lobby" to channel "AFK"', + '"Carol" connected to channel "Lobby"', + '"Bob" disconnected from channel "AFK"', + ]); + expect( + entries.every( + (entry) => entry.target == const rust.BridgeMessageTarget.server(), + ), + isTrue, + ); + }); + + test('ignores ServerQuery clients in snapshot-driven server activity', () { + final before = snapshot( + channels: [channel(BigInt.from(10), 'Lobby')], + clients: [ + client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)), + client( + id: BigInt.from(2), + name: 'Query', + channelId: BigInt.from(10), + isServerQuery: true, + ), + ], + ); + final after = snapshot( + channels: [channel(BigInt.from(10), 'Lobby')], + clients: [client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10))], + ); + + final entries = buildServerActivityEntries( + previous: before, + current: after, + timestamp: DateTime(2026, 1, 2, 3, 4, 5), + ); + + expect(entries, isEmpty); + }); + test('formats chat target titles', () { expect( chatTargetTitle( @@ -366,9 +494,16 @@ void main() { final serverTop = tester.getTopLeft(find.text('Server').first).dy; final channelTop = tester.getTopLeft(find.text('Channel')).dy; final privateTop = tester.getTopLeft(find.text('Alpha').first).dy; + final serverTileSize = tester.getSize( + find + .ancestor(of: find.text('Server').first, matching: find.byType(Ink)) + .first, + ); expect(serverTop, lessThan(channelTop)); expect(channelTop, lessThan(privateTop)); + expect(serverTileSize.width, 92); + expect(serverTileSize.height, 92); }); testWidgets( @@ -476,6 +611,80 @@ void main() { expect(find.text('<05:10:37> “EdisonJwa”戳了你一下'), findsOneWidget); }); + testWidgets('server activity renders as timestamped styled row', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: ChatPage( + messages: [ + ChatEntry( + senderId: BigInt.zero, + senderName: 'Server', + message: '"Alice" connected to channel "Lobby"', + target: const rust.BridgeMessageTarget.server(), + timestamp: DateTime(2026, 5, 24, 11, 40, 39), + countsTowardUnread: false, + ), + ], + snapshot: snapshot(channels: const [], clients: const []), + initialTarget: const rust.BridgeMessageTarget.server(), + ), + ), + ); + + expect(find.textContaining('<11:40:39>'), findsOneWidget); + expect(find.textContaining('"Alice"'), findsOneWidget); + expect(find.textContaining('"Lobby"'), findsOneWidget); + expect(find.byType(CircleAvatar), findsNothing); + expect(find.byType(RichText), findsWidgets); + }); + + testWidgets('chat page updates while open when backing messages change', ( + tester, + ) async { + final messages = [ + ChatEntry( + senderId: BigInt.one, + senderName: 'Sender', + message: 'First', + target: const rust.BridgeMessageTarget.server(), + ), + ]; + var currentSnapshot = snapshot(channels: const [], clients: const []); + final refresh = ValueNotifier(0); + + await tester.pumpWidget( + MaterialApp( + home: ChatPage( + messages: messages, + snapshot: currentSnapshot, + messagesSource: () => messages, + snapshotSource: () => currentSnapshot, + refreshListenable: refresh, + initialTarget: const rust.BridgeMessageTarget.server(), + ), + ), + ); + + expect(find.text('First'), findsOneWidget); + expect(find.text('Second'), findsNothing); + + messages.add( + ChatEntry( + senderId: BigInt.from(2), + senderName: 'Other', + message: 'Second', + target: const rust.BridgeMessageTarget.server(), + ), + ); + refresh.value++; + await tester.pump(); + + expect(find.text('Second'), findsOneWidget); + refresh.dispose(); + }); + test('blocks channel chat when no voice channel is joined', () { expect( canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null), diff --git a/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart b/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart index 727b16d..9922be8 100644 --- a/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart +++ b/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart @@ -17,18 +17,16 @@ void main() { expect(androidProcessingSegments.map((s) => s.value), [true, false]); }); - test('shared iOS processing segments expose VPIO and Sonora', () { - expect(iosProcessingSegments.map((s) => s.value), [ - rust.BridgeIosVoiceProcessingMode.platformVoiceProcessing, - rust.BridgeIosVoiceProcessingMode.sonoraExperimental, - ]); - }); - test('shared VAD segments expose supported non-disabled backends', () { expect(vadBackendSegments.map((s) => s.value), [ rust.BridgeVadBackend.webrtcVad, rust.BridgeVadBackend.sileroOnnx, - rust.BridgeVadBackend.tenVad, + ]); + }); + + test('desktop VAD segments expose Silero as the primary backend', () { + expect(desktopVadBackendSegments.map((s) => s.value), [ + rust.BridgeVadBackend.sileroOnnx, ]); }); diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index cebbe60..eada217 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -65,7 +65,7 @@ pub use chanora_diagnostics::{ }; pub use chanora_protocol::{ ChannelInfo, ChatMessage, ClientInfo, ConnectConfig, DisconnectReason, MessageTarget, - ProtocolError, ServerSnapshot, + ProtocolError, ServerActivity, ServerSnapshot, }; pub use chanora_storage::{Bookmark, BookmarkRepository, IdentityFileStore}; @@ -255,6 +255,11 @@ pub enum SessionEvent { /// Target scope (server/channel/private/poke). target: MessageTarget, }, + /// Human-readable TeamSpeak-style server activity. + ServerActivity { + /// Activity line text. + message: String, + }, /// Audio route changed (speaker/earpiece/BT/wired headset). AudioRouteChanged { /// New audio route. @@ -644,17 +649,32 @@ impl ChanoraSession { if cfg.identity.is_none() { let store_guard = self.identity_store.lock().await; if let Some(store) = store_guard.as_ref() { - match store.load()? { - Some(saved) => { + match store.load() { + Ok(Some(saved)) => { info!(target: "chanora_core", "reusing persisted identity"); cfg.identity = Some(saved); } - None => { + Ok(None) => { let fresh = chanora_protocol::ProtocolClient::generate_identity(); - store.save(&fresh)?; - info!(target: "chanora_core", "generated + persisted fresh identity"); + if let Err(err) = store.save(&fresh) { + warn!( + target: "chanora_core", + error = %err, + "could not persist fresh identity; continuing with in-memory identity" + ); + } else { + info!(target: "chanora_core", "generated + persisted fresh identity"); + } cfg.identity = Some(fresh); } + Err(err) => { + warn!( + target: "chanora_core", + error = %err, + "identity store read failed; continuing with in-memory identity" + ); + cfg.identity = Some(chanora_protocol::ProtocolClient::generate_identity()); + } } } } @@ -770,6 +790,25 @@ impl ChanoraSession { }); } + if let Some(activity_rx) = client.take_activity_rx() { + let ev_tx = self.events_tx.clone(); + tokio::spawn(async move { + use tokio::time::{sleep, Duration}; + let mut rx = activity_rx; + loop { + match rx.try_recv() { + Ok(ServerActivity { message }) => { + let _ = ev_tx.send(SessionEvent::ServerActivity { message }); + } + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => { + sleep(Duration::from_millis(200)).await; + } + } + } + }); + } + *guard = Some(ConnectedState { protocol: client, audio: None, @@ -904,11 +943,11 @@ impl ChanoraSession { { let dev_in = self.preferred_input_device.lock().await; let dev_out = self.preferred_output_device.lock().await; - if cfg.input_device_name.is_none() { - cfg.input_device_name = dev_in.clone(); + if cfg.input_device_id.is_none() { + cfg.input_device_id = dev_in.clone(); } - if cfg.output_device_name.is_none() { - cfg.output_device_name = dev_out.clone(); + if cfg.output_device_id.is_none() { + cfg.output_device_id = dev_out.clone(); } } @@ -1292,8 +1331,8 @@ impl ChanoraSession { Ok(()) } - /// Route-change hook (SDD-100/SRS-112). Updates audio processing - /// config and notifies Flutter of the new route. + /// Route-change hook. Updates audio processing config, bounces the + /// platform backend when needed, and notifies Flutter of the new route. pub async fn ios_handle_route_change(&self, route: AudioRoute) -> Result<(), CoreError> { let guard = self.inner.lock().await; if let Some(state) = guard.as_ref() { @@ -1301,6 +1340,14 @@ impl ChanoraSession { let mut config = audio.audio_processing_config_snapshot(); config.route = route; audio.set_audio_processing_config(config)?; + #[cfg(any(target_os = "ios", target_os = "macos"))] + { + audio.ios_restart_voice_unit()?; + } + #[cfg(target_os = "android")] + { + audio.android_restart_voice_unit()?; + } } } let _ = self @@ -1420,6 +1467,12 @@ impl ChanoraSession { channel_id: u64, password: Option, ) -> Result<(), CoreError> { + let join_started = std::time::Instant::now(); + info!( + target: "chanora_core", + channel_id, + "voice_join requested" + ); let mut guard = self.inner.lock().await; let state = guard.as_mut().ok_or(CoreError::NotConnected)?; let join_start = std::time::Instant::now(); @@ -1461,19 +1514,6 @@ impl ChanoraSession { .ok_or(CoreError::Invariant( "missing pending generation after join request", ))?; - let pending_key = state - .join_state - .pending - .filter(|pending| pending.generation == generation) - .map(|pending| channel_join::JoinOutcomeKey { - connection_epoch: pending.connection_epoch, - generation: pending.generation, - request_id: pending.request_id, - }) - .ok_or(CoreError::Invariant( - "missing pending key after join request", - ))?; - // 1. Send the move command. With send_with_result the // adapter now correlates against the server's typed // error reply, so on rejection (no permission, wrong @@ -1486,7 +1526,7 @@ impl ChanoraSession { .or_else(|| state.channel_passwords.get(&channel_id).cloned()); if let Err(e) = state .protocol - .move_to_channel(channel_id, password_to_send) + .queue_move_to_channel(channel_id, password_to_send) .await { // TS3 error 0x0302 = `channel_already_in`: we're already @@ -1520,9 +1560,11 @@ impl ChanoraSession { return Err(CoreError::Protocol(e)); } } - let _ = channel_join::reduce( - &mut state.join_state, - ChannelJoinEvent::ProtocolJoinSucceeded { key: pending_key }, + info!( + target: "chanora_core", + channel_id, + elapsed_ms = join_started.elapsed().as_millis() as u64, + "voice_join move_to_channel acknowledged" ); if let Some(pw) = requested_password { state.channel_passwords.insert(channel_id, pw); @@ -1540,37 +1582,26 @@ impl ChanoraSession { self.voice_selector.set_in_channel(true); self.emit_voice_state(projection).await; drop(guard); - // 2. Bring the audio engine up. Tolerate failure: the - // server-side channel move has ALREADY succeeded (step - // 1), so the user is in the channel from every other - // peer's perspective. Failing voice_join hard here would - // leave the UI in a phantom "you're in the channel - // visually but no controls" state because the calling - // Dart code wouldn't receive the VoiceState(true) event - // that gates the AppBar mute icons + the on-screen PTT - // button. Honest behaviour: surface the audio error - // once on the event channel (via the AudioStopped event - // consumers already handle), then continue so the UI - // matches reality — user is in the channel, but mic / - // speakers may be silent until they resolve the audio - // error (e.g. grant mic permission, plug in a working - // device). - if let Err(audio_err) = self.ensure_audio_running().await { - warn!( - target: "chanora_core", - error = %audio_err, - channel_id, - "voice_join: server move succeeded but audio engine \ - failed to start; continuing with no-audio in-channel \ - state so the UI matches the server-side state" - ); - // Tell subscribers the audio engine is not running so - // any audio-stats poll / level meter renders correctly. - // The voice_join itself still resolves Ok below so the - // UI gains the channel + mute + PTT controls. - let _ = self.events_tx.send(SessionEvent::AudioStopped); - } info!(target: "chanora_core", channel_id, "voice_join accepted by server"); + let session = self.clone(); + tokio::spawn(async move { + if let Err(audio_err) = session.ensure_audio_running().await { + warn!( + target: "chanora_core", + error = %audio_err, + channel_id, + "voice_join: deferred audio engine start failed after \ + server-side join; continuing with no-audio in-channel state" + ); + let _ = session.events_tx.send(SessionEvent::AudioStopped); + } + }); + info!( + target: "chanora_core", + channel_id, + elapsed_ms = join_started.elapsed().as_millis() as u64, + "voice_join returned to caller" + ); Ok(()) } @@ -1661,19 +1692,29 @@ impl ChanoraSession { self.voice_selector.hard_mute() } - /// Set the preferred input device name (SRS-026). Takes effect + /// Set the preferred input device id (SRS-026). Takes effect /// on the next audio engine start. - pub async fn set_input_device(&self, name: Option) -> Result<(), CoreError> { - *self.preferred_input_device.lock().await = name; + pub async fn set_input_device(&self, id: Option) -> Result<(), CoreError> { + *self.preferred_input_device.lock().await = id; Ok(()) } - /// Set the preferred output device name (SRS-026). - pub async fn set_output_device(&self, name: Option) -> Result<(), CoreError> { - *self.preferred_output_device.lock().await = name; + /// Set the preferred output device id (SRS-026). + pub async fn set_output_device(&self, id: Option) -> Result<(), CoreError> { + *self.preferred_output_device.lock().await = id; Ok(()) } + /// Read the preferred input device id, if one is pinned. + pub async fn preferred_input_device(&self) -> Option { + self.preferred_input_device.lock().await.clone() + } + + /// Read the preferred output device id, if one is pinned. + pub async fn preferred_output_device(&self) -> Option { + self.preferred_output_device.lock().await.clone() + } + /// Update the release-tail (SDD-096). Clamped to `0..=500` ms /// inclusive. Persists best-effort and re-emits voice state. pub async fn set_release_tail_ms(&self, ms: u32) -> Result<(), CoreError> { diff --git a/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index 1a14951..9bd2e93 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -17,7 +17,7 @@ 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. +# Silero VAD ONNX inference across all platforms. ndarray = "0.17" # Opus encoder. tsclientlib already pulls this; we depend explicitly so diff --git a/crates/chanora_audio/src/android_voice_unit.rs b/crates/chanora_audio/src/android_voice_unit.rs index 14a7ee8..4d06dab 100644 --- a/crates/chanora_audio/src/android_voice_unit.rs +++ b/crates/chanora_audio/src/android_voice_unit.rs @@ -145,16 +145,18 @@ struct AndroidCaptureState { voice_activity_selector: Option>, vad_detector: crate::vad::WebRtcFallbackVad, silero_vad_worker: Option, - ten_vad_worker: Option, current_vad_backend: crate::VadBackend, silero_model_epoch: u64, - ten_model_epoch: u64, capture_frame_seq: u64, vad_state: crate::voice_activity::VoiceActivityStateMachine, webrtc_apm_processor: crate::processor::WebRtcApmProcessor, audio_processing_config: Arc>, audio_processing_stats: Arc, render_reference: Arc, + input_sample_rate_hz: u32, + resample_pos: f64, + resample_last: i16, + resample_scratch: Vec, pending_10ms: [i16; crate::frame::FRAME_10MS_SAMPLES], pending_10ms_len: usize, fallback_warned_backend: Option, @@ -171,29 +173,18 @@ impl AndroidCaptureState { audio_processing_config: Arc>, audio_processing_stats: Arc, render_reference: Arc, + input_sample_rate_hz: u32, ) -> Result { let encoder = crate::opus_voice::new_voip_encoder("android")?; - // Android always uses software WebRTC APM for AEC/NS/AGC/HPF. - // The config's EffectOwner fields are resolved by open() AFTER - // hardware-effect binding; the processor is constructed here - // with all modules enabled regardless, so the resolved config - // (Platform vs WebrtcApm) only affects diagnostics, not behaviour. + // Seed the processor from the current shared config snapshot. + // `open()` may later resolve Platform-owned stages to WebRTC + // fallback (or keep them hardware-owned) once hardware-effect + // binding completes; that resolved config is pushed back into + // the live processor before the streams are started. let webrtc_apm_config = audio_processing_config .lock() - .map(|cfg| { - let mut c = crate::processor::webrtc_apm::WebRtcApmConfig::from_audio_config(&cfg); - c.aec = true; - c.ns = true; - c.agc = true; - c - }) - .unwrap_or(crate::processor::webrtc_apm::WebRtcApmConfig { - aec: true, - ns: true, - agc: true, - hpf: true, - ..Default::default() - }); + .map(|cfg| webrtc_apm_config_from_audio_config(&cfg)) + .unwrap_or_default(); Ok(Self { encoder, pcm_accum: Vec::with_capacity(crate::frame::FRAME_20MS_SAMPLES * 2), @@ -206,10 +197,8 @@ impl AndroidCaptureState { voice_activity_selector, vad_detector: crate::vad::WebRtcFallbackVad::default(), silero_vad_worker: None, - ten_vad_worker: None, current_vad_backend: crate::VadBackend::WebrtcVad, silero_model_epoch: crate::vad::silero_model_epoch(), - ten_model_epoch: crate::vad::ten_model_epoch(), capture_frame_seq: 0, vad_state: crate::voice_activity::VoiceActivityStateMachine::default(), webrtc_apm_processor: crate::processor::WebRtcApmProcessor::with_config( @@ -218,6 +207,10 @@ impl AndroidCaptureState { audio_processing_config, audio_processing_stats, render_reference, + input_sample_rate_hz: input_sample_rate_hz.max(1), + resample_pos: 0.0, + resample_last: 0, + resample_scratch: Vec::with_capacity(crate::frame::FRAME_20MS_SAMPLES * 2), pending_10ms: [0_i16; crate::frame::FRAME_10MS_SAMPLES], pending_10ms_len: 0, fallback_warned_backend: None, @@ -227,6 +220,17 @@ impl AndroidCaptureState { /// Consume i16 mono frames from Oboe. Accumulate to 10 ms chunks, /// process each through WebRTC APM + VAD, then encode 20 ms frames. fn ingest_i16(&mut self, samples: &[i16]) { + self.audio_processing_stats + .record_callback_frames(samples.len() as u64); + if self.input_sample_rate_hz != crate::frame::SAMPLE_RATE_HZ { + let resampled = self.resample_capture_to_48k(samples); + self.ingest_48k_i16(&resampled); + return; + } + self.ingest_48k_i16(samples); + } + + fn ingest_48k_i16(&mut self, samples: &[i16]) { let mut offset = 0; while offset < samples.len() { let remaining = crate::frame::FRAME_10MS_SAMPLES - self.pending_10ms_len; @@ -284,6 +288,41 @@ impl AndroidCaptureState { } } + fn resample_capture_to_48k(&mut self, samples: &[i16]) -> Vec { + if samples.is_empty() { + return Vec::new(); + } + self.resample_scratch.clear(); + let ratio = self.input_sample_rate_hz as f64 / crate::frame::SAMPLE_RATE_HZ as f64; + let mut pos = self.resample_pos; + while pos < samples.len() as f64 { + let i = pos.floor() as isize; + let frac = pos - i as f64; + let a = if i <= 0 { + self.resample_last as f64 + } else { + samples[(i - 1) as usize] as f64 + }; + let b = if i < samples.len() as isize { + samples[i as usize] as f64 + } else { + a + }; + let value = (a + frac * (b - a)) + .round() + .clamp(i16::MIN as f64, i16::MAX as f64) as i16; + self.resample_scratch.push(value); + pos += ratio; + } + self.resample_pos = pos - samples.len() as f64; + self.resample_last = *samples.last().unwrap_or(&self.resample_last); + self.resample_scratch.clone() + } + + fn set_input_sample_rate_hz(&mut self, sample_rate_hz: u32) { + self.input_sample_rate_hz = sample_rate_hz.max(1); + } + fn mark_vad_fallback_active(&mut self, failed_backend: crate::VadBackend) { if self.fallback_warned_backend != Some(failed_backend) { self.fallback_warned_backend = Some(failed_backend); @@ -317,6 +356,18 @@ impl AndroidCaptureState { self.webrtc_apm_processor.process_render(&render_ref); self.webrtc_apm_processor.process_capture(&mut frame); + let voice_activity_mode = self + .voice_activity_selector + .as_ref() + .map(|selector| selector.mode() == crate::TransmitMode::VoiceActivity) + .unwrap_or(false); + if !voice_activity_mode { + self.silero_vad_worker = None; + self.current_vad_backend = crate::VadBackend::Disabled; + self.fallback_warned_backend = None; + self.audio_processing_stats.set_vad_fallback_active(false); + } + let (vad_hangover, vad_backend) = self .audio_processing_config .try_lock() @@ -325,30 +376,28 @@ impl AndroidCaptureState { crate::voice_activity::VAD_HANGOVER_MS, crate::VadBackend::WebrtcVad, )); - self.vad_state.configure( - crate::voice_activity::VAD_OPEN_AFTER_MS, - vad_hangover, - crate::voice_activity::VAD_MIN_TX_MS, - ); + if voice_activity_mode { + self.vad_state.configure( + crate::voice_activity::VAD_OPEN_AFTER_MS, + vad_hangover, + crate::voice_activity::VAD_MIN_TX_MS, + ); + } - // VAD backend switching (mirrors iOS Raw path). + // VAD backend switching only while VoiceActivity mode is active. let silero_epoch = crate::vad::silero_model_epoch(); - let ten_epoch = crate::vad::ten_model_epoch(); - let silero_changed = - vad_backend == crate::VadBackend::SileroOnnx && silero_epoch != self.silero_model_epoch; - let ten_changed = - vad_backend == crate::VadBackend::TenVad && ten_epoch != self.ten_model_epoch; - if vad_backend != self.current_vad_backend || silero_changed || ten_changed { + let silero_changed = voice_activity_mode + && vad_backend == crate::VadBackend::SileroOnnx + && silero_epoch != self.silero_model_epoch; + if voice_activity_mode && (vad_backend != self.current_vad_backend || silero_changed) { self.current_vad_backend = vad_backend; self.silero_model_epoch = silero_epoch; - self.ten_model_epoch = ten_epoch; self.fallback_warned_backend = None; match vad_backend { crate::VadBackend::SileroOnnx => { let path = crate::vad::silero_model_bundle_path(); self.silero_vad_worker = crate::vad::silero_onnx::SileroOnnxVadWorker::try_new(&path); - self.ten_vad_worker = None; if self.silero_vad_worker.is_none() { warn!( target: "chanora_audio", @@ -356,48 +405,43 @@ impl AndroidCaptureState { ); } } - crate::VadBackend::TenVad => { - let path = crate::vad::ten_model_bundle_path(); - self.ten_vad_worker = crate::vad::TenOnnxVadWorker::try_new(&path); - self.silero_vad_worker = None; - if self.ten_vad_worker.is_none() { - warn!( - target: "chanora_audio", - "android: TEN VAD ONNX model not found at {path}; falling back to WebRTC VAD" - ); - } - } _ => { self.silero_vad_worker = None; - self.ten_vad_worker = None; } } self.vad_state.reset(); } - self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); - let capture_seq = self.capture_frame_seq; - let mut used_fallback_vad = false; - let vad = if vad_backend == crate::VadBackend::Disabled { - crate::vad::VadOutput { - probability: 1.0, - speech: true, - } - } else if vad_backend == crate::VadBackend::SileroOnnx { - if let Some(worker) = self.silero_vad_worker.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let p = worker.latest_probability(); - crate::vad::VadOutput { - probability: p, - speech: p >= 0.5, - } - } else if enqueued { - // Warm-up: worker dispatched but hasn't finished yet. - // Default to no-speech until first result arrives (~100ms). - crate::vad::VadOutput { - probability: 0.0, - speech: false, + let (vad_probability, active) = if voice_activity_mode { + self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); + let capture_seq = self.capture_frame_seq; + let mut used_fallback_vad = false; + let vad = if vad_backend == crate::VadBackend::Disabled { + crate::vad::VadOutput { + probability: 1.0, + speech: true, + } + } else if vad_backend == crate::VadBackend::SileroOnnx { + if let Some(worker) = self.silero_vad_worker.as_ref() { + let enqueued = worker.try_send(capture_seq, &frame); + if !worker.is_stale(capture_seq) { + let p = worker.latest_probability(); + crate::vad::VadOutput { + probability: p, + speech: p >= 0.5, + } + } else if enqueued { + crate::vad::VadOutput { + probability: 0.0, + speech: false, + } + } else { + used_fallback_vad = true; + self.mark_vad_fallback_active(vad_backend); + crate::vad::VoiceActivityDetector::process_10ms( + &mut self.vad_detector, + &frame, + ) } } else { used_fallback_vad = true; @@ -405,53 +449,28 @@ impl AndroidCaptureState { crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) } } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } - } else if vad_backend == crate::VadBackend::TenVad { - if let Some(worker) = self.ten_vad_worker.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let p = worker.latest_probability(); - crate::vad::VadOutput { - probability: p, - speech: p >= 0.5, - } - } else if enqueued { - crate::vad::VadOutput { - probability: 0.0, - speech: false, - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } + }; + self.audio_processing_stats + .set_vad_fallback_active(used_fallback_vad); + (vad.probability, self.vad_state.update(vad.speech)) } else { - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) + (0.0, false) }; - self.audio_processing_stats - .set_vad_fallback_active(used_fallback_vad); - let active = self.vad_state.update(vad.speech); - let output_muted = self.output_muted.load(Ordering::Relaxed); if let Some(sel) = &self.voice_activity_selector { - sel.set_voice_activity_open(active && !output_muted); + sel.set_voice_activity_open(voice_activity_mode && active); } self.audio_processing_stats.update_capture( input_dbfs, crate::frame::dbfs(&frame), - vad.probability, - active && !output_muted, + vad_probability, + voice_activity_mode && active, self.transmit_active.load(Ordering::Relaxed), ); + self.audio_processing_stats + .record_capture_frame(frame.iter().all(|sample| sample.abs() < 1.0e-6)); - if !self.transmit_active.load(Ordering::Relaxed) || output_muted { + if !self.transmit_active.load(Ordering::Relaxed) { return; } @@ -511,6 +530,9 @@ struct OutputCallback { event_tx: BackendEventTx, scratch: Arc>>, render_reference: Arc, + audio_processing_stats: Arc, + pending_render_ref: [f32; crate::frame::FRAME_10MS_SAMPLES], + pending_render_ref_len: usize, } impl AudioOutputCallback for OutputCallback { @@ -552,19 +574,20 @@ impl AudioOutputCallback for OutputCallback { gain, muted, ); + self.audio_processing_stats + .update_render(crate::frame::dbfs(&scratch[..needed]), frames.len() as u32); - // Write the first 10 ms of render audio into the reference - // buffer for the capture-side AEC. - let mono_n = needed / 2; - let render_n = mono_n.min(crate::frame::FRAME_10MS_SAMPLES); - let mut ref_frame = [0.0_f32; crate::frame::FRAME_10MS_SAMPLES]; - for (i, chunk) in scratch[..render_n * 2].chunks_exact(2).enumerate() { - if i >= render_n { - break; + // Accumulate the full render callback into 10 ms mono chunks so + // AEC sees consistent reference timing even when output callbacks + // are shorter or longer than 10 ms. + for chunk in scratch[..needed].chunks_exact(2) { + self.pending_render_ref[self.pending_render_ref_len] = (chunk[0] + chunk[1]) * 0.5; + self.pending_render_ref_len += 1; + if self.pending_render_ref_len == crate::frame::FRAME_10MS_SAMPLES { + self.render_reference.write(&self.pending_render_ref); + self.pending_render_ref_len = 0; } - ref_frame[i] = (chunk[0] + chunk[1]) * 0.5; } - self.render_reference.write(&ref_frame); })); DataCallbackResult::Continue } @@ -640,6 +663,7 @@ impl AndroidVoiceUnit { // Clone the APM config Arc before params is partially moved // into the capture state constructor below. let apm_config_clone = params.audio_processing_config.clone(); + let audio_processing_stats = params.audio_processing_stats.clone(); let capture_state = Arc::new(Mutex::new( AndroidCaptureState::new( @@ -650,8 +674,9 @@ impl AndroidVoiceUnit { params.mic_gain, params.voice_activity_selector, params.audio_processing_config, - params.audio_processing_stats, + audio_processing_stats.clone(), render_ref_for_capture, + cfg.sample_rate, ) .map_err(|e| BackendError::OpenFailed(format!("capture state init: {e}")))?, )); @@ -768,6 +793,9 @@ impl AndroidVoiceUnit { event_tx: event_tx.clone(), scratch: scratch.clone(), render_reference: render_ref_for_output, + audio_processing_stats: audio_processing_stats.clone(), + pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES], + pending_render_ref_len: 0, }; let output_builder = output_builder.set_callback(output_cb); @@ -785,6 +813,7 @@ impl AndroidVoiceUnit { params.handler.clone(), params.output_gain.clone(), params.output_muted.clone(), + audio_processing_stats.clone(), scratch.clone(), render_ref_buf, )? @@ -869,6 +898,14 @@ impl AndroidVoiceUnit { "android: audio processing config resolved (hardware effects: aec={hw_aec} ns={hw_ns} agc={hw_agc})" ); } + if let Ok(mut capture) = capture_state.lock() { + capture.set_input_sample_rate_hz(input_sample_rate.max(1) as u32); + let resolved_cfg = apm_config_clone + .lock() + .map(|cfg| webrtc_apm_config_from_audio_config(&cfg)) + .unwrap_or_default(); + capture.webrtc_apm_processor.apply_config(resolved_cfg); + } // --- SDD-112 item 10 / SDD-113 item 7 / SDD-116 item 3 --- // Publish the diagnostics snapshot. Per-effect engagement is @@ -910,6 +947,9 @@ impl AndroidVoiceUnit { latency_tier: latency_tier_for(input_perf), }; publish_android_audio_diagnostics(diagnostics); + params + .audio_processing_stats + .set_actual_sample_rate_hz(input_sample_rate.max(0) as u32); Ok(Self { input: input_stream, @@ -1005,6 +1045,7 @@ impl AndroidVoiceUnit { handler: Arc>>, output_gain: Arc, output_muted: Arc, + audio_processing_stats: Arc, scratch: Arc>>, render_reference: Arc, ) -> Result, BackendError> { @@ -1015,6 +1056,9 @@ impl AndroidVoiceUnit { event_tx: event_tx.clone(), scratch, render_reference, + audio_processing_stats, + pending_render_ref: [0.0_f32; crate::frame::FRAME_10MS_SAMPLES], + pending_render_ref_len: 0, }; let builder = AudioStreamBuilder::default() .set_direction::() @@ -1038,6 +1082,12 @@ impl AndroidVoiceUnit { } } +fn webrtc_apm_config_from_audio_config( + config: &crate::AudioProcessingConfig, +) -> crate::processor::webrtc_apm::WebRtcApmConfig { + crate::processor::webrtc_apm::WebRtcApmConfig::from_audio_config(config) +} + impl MobileVoiceAudioBackend for AndroidVoiceUnit { fn start(&mut self) -> Result<(), BackendError> { if let Some(s) = self.input.as_mut() { diff --git a/crates/chanora_audio/src/audio_processing.rs b/crates/chanora_audio/src/audio_processing.rs index a0dddec..f7ec530 100644 --- a/crates/chanora_audio/src/audio_processing.rs +++ b/crates/chanora_audio/src/audio_processing.rs @@ -92,9 +92,6 @@ impl AudioBackend { pub enum VadBackend { /// Silero ONNX VAD. P1 schema default when model/runtime exist. SileroOnnx, - /// TEN VAD backend. Native TEN runtime is optional; unavailable - /// builds fall back to the realtime-safe WebRTC detector. - TenVad, /// WebRTC-style fallback VAD. WebrtcVad, /// Debug-only energy VAD. @@ -108,7 +105,6 @@ impl VadBackend { pub fn as_str(self) -> &'static str { match self { Self::SileroOnnx => "silero_vad_onnx", - Self::TenVad => "ten_vad", Self::WebrtcVad => "webrtc_vad", Self::EnergyDebug => "energy_debug", Self::Disabled => "disabled", @@ -168,7 +164,7 @@ impl Default for AudioProcessingConfig { route: AudioRoute::Speaker, ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing, processing_backend: AudioBackend::PlatformVoiceProcessing, - vad_backend: VadBackend::TenVad, + vad_backend: VadBackend::SileroOnnx, aec: EffectOwner::Platform, // iOS VPIO owns NS/AGC on the default shipping path. Software // effects are opt-in through the experimental raw route only. @@ -246,7 +242,7 @@ mod tests { assert_eq!(config.aec, EffectOwner::Platform); assert_eq!(config.ns, EffectOwner::Platform); assert_eq!(config.agc, EffectOwner::Platform); - assert_eq!(config.vad_backend, VadBackend::TenVad); + assert_eq!(config.vad_backend, VadBackend::SileroOnnx); } #[test] @@ -259,11 +255,6 @@ mod tests { assert!(config.validate_for_ios().is_err()); } - #[test] - fn ten_vad_has_stable_debug_string() { - assert_eq!(VadBackend::TenVad.as_str(), "ten_vad"); - } - #[test] fn raw_processing_allows_full_webrtc_apm_chain() { let config = AudioProcessingConfig { @@ -342,6 +333,16 @@ pub struct AudioProcessingStats { pub callback_xruns: u64, /// Clipped sample count. pub clipped_samples: u64, + /// Number of effectively silent processed capture frames. + pub zero_frames: u64, + /// Number of processed capture frames. + pub capture_frames: u64, + /// Number of input callbacks carrying 10 ms of audio. + pub callbacks_10ms: u64, + /// Number of input callbacks carrying 20 ms of audio. + pub callbacks_20ms: u64, + /// Number of input callbacks carrying any other size. + pub callbacks_other: u64, /// Sonora enabled. pub sonora_enabled: bool, /// Platform voice processing enabled. @@ -361,6 +362,11 @@ pub struct SharedAudioProcessingStats { output_underruns: AtomicU64, callback_xruns: AtomicU64, clipped_samples: AtomicU64, + zero_frames: AtomicU64, + capture_frames: AtomicU64, + callbacks_10ms: AtomicU64, + callbacks_20ms: AtomicU64, + callbacks_other: AtomicU64, actual_sample_rate_hz: AtomicU32, actual_io_buffer_frames: AtomicU32, } @@ -379,6 +385,11 @@ impl Default for SharedAudioProcessingStats { output_underruns: AtomicU64::new(0), callback_xruns: AtomicU64::new(0), clipped_samples: AtomicU64::new(0), + zero_frames: AtomicU64::new(0), + capture_frames: AtomicU64::new(0), + callbacks_10ms: AtomicU64::new(0), + callbacks_20ms: AtomicU64::new(0), + callbacks_other: AtomicU64::new(0), actual_sample_rate_hz: AtomicU32::new(crate::frame::SAMPLE_RATE_HZ), actual_io_buffer_frames: AtomicU32::new(crate::frame::FRAME_20MS_SAMPLES as u32), } @@ -412,6 +423,12 @@ impl SharedAudioProcessingStats { .store(io_buffer_frames, Ordering::Relaxed); } + /// Record the actual device sample rate. + pub fn set_actual_sample_rate_hz(&self, sample_rate_hz: u32) { + self.actual_sample_rate_hz + .store(sample_rate_hz, Ordering::Relaxed); + } + /// Increment output underrun count. pub fn increment_output_underrun(&self) { self.output_underruns.fetch_add(1, Ordering::Relaxed); @@ -427,6 +444,32 @@ impl SharedAudioProcessingStats { self.clipped_samples.fetch_add(count, Ordering::Relaxed); } + /// Record one processed capture frame and whether it was effectively silent. + pub fn record_capture_frame(&self, zero_frame: bool) { + self.capture_frames.fetch_add(1, Ordering::Relaxed); + if zero_frame { + self.zero_frames.fetch_add(1, Ordering::Relaxed); + } + } + + /// Bucket callback delivery sizes to diagnose timing jitter and packetization. + pub fn record_callback_frames(&self, frames: u64) { + let sample_rate_hz = self.actual_sample_rate_hz.load(Ordering::Relaxed).max(1); + let frames_10ms = (sample_rate_hz / 100) as u64; + let frames_20ms = (sample_rate_hz / 50) as u64; + match frames { + value if value == frames_10ms => { + self.callbacks_10ms.fetch_add(1, Ordering::Relaxed); + } + value if value == frames_20ms => { + self.callbacks_20ms.fetch_add(1, Ordering::Relaxed); + } + _ => { + self.callbacks_other.fetch_add(1, Ordering::Relaxed); + } + } + } + /// Store whether the selected VAD backend is currently using a fallback. pub fn set_vad_fallback_active(&self, active: bool) { self.vad_fallback_active.store(active, Ordering::Relaxed); @@ -452,6 +495,11 @@ impl SharedAudioProcessingStats { output_underruns: self.output_underruns.load(Ordering::Relaxed), callback_xruns: self.callback_xruns.load(Ordering::Relaxed), clipped_samples: self.clipped_samples.load(Ordering::Relaxed), + zero_frames: self.zero_frames.load(Ordering::Relaxed), + capture_frames: self.capture_frames.load(Ordering::Relaxed), + callbacks_10ms: self.callbacks_10ms.load(Ordering::Relaxed), + callbacks_20ms: self.callbacks_20ms.load(Ordering::Relaxed), + callbacks_other: self.callbacks_other.load(Ordering::Relaxed), sonora_enabled: config.processing_backend == AudioBackend::Sonora, platform_voice_processing_enabled: config.processing_backend == AudioBackend::PlatformVoiceProcessing, diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index c2882a2..376ee23 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -20,6 +20,18 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; not(target_os = "android") ))] use cpal::{SampleFormat, SizedSample}; +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] +use std::collections::hash_map::DefaultHasher; +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] +use std::hash::{Hash, Hasher}; use tokio::sync::mpsc; use tracing::{debug, info}; // `error!` and `warn!` are used only inside the cpal capture / @@ -80,12 +92,77 @@ pub struct AudioDeviceList { /// Info about a single audio device. #[derive(Debug, Clone)] pub struct AudioDeviceInfo { + /// Stable platform-reported device identifier. + pub id: String, /// Human-readable device name from the OS. pub name: String, + /// Additional device details useful for disambiguation. + pub details: String, /// True if the OS reports this as the default device. pub is_default: bool, } +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] +fn desktop_device_id(device: &cpal::Device) -> Option { + device.id().ok().map(|id| id.to_string()) +} + +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] +fn short_device_id(id: &str) -> String { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + format!("{:016x}", hasher.finish()) +} + +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] +fn describe_device(device: &cpal::Device) -> Option { + let id = desktop_device_id(device)?; + let description = device.description().ok(); + let name = description + .as_ref() + .map(|d| d.name().trim().to_owned()) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("Device {}", &short_device_id(&id)[..8])); + + let mut details = Vec::new(); + if let Some(description) = description.as_ref() { + if let Some(manufacturer) = description.manufacturer() { + details.push(manufacturer.to_owned()); + } + if let Some(driver) = description.driver() { + details.push(driver.to_owned()); + } + let device_type = description.device_type(); + if device_type != cpal::device_description::DeviceType::Unknown { + details.push(format!("{device_type}")); + } + let interface_type = description.interface_type(); + if interface_type != cpal::device_description::InterfaceType::Unknown { + details.push(format!("{interface_type}")); + } + } + details.push(format!("id={}", short_device_id(&id))); + + Some(AudioDeviceInfo { + id, + name, + details: details.join(" · "), + is_default: false, + }) +} + /// Enumerate available audio input and output devices. /// On mobile platforms (iOS, Android) returns an empty list because /// device selection is managed by the OS audio session. @@ -100,38 +177,30 @@ pub fn list_audio_devices() -> AudioDeviceList { input_devices: Vec::new(), output_devices: Vec::new(), }; - let Ok(host) = cpal::default_host() else { - return list; - }; - let default_in = host.default_input_device(); - let default_out = host.default_output_device(); + let host = cpal::default_host(); + let default_in = host + .default_input_device() + .and_then(|device| desktop_device_id(&device)); + let default_out = host + .default_output_device() + .and_then(|device| desktop_device_id(&device)); if let Ok(devices) = host.input_devices() { for d in devices { - let name = d - .description() - .map(|n| n.name().to_owned()) - .unwrap_or_default(); - if !name.is_empty() { - let is_default = default_in + if let Some(mut device) = describe_device(&d) { + device.is_default = default_in .as_ref() - .is_some_and(|di| di.description().is_ok_and(|dn| dn.name() == name.as_str())); - list.input_devices - .push(AudioDeviceInfo { name, is_default }); + .is_some_and(|default_id| default_id == &device.id); + list.input_devices.push(device); } } } if let Ok(devices) = host.output_devices() { for d in devices { - let name = d - .description() - .map(|n| n.name().to_owned()) - .unwrap_or_default(); - if !name.is_empty() { - let is_default = default_out + if let Some(mut device) = describe_device(&d) { + device.is_default = default_out .as_ref() - .is_some_and(|di| di.description().is_ok_and(|dn| dn.name() == name.as_str())); - list.output_devices - .push(AudioDeviceInfo { name, is_default }); + .is_some_and(|default_id| default_id == &device.id); + list.output_devices.push(device); } } } @@ -172,12 +241,12 @@ pub struct AudioEngineConfig { /// is rejected on Android because the P0 path intentionally has /// no generic mobile-audio fallback. pub mobile_voice_preset: bool, - /// Optional input device name override. When `None`, the system - /// default input device is used. Set to a device name from + /// Optional input device id override. When `None`, the system + /// default input device is used. Set to a device id from /// [`list_audio_devices`] to pin a specific microphone. - pub input_device_name: Option, - /// Optional output device name override. - pub output_device_name: Option, + pub input_device_id: Option, + /// Optional output device id override. + pub output_device_id: Option, /// Optional selector used by P1 VoiceActivity to publish VAD state. #[doc(hidden)] pub voice_activity_selector: Option>, @@ -190,8 +259,8 @@ impl std::fmt::Debug for AudioEngineConfig { .field("ptt_initial", &self.ptt_initial) .field("effects", &self.effects) .field("mobile_voice_preset", &self.mobile_voice_preset) - .field("input_device_name", &self.input_device_name) - .field("output_device_name", &self.output_device_name) + .field("input_device_id", &self.input_device_id) + .field("output_device_id", &self.output_device_id) .field( "voice_activity_selector", &self.voice_activity_selector.as_ref().map(|_| "present"), @@ -207,8 +276,8 @@ impl Default for AudioEngineConfig { ptt_initial: false, effects: crate::AudioEffects::default(), mobile_voice_preset: true, - input_device_name: None, - output_device_name: None, + input_device_id: None, + output_device_id: None, voice_activity_selector: None, } } @@ -237,13 +306,13 @@ pub struct AudioEngine { audio_processing_config: Arc>, audio_processing_stats: Arc, audio_handler: Arc>>, - #[cfg(any(target_os = "ios", target_os = "macos"))] + #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] voice_out_tx: mpsc::Sender, - #[cfg(any(target_os = "ios", target_os = "macos"))] + #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] voice_activity_selector: Option>, - #[cfg(any(target_os = "ios", target_os = "macos"))] + #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] mic_gain: f32, - + #[cfg(any(target_os = "ios", target_os = "macos"))] // Streams must be dropped to stop audio. Both are `!Send` because // cpal's Stream isn't Send on some backends; we keep them in an // Option wrapped by Mutex so stop() can move them out. On Linux @@ -277,7 +346,11 @@ pub struct AudioEngine { /// unbinds effects and stops the service in SDD-115 reverse /// order. #[cfg(target_os = "android")] - _android_voice_unit: Mutex>, + _android_voice_unit: Arc>>, + /// Persist the Android stream request so route-change reopen uses the + /// same effect and latency policy as the original session start. + #[cfg(target_os = "android")] + android_voice_stream_config: Arc>, /// SDD-108 §1/§2: refcount-composable audio-mode controller. /// Snapshots `AudioManager.getMode()` on the 0 → 1 transition and /// restores it on the 1 → 0 transition. Held in a `Mutex` so the @@ -286,7 +359,7 @@ pub struct AudioEngine { /// mode lifecycle is bound to the voice-session lifecycle /// (SDD-108 §3). #[cfg(target_os = "android")] - audio_mode_stack: Mutex, + audio_mode_stack: Arc>, // Hand the inbound-voice forwarder task a shutdown signal. shutdown_tx: Option>, /// True if the capture stream actually opened. If false (typical @@ -385,6 +458,183 @@ fn open_ios_voice_backend( } impl AudioEngine { + #[cfg(target_os = "android")] + fn spawn_android_backend_event_task( + android_voice_unit: Arc>>, + android_voice_stream_config: Arc< + Mutex, + >, + voice_out_tx: mpsc::Sender, + transmit_gate: crate::ptt::AudioTransmitGate, + frames_sent: Arc, + audio_handler: Arc>>, + output_gain: Arc, + output_muted: Arc, + voice_activity_selector: Option>, + audio_processing_config: Arc>, + audio_processing_stats: Arc, + mic_gain: f32, + audio_mode_stack: Arc>, + mut event_rx: crate::mobile_voice_backend::BackendEventRx, + ) { + use crate::mobile_voice_backend::BackendEvent; + + tokio::spawn(async move { + while let Some(event) = event_rx.recv().await { + match event { + BackendEvent::Disconnected => { + warn!(target: "chanora_audio", "android: backend disconnected; reopening voice unit"); + 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) = android_voice_unit.lock().unwrap().take() { + use crate::mobile_voice_backend::MobileVoiceAudioBackend; + if let Err(e) = unit.close() { + warn!( + target: "chanora_audio", + error = %e, + "android: failed to close disconnected voice unit before reopen" + ); + } + } + + if crate::android_voice_unit::chanora_android_start_voice_service() { + info!( + target: "chanora_audio", + "android: voice foreground service restart dispatched after backend disconnect" + ); + } + + match android_get_audio_mode() { + Ok(current_mode) => { + if current_mode != ANDROID_MODE_IN_COMMUNICATION { + match android_set_audio_mode(ANDROID_MODE_IN_COMMUNICATION) { + Ok(()) => info!( + target: "chanora_audio", + prior_mode = current_mode, + "android: AudioManager mode re-engaged after backend disconnect" + ), + Err(e) => warn!( + target: "chanora_audio", + error = %e, + prior_mode = current_mode, + "android: failed to re-engage MODE_IN_COMMUNICATION after backend disconnect" + ), + } + } + } + Err(e) => warn!( + target: "chanora_audio", + error = %e, + "android: AudioManager.getMode failed during backend reopen" + ), + } + + let cfg_av = android_voice_stream_config.lock().unwrap().clone(); + let params = crate::mobile_voice_backend::VoiceAudioParams { + voice_out_tx: voice_out_tx.clone(), + transmit_active: transmit_gate.flag_arc(), + frames_sent: frames_sent.clone(), + mic_gain, + handler: audio_handler.clone(), + output_gain: output_gain.clone(), + output_muted: output_muted.clone(), + voice_activity_selector: voice_activity_selector.clone(), + audio_processing_config: audio_processing_config.clone(), + audio_processing_stats: audio_processing_stats.clone(), + }; + + match crate::android_voice_unit::AndroidVoiceUnit::open(&cfg_av, params) { + Ok(mut reopened) => { + use crate::mobile_voice_backend::MobileVoiceAudioBackend; + match reopened.start() { + Ok(()) => { + if let Some(next_rx) = reopened.take_event_rx() { + Self::spawn_android_backend_event_task( + android_voice_unit.clone(), + android_voice_stream_config.clone(), + voice_out_tx.clone(), + transmit_gate.clone(), + frames_sent.clone(), + audio_handler.clone(), + output_gain.clone(), + output_muted.clone(), + voice_activity_selector.clone(), + audio_processing_config.clone(), + audio_processing_stats.clone(), + mic_gain, + audio_mode_stack.clone(), + next_rx, + ); + } + crate::android_voice_unit::register_global_event_sender( + reopened.event_sender(), + ); + if crate::android_voice_unit::chanora_android_request_audio_focus() { + info!( + target: "chanora_audio", + "android: audio focus re-requested after backend disconnect" + ); + } + if crate::android_voice_unit::chanora_android_start_bluetooth_sco() { + info!( + target: "chanora_audio", + "android: bluetooth route re-engaged after backend disconnect" + ); + } + *android_voice_unit.lock().unwrap() = Some(reopened); + } + Err(e) => warn!( + target: "chanora_audio", + error = %e, + "android: reopened voice unit failed to start after backend disconnect" + ), + } + } + Err(e) => warn!( + target: "chanora_audio", + error = %e, + "android: failed to reopen voice unit after backend disconnect" + ), + } + } + 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)" + ); + } + } + } + }); + } + /// Start the engine: open capture + playback streams, spawn the /// inbound-voice forwarder, return a handle. pub fn start( @@ -461,21 +711,22 @@ impl AudioEngine { "starting audio engine: cpal host selected" ); - /// Helper: find a device by name, falling back to default. - fn find_device( + /// Helper: find a device by stable id, falling back to default. + fn find_device( host: &cpal::Host, - default_fn: fn(&cpal::Host) -> Option, - all_fn: fn(&cpal::Host) -> Result, + default_fn: DefaultFn, + all_fn: AllFn, prefer: Option<&str>, - ) -> Option { - if let Some(name) = prefer { + ) -> Option + where + DefaultFn: Fn(&cpal::Host) -> Option, + AllFn: Fn(&cpal::Host) -> Result, + Devices: IntoIterator, + { + if let Some(id) = prefer { if let Ok(devices) = all_fn(host) { for d in devices { - let dn = d - .description() - .map(|n| n.name().to_owned()) - .unwrap_or_default(); - if dn == name { + if desktop_device_id(&d).as_deref() == Some(id) { return Some(d); } } @@ -488,7 +739,7 @@ impl AudioEngine { &host, cpal::Host::default_input_device, cpal::Host::input_devices, - cfg.input_device_name.as_deref(), + cfg.input_device_id.as_deref(), ) .ok_or(AudioError::NoInputDevice)?; @@ -496,7 +747,7 @@ impl AudioEngine { &host, cpal::Host::default_output_device, cpal::Host::output_devices, - cfg.output_device_name.as_deref(), + cfg.output_device_id.as_deref(), ) .ok_or(AudioError::NoOutputDevice)?; @@ -729,7 +980,7 @@ impl AudioEngine { mut voice_in_rx: mpsc::Receiver, transmit_gate: crate::ptt::AudioTransmitGate, ) -> Result { - use crate::mobile_voice_backend::{BackendEvent, MobileVoiceAudioBackend}; + use crate::mobile_voice_backend::MobileVoiceAudioBackend; info!(target: "chanora_audio", "starting audio engine: Android Oboe backend"); @@ -799,7 +1050,7 @@ impl AudioEngine { ..Default::default() }; let params = crate::mobile_voice_backend::VoiceAudioParams { - voice_out_tx, + voice_out_tx: voice_out_tx.clone(), transmit_active: transmit_flag_for_capture, frames_sent: frames_sent.clone(), mic_gain: cfg.mic_gain, @@ -821,53 +1072,42 @@ 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 { - 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)" - ); - } - } - } - }); + let android_voice_unit = Arc::new(Mutex::new(Some(android_voice_unit))); + let android_voice_stream_config = Arc::new(Mutex::new(cfg_av.clone())); + let audio_mode_stack = Arc::new(Mutex::new(audio_mode_stack)); + + if let Some(event_rx) = android_voice_unit + .lock() + .unwrap() + .as_mut() + .and_then(|unit| unit.take_event_rx()) + { + Self::spawn_android_backend_event_task( + android_voice_unit.clone(), + android_voice_stream_config.clone(), + voice_out_tx.clone(), + transmit_gate.clone(), + frames_sent.clone(), + audio_handler.clone(), + output_gain.clone(), + output_muted.clone(), + cfg.voice_activity_selector.clone(), + audio_processing_config.clone(), + audio_processing_stats.clone(), + cfg.mic_gain, + audio_mode_stack.clone(), + event_rx, + ); } - crate::android_voice_unit::register_global_event_sender(android_voice_unit.event_sender()); + crate::android_voice_unit::register_global_event_sender( + android_voice_unit + .lock() + .unwrap() + .as_ref() + .expect("android voice unit installed") + .event_sender(), + ); if crate::android_voice_unit::chanora_android_request_audio_focus() { info!( @@ -932,8 +1172,12 @@ impl AudioEngine { audio_processing_config, audio_processing_stats, audio_handler, - _android_voice_unit: Mutex::new(Some(android_voice_unit)), - audio_mode_stack: Mutex::new(audio_mode_stack), + voice_out_tx, + voice_activity_selector: cfg.voice_activity_selector.clone(), + mic_gain: cfg.mic_gain, + _android_voice_unit: android_voice_unit, + android_voice_stream_config, + audio_mode_stack, shutdown_tx: Some(shutdown_tx), capture_active, }) @@ -1182,6 +1426,96 @@ impl AudioEngine { } } + /// Android-only: reopen the underlying Oboe voice backend after a + /// route/device change while preserving the engine-owned state. + pub fn android_restart_voice_unit(&self) -> Result<(), AudioError> { + #[cfg(target_os = "android")] + { + use crate::mobile_voice_backend::MobileVoiceAudioBackend; + + 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() { + unit.close().map_err(|e| { + AudioError::Backend(format!( + "android: failed to close Oboe voice unit during route restart: {e}" + )) + })?; + } + + if crate::android_voice_unit::chanora_android_start_voice_service() { + info!( + target: "chanora_audio", + "android: voice foreground service restart dispatched after route change" + ); + } + + if crate::android_voice_unit::chanora_android_request_audio_focus() { + info!( + target: "chanora_audio", + "android: audio focus re-requested after route change" + ); + } + + let cfg_av = self.android_voice_stream_config.lock().unwrap().clone(); + let params = crate::mobile_voice_backend::VoiceAudioParams { + voice_out_tx: self.voice_out_tx.clone(), + transmit_active: self.transmit_gate.flag_arc(), + frames_sent: self.frames_sent.clone(), + mic_gain: self.mic_gain, + handler: self.audio_handler.clone(), + output_gain: self.output_gain.clone(), + output_muted: self.output_muted.clone(), + voice_activity_selector: self.voice_activity_selector.clone(), + audio_processing_config: self.audio_processing_config.clone(), + audio_processing_stats: self.audio_processing_stats.clone(), + }; + let mut reopened = crate::android_voice_unit::AndroidVoiceUnit::open(&cfg_av, params) + .map_err(|e| { + AudioError::Backend(format!("android: failed to reopen Oboe voice unit: {e}")) + })?; + reopened.start().map_err(|e| { + AudioError::Backend(format!("android: failed to restart Oboe voice unit: {e}")) + })?; + if let Some(event_rx) = reopened.take_event_rx() { + Self::spawn_android_backend_event_task( + self._android_voice_unit.clone(), + self.android_voice_stream_config.clone(), + self.voice_out_tx.clone(), + self.transmit_gate.clone(), + self.frames_sent.clone(), + self.audio_handler.clone(), + self.output_gain.clone(), + self.output_muted.clone(), + self.voice_activity_selector.clone(), + self.audio_processing_config.clone(), + self.audio_processing_stats.clone(), + self.mic_gain, + self.audio_mode_stack.clone(), + event_rx, + ); + } + crate::android_voice_unit::register_global_event_sender(reopened.event_sender()); + + if crate::android_voice_unit::chanora_android_start_bluetooth_sco() { + info!( + target: "chanora_audio", + "android: bluetooth route re-engaged after route change" + ); + } + + let mut guard = self._android_voice_unit.lock().unwrap(); + *guard = Some(reopened); + Ok(()) + } + #[cfg(not(target_os = "android"))] + { + Ok(()) + } + } + /// iOS-only: pause the underlying VoiceProcessingIO unit. pub fn ios_pause_voice_unit(&self) -> Result<(), AudioError> { #[cfg(any(target_os = "ios", target_os = "macos"))] @@ -2086,7 +2420,7 @@ pub fn android_set_audio_mode(mode: i32) -> Result<(), AudioModeError> { #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] #[doc(hidden)] pub mod bench_seam { - use super::{Arc, AtomicBool, AtomicU32, CaptureState, OpusEncoder, OutPacket}; + use super::{Arc, AtomicBool, AtomicU32, CaptureState, OutPacket}; use tokio::sync::mpsc; /// Opaque handle wrapping a CaptureState plus the dummy mpsc diff --git a/crates/chanora_audio/src/ios_raw_unit.rs b/crates/chanora_audio/src/ios_raw_unit.rs index f084544..46275e3 100644 --- a/crates/chanora_audio/src/ios_raw_unit.rs +++ b/crates/chanora_audio/src/ios_raw_unit.rs @@ -117,10 +117,8 @@ mod inner { voice_activity_selector: Option>, vad_detector: crate::vad::WebRtcFallbackVad, silero_vad_worker: Option, - ten_vad_worker: Option, current_vad_backend: crate::VadBackend, silero_model_epoch: u64, - ten_model_epoch: u64, capture_frame_seq: u64, vad_state: crate::voice_activity::VoiceActivityStateMachine, /// Processing config — retained for route-change reloads. @@ -157,10 +155,8 @@ mod inner { voice_activity_selector: params.voice_activity_selector.clone(), vad_detector: crate::vad::WebRtcFallbackVad::default(), silero_vad_worker: None, - ten_vad_worker: None, current_vad_backend: crate::VadBackend::WebrtcVad, silero_model_epoch: crate::vad::silero_model_epoch(), - ten_model_epoch: crate::vad::ten_model_epoch(), capture_frame_seq: 0, vad_state: crate::voice_activity::VoiceActivityStateMachine::default(), audio_processing_config: params.audio_processing_config.clone(), @@ -274,6 +270,18 @@ mod inner { rec.push_processed_mic(&frame); } + let voice_activity_mode = self + .voice_activity_selector + .as_ref() + .map(|selector| selector.mode() == crate::TransmitMode::VoiceActivity) + .unwrap_or(false); + if !voice_activity_mode { + self.silero_vad_worker = None; + self.current_vad_backend = crate::VadBackend::Disabled; + self.fallback_warned_backend = None; + self.audio_processing_stats.set_vad_fallback_active(false); + } + let (vad_backend, vad_hangover) = self .audio_processing_config .try_lock() @@ -282,30 +290,28 @@ mod inner { crate::VadBackend::WebrtcVad, crate::voice_activity::VAD_HANGOVER_MS, )); - self.vad_state.configure( - crate::voice_activity::VAD_OPEN_AFTER_MS, - vad_hangover, - crate::voice_activity::VAD_MIN_TX_MS, - ); + if voice_activity_mode { + self.vad_state.configure( + crate::voice_activity::VAD_OPEN_AFTER_MS, + vad_hangover, + crate::voice_activity::VAD_MIN_TX_MS, + ); + } - // Switch VAD backend when config changes. + // Switch VAD backend only while VoiceActivity mode is active. let silero_epoch = crate::vad::silero_model_epoch(); - let ten_epoch = crate::vad::ten_model_epoch(); - let silero_changed = vad_backend == crate::VadBackend::SileroOnnx + let silero_changed = voice_activity_mode + && vad_backend == crate::VadBackend::SileroOnnx && silero_epoch != self.silero_model_epoch; - let ten_changed = - vad_backend == crate::VadBackend::TenVad && ten_epoch != self.ten_model_epoch; - if vad_backend != self.current_vad_backend || silero_changed || ten_changed { + if voice_activity_mode && (vad_backend != self.current_vad_backend || silero_changed) { self.current_vad_backend = vad_backend; self.silero_model_epoch = silero_epoch; - self.ten_model_epoch = ten_epoch; self.fallback_warned_backend = None; match vad_backend { crate::VadBackend::SileroOnnx => { let path = crate::vad::silero_model_bundle_path(); self.silero_vad_worker = crate::vad::silero_onnx::SileroOnnxVadWorker::try_new(&path); - self.ten_vad_worker = None; if self.silero_vad_worker.is_none() { tracing::warn!( target: "chanora_audio", @@ -313,46 +319,43 @@ mod inner { ); } } - crate::VadBackend::TenVad => { - let path = crate::vad::ten_model_bundle_path(); - self.ten_vad_worker = crate::vad::TenOnnxVadWorker::try_new(&path); - self.silero_vad_worker = None; - if self.ten_vad_worker.is_none() { - tracing::warn!( - target: "chanora_audio", - "TEN VAD ONNX model not found at {path}; falling back to WebRTC VAD" - ); - } - } _ => { self.silero_vad_worker = None; - self.ten_vad_worker = None; } } self.vad_state.reset(); } - self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); - let capture_seq = self.capture_frame_seq; - let mut used_fallback_vad = false; - let vad = if vad_backend == crate::VadBackend::Disabled { - crate::vad::VadOutput { - probability: 1.0, - speech: true, - } - } else if vad_backend == crate::VadBackend::SileroOnnx { - if let Some(worker) = self.silero_vad_worker.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let p = worker.latest_probability(); - crate::vad::VadOutput { - probability: p, - speech: p >= 0.5, - } - } else if enqueued { - crate::vad::VadOutput { - probability: 0.0, - speech: false, + let (vad_probability, active) = if voice_activity_mode { + self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); + let capture_seq = self.capture_frame_seq; + let mut used_fallback_vad = false; + let vad = if vad_backend == crate::VadBackend::Disabled { + crate::vad::VadOutput { + probability: 1.0, + speech: true, + } + } else if vad_backend == crate::VadBackend::SileroOnnx { + if let Some(worker) = self.silero_vad_worker.as_ref() { + let enqueued = worker.try_send(capture_seq, &frame); + if !worker.is_stale(capture_seq) { + let p = worker.latest_probability(); + crate::vad::VadOutput { + probability: p, + speech: p >= 0.5, + } + } else if enqueued { + crate::vad::VadOutput { + probability: 0.0, + speech: false, + } + } else { + used_fallback_vad = true; + self.mark_vad_fallback_active(vad_backend); + crate::vad::VoiceActivityDetector::process_10ms( + &mut self.vad_detector, + &frame, + ) } } else { used_fallback_vad = true; @@ -363,56 +366,26 @@ mod inner { ) } } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } - } else if vad_backend == crate::VadBackend::TenVad { - if let Some(worker) = self.ten_vad_worker.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let p = worker.latest_probability(); - crate::vad::VadOutput { - probability: p, - speech: p >= 0.5, - } - } else if enqueued { - crate::vad::VadOutput { - probability: 0.0, - speech: false, - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); - crate::vad::VoiceActivityDetector::process_10ms( - &mut self.vad_detector, - &frame, - ) - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(vad_backend); - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } + }; + self.audio_processing_stats + .set_vad_fallback_active(used_fallback_vad); + (vad.probability, self.vad_state.update(vad.speech)) } else { - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) + (0.0, false) }; - self.audio_processing_stats - .set_vad_fallback_active(used_fallback_vad); - let active = self.vad_state.update(vad.speech); - let output_muted = self.output_muted.load(Ordering::Relaxed); if let Some(sel) = &self.voice_activity_selector { - sel.set_voice_activity_open(active && !output_muted); + sel.set_voice_activity_open(voice_activity_mode && active); } self.audio_processing_stats.update_capture( input_dbfs, crate::frame::dbfs(&frame), - vad.probability, - active && !output_muted, + vad_probability, + voice_activity_mode && active, self.transmit_active.load(Ordering::Relaxed), ); - if !self.transmit_active.load(Ordering::Relaxed) || output_muted { + if !self.transmit_active.load(Ordering::Relaxed) { return; } diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index d1dc79d..b36769b 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -146,13 +146,10 @@ struct IosCaptureState { /// Background Silero worker — enqueues frames off the realtime /// callback and publishes the latest probability atomically. silero_vad_worker: Option, - ten_vad: Option, /// Last VAD backend we configured — used to detect backend changes. current_vad_backend: crate::VadBackend, /// Last observed configured Silero model epoch. silero_model_epoch: u64, - /// Last observed configured TEN model epoch. - ten_model_epoch: u64, fallback_warned_backend: Option, vad_state: crate::voice_activity::VoiceActivityStateMachine, audio_processing_config: Arc>, @@ -191,10 +188,8 @@ impl IosCaptureState { voice_activity_selector: params.voice_activity_selector.clone(), vad_detector: crate::vad::WebRtcFallbackVad::default(), silero_vad_worker: None, - ten_vad: None, current_vad_backend: crate::VadBackend::WebrtcVad, silero_model_epoch: crate::vad::silero_model_epoch(), - ten_model_epoch: crate::vad::ten_model_epoch(), fallback_warned_backend: None, vad_state: crate::voice_activity::VoiceActivityStateMachine::default(), audio_processing_config: params.audio_processing_config.clone(), @@ -361,13 +356,23 @@ impl IosCaptureState { crate::AudioBackend::PlatformVoiceProcessing, )); - // Switch VAD backend when the config changes. + let voice_activity_mode = self + .voice_activity_selector + .as_ref() + .map(|selector| selector.mode() == crate::TransmitMode::VoiceActivity) + .unwrap_or(false); + if !voice_activity_mode { + self.silero_vad_worker = None; + self.current_vad_backend = crate::VadBackend::Disabled; + self.fallback_warned_backend = None; + self.audio_processing_stats.set_vad_fallback_active(false); + } + + // Switch VAD backend only while VoiceActivity mode is active. let silero_model_epoch = crate::vad::silero_model_epoch(); - let ten_model_epoch = crate::vad::ten_model_epoch(); - let silero_model_changed = vad_backend == crate::VadBackend::SileroOnnx + let silero_model_changed = voice_activity_mode + && vad_backend == crate::VadBackend::SileroOnnx && silero_model_epoch != self.silero_model_epoch; - let ten_model_changed = - vad_backend == crate::VadBackend::TenVad && ten_model_epoch != self.ten_model_epoch; if let Ok(mut recorder_guard) = self.wav_recorder.try_lock() { if debug_wav_dump_enabled { @@ -382,11 +387,11 @@ impl IosCaptureState { } } - if vad_backend != self.current_vad_backend || silero_model_changed || ten_model_changed { + if voice_activity_mode && (vad_backend != self.current_vad_backend || silero_model_changed) + { self.current_vad_backend = vad_backend; self.fallback_warned_backend = None; self.silero_model_epoch = silero_model_epoch; - self.ten_model_epoch = ten_model_epoch; match vad_backend { crate::VadBackend::SileroOnnx => { // Attempt to load Silero model from the well-known @@ -407,23 +412,8 @@ impl IosCaptureState { self.audio_processing_stats .set_vad_fallback_active(self.silero_vad_worker.is_none()); } - crate::VadBackend::TenVad => { - self.silero_vad_worker = None; - let model_path = crate::vad::ten_model_bundle_path(); - self.ten_vad = crate::vad::TenOnnxVadWorker::try_new(&model_path); - if self.ten_vad.is_none() { - warn!( - target: "chanora_audio", - "TEN VAD ONNX model not available at {model_path}; falling back to WebRTC VAD" - ); - self.mark_vad_fallback_active(crate::VadBackend::TenVad); - } - self.audio_processing_stats - .set_vad_fallback_active(self.ten_vad.is_none()); - } _ => { self.silero_vad_worker = None; - self.ten_vad = None; self.audio_processing_stats.set_vad_fallback_active(false); } } @@ -436,13 +426,16 @@ impl IosCaptureState { self.vad_state.reset(); } - // Keep the VAD state machine aligned with the active config. - self.vad_state.configure( - crate::voice_activity::VAD_OPEN_AFTER_MS, - vad_hangover, - crate::voice_activity::VAD_MIN_TX_MS, - ); let transmit_active = self.transmit_active.load(Ordering::Relaxed); + if voice_activity_mode { + // Keep the VAD state machine aligned with the active config only + // while VoiceActivity mode owns the transmit gate. + self.vad_state.configure( + crate::voice_activity::VAD_OPEN_AFTER_MS, + vad_hangover, + crate::voice_activity::VAD_MIN_TX_MS, + ); + } // VPIO owns AEC. Keep the legacy non-AEC conditioning path here until // the raw WebRTC APM path is explicitly selected. @@ -461,29 +454,37 @@ impl IosCaptureState { self.sonora_processor.process_capture(&mut frame); } - // VAD: use Silero if loaded, otherwise WebRTC fallback. - // Disabled backend → always open (Continuous-like for VAD mode). - self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); - let capture_seq = self.capture_frame_seq; - let mut used_fallback_vad = false; - let vad = if vad_backend == crate::VadBackend::Disabled { - crate::vad::VadOutput { - probability: 1.0, - speech: true, - } - } else if vad_backend == crate::VadBackend::SileroOnnx { - if let Some(worker) = self.silero_vad_worker.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let probability = worker.latest_probability(); - crate::vad::VadOutput { - probability, - speech: probability >= 0.5, - } - } else if enqueued { - crate::vad::VadOutput { - probability: 0.0, - speech: false, + // VAD: only evaluate while VoiceActivity mode is active. + let (vad_probability, gate_open) = if voice_activity_mode { + self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1); + let capture_seq = self.capture_frame_seq; + let mut used_fallback_vad = false; + let vad = if vad_backend == crate::VadBackend::Disabled { + crate::vad::VadOutput { + probability: 1.0, + speech: true, + } + } else if vad_backend == crate::VadBackend::SileroOnnx { + if let Some(worker) = self.silero_vad_worker.as_ref() { + let enqueued = worker.try_send(capture_seq, &frame); + if !worker.is_stale(capture_seq) { + let probability = worker.latest_probability(); + crate::vad::VadOutput { + probability, + speech: probability >= 0.5, + } + } else if enqueued { + crate::vad::VadOutput { + probability: 0.0, + speech: false, + } + } else { + used_fallback_vad = true; + self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx); + crate::vad::VoiceActivityDetector::process_10ms( + &mut self.vad_detector, + &frame, + ) } } else { used_fallback_vad = true; @@ -491,49 +492,23 @@ impl IosCaptureState { crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) } } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx); crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } - } else if vad_backend == crate::VadBackend::TenVad { - if let Some(worker) = self.ten_vad.as_ref() { - let enqueued = worker.try_send(capture_seq, &frame); - if !worker.is_stale(capture_seq) { - let probability = worker.latest_probability(); - crate::vad::VadOutput { - probability, - speech: probability >= 0.5, - } - } else if enqueued { - crate::vad::VadOutput { - probability: 0.0, - speech: false, - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(crate::VadBackend::TenVad); - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } - } else { - used_fallback_vad = true; - self.mark_vad_fallback_active(crate::VadBackend::TenVad); - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) - } + }; + self.audio_processing_stats + .set_vad_fallback_active(used_fallback_vad); + (vad.probability, self.vad_state.update(vad.speech)) } else { - crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame) + (0.0, false) }; - self.audio_processing_stats - .set_vad_fallback_active(used_fallback_vad); - let gate_open = self.vad_state.update(vad.speech); let output_muted = self.output_muted.load(Ordering::Relaxed); if let Some(selector) = &self.voice_activity_selector { - selector.set_voice_activity_open(gate_open && !output_muted); + selector.set_voice_activity_open(voice_activity_mode && gate_open && !output_muted); } self.audio_processing_stats.update_capture( input_dbfs, crate::frame::dbfs(&frame), - vad.probability, - gate_open && !output_muted, + vad_probability, + voice_activity_mode && gate_open && !output_muted, transmit_active, ); diff --git a/crates/chanora_audio/src/mobile_voice_backend.rs b/crates/chanora_audio/src/mobile_voice_backend.rs index 303c80f..8a352cd 100644 --- a/crates/chanora_audio/src/mobile_voice_backend.rs +++ b/crates/chanora_audio/src/mobile_voice_backend.rs @@ -203,8 +203,8 @@ impl Default for AndroidVoiceStreamConfig { Self { sample_rate: 48_000, channel_count: 1, - request_low_latency: true, - request_exclusive: true, + request_low_latency: false, + request_exclusive: false, effects: AudioEffects::default(), } } @@ -488,7 +488,7 @@ impl fmt::Display for AchievedInputPreset { #[derive(Debug, Clone)] pub struct AndroidAudioDiagnostics { // Requested side (SDD-112 items 4..7) — fixed by SDD-112. - /// Requested performance mode (always "LowLatency" in P0). + /// Requested performance mode for the current Android request profile. pub requested_performance_mode: &'static str, /// Requested output usage (always "VoiceCommunication" in P0). pub requested_usage: &'static str, @@ -496,7 +496,7 @@ pub struct AndroidAudioDiagnostics { pub requested_content_type: &'static str, /// Requested input preset (always "VoiceCommunication" first). pub requested_input_preset: &'static str, - /// Requested sharing mode (always "Exclusive" first). + /// Requested sharing mode for the current Android request profile. pub requested_sharing_mode: &'static str, /// Requested sample rate (Hz). pub requested_sample_rate_hz: u32, @@ -640,15 +640,15 @@ pub fn current_android_audio_diagnostics() -> Option { mod tests { use super::*; - /// SWE4-UV-047: default config records requested low-latency, - /// exclusive sharing, mono 48 kHz, and default effects. + /// SWE4-UV-047: default config records the conservative shared + /// voice-input request profile, mono 48 kHz, and default effects. #[test] fn swe4_uv_047_default_config_records_requested_values() { let cfg = AndroidVoiceStreamConfig::default(); assert_eq!(cfg.sample_rate, 48_000); assert_eq!(cfg.channel_count, 1); - assert!(cfg.request_low_latency); - assert!(cfg.request_exclusive); + assert!(!cfg.request_low_latency); + assert!(!cfg.request_exclusive); // AudioEffects defaults are all-on per DEC-007..010. assert!(cfg.effects.aec); assert!(cfg.effects.noise_suppression); @@ -702,7 +702,9 @@ mod tests { ); } - /// SWE4-UV-049: sharing-mode ladder is Exclusive -> Shared -> exhausted. + /// SWE4-UV-049: sharing-mode ladder remains Exclusive -> Shared -> exhausted. + /// The default request profile may start at Shared, but the ladder still + /// exists for explicitly opt-in low-latency / exclusive experiments. #[test] fn swe4_uv_049_sharing_mode_fallback_ladder_order() { assert_eq!( diff --git a/crates/chanora_audio/src/vad/mod.rs b/crates/chanora_audio/src/vad/mod.rs index f68df9c..db878d9 100644 --- a/crates/chanora_audio/src/vad/mod.rs +++ b/crates/chanora_audio/src/vad/mod.rs @@ -7,7 +7,6 @@ pub mod resampler; pub mod silero_onnx; -pub mod ten_onnx; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{OnceLock, RwLock}; @@ -17,7 +16,6 @@ use crate::AudioError; use resampler::{Downsampler48to16, INPUT_FRAME_10MS}; pub use silero_onnx::SileroOnnxVad; -pub use ten_onnx::{TenOnnxVad, TenOnnxVadWorker}; /// Voice activity detector output for one 10 ms frame. #[derive(Debug, Clone, Copy)] @@ -108,17 +106,11 @@ pub fn process_i16_10ms(detector: &mut dyn VoiceActivityDetector, samples: &[i16 static SILERO_MODEL_PATH_OVERRIDE: OnceLock>> = OnceLock::new(); static SILERO_MODEL_EPOCH: AtomicU64 = AtomicU64::new(0); -static TEN_MODEL_PATH_OVERRIDE: OnceLock>> = OnceLock::new(); -static TEN_MODEL_EPOCH: AtomicU64 = AtomicU64::new(0); fn silero_model_path_override() -> &'static RwLock> { SILERO_MODEL_PATH_OVERRIDE.get_or_init(|| RwLock::new(None)) } -fn ten_model_path_override() -> &'static RwLock> { - TEN_MODEL_PATH_OVERRIDE.get_or_init(|| RwLock::new(None)) -} - /// Configure the preferred Silero ONNX model path. /// /// The path is validated eagerly. A successful call increments the @@ -149,32 +141,6 @@ pub fn silero_model_epoch() -> u64 { SILERO_MODEL_EPOCH.load(Ordering::Relaxed) } -/// Configure the preferred TEN VAD ONNX model path. -pub fn set_ten_model_path(path: &str) -> Result<(), AudioError> { - let path = path.trim(); - if path.is_empty() { - return Err(AudioError::InvalidAudioProcessingConfig( - "ten vad model path must not be empty".to_string(), - )); - } - if !std::path::Path::new(path).is_file() { - return Err(AudioError::InvalidAudioProcessingConfig(format!( - "ten vad model path does not exist or is not a file: {path}" - ))); - } - let mut guard = ten_model_path_override() - .write() - .map_err(|_| AudioError::Backend("ten vad model path lock poisoned".to_string()))?; - *guard = Some(path.to_string()); - TEN_MODEL_EPOCH.fetch_add(1, Ordering::Relaxed); - Ok(()) -} - -/// Monotonic counter incremented whenever the configured TEN model path changes. -pub fn ten_model_epoch() -> u64 { - TEN_MODEL_EPOCH.load(Ordering::Relaxed) -} - /// Return the expected path of the Silero VAD v6 ONNX model. /// The model is shipped as a Flutter asset and copied to the app's /// data directory by the Dart-side asset loader. @@ -231,46 +197,6 @@ pub fn silero_model_bundle_path() -> String { } } -/// Return the expected path of the TEN VAD ONNX model copied by Flutter. -pub fn ten_model_bundle_path() -> String { - if let Ok(guard) = ten_model_path_override().read() { - if let Some(path) = guard.as_ref() { - return path.clone(); - } - } - - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - if let Ok(home) = std::env::var("HOME") { - let docs = format!("{home}/Documents/ten_vad.onnx"); - if std::path::Path::new(&docs).exists() { - return docs; - } - let bundle = format!("{home}/../Library/ten_vad.onnx"); - if std::path::Path::new(&bundle).exists() { - return bundle; - } - } - "ten_vad.onnx".to_string() - } - - #[cfg(target_os = "android")] - { - "ten_vad.onnx".to_string() - } - - #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] - { - if let Ok(cwd) = std::env::current_dir() { - let local = cwd.join("ten_vad.onnx"); - if local.exists() { - return local.to_string_lossy().to_string(); - } - } - "ten_vad.onnx".to_string() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/chanora_audio/src/vad/ten_onnx.rs b/crates/chanora_audio/src/vad/ten_onnx.rs deleted file mode 100644 index b63a778..0000000 --- a/crates/chanora_audio/src/vad/ten_onnx.rs +++ /dev/null @@ -1,452 +0,0 @@ -//! TEN VAD ONNX backend. -//! -//! TEN's ONNX graph does not accept raw PCM. It expects the same feature -//! stack produced by TEN's `AUP_Aed_aivad_proc`: three context frames of -//! 40 log-mel powers plus one pitch feature, followed by four recurrent -//! state tensors. This module ports that preprocessing path to Rust and -//! keeps ONNX Runtime off the realtime callback where possible. - -use crate::frame::f32_to_i16; - -use rustfft::{num_complex::Complex32, FftPlanner}; - -use super::resampler::{Downsampler48to16, INPUT_FRAME_10MS}; -use super::{VadOutput, VoiceActivityDetector}; - -const SAMPLE_RATE_16K: f32 = 16_000.0; -const HOP_16K: usize = 256; -const WINDOW_16K: usize = 768; -const FFT_SIZE: usize = 1024; -const N_BINS: usize = FFT_SIZE / 2 + 1; -const MEL_BANDS: usize = 40; -const FEATURE_LEN: usize = 41; -const CONTEXT: usize = 3; -const HIDDEN: usize = 64; -const POWER_NORMALIZER: f32 = 32768.0 * 32768.0; -const EPS: f32 = 1.0e-20; - -const FEATURE_MEANS: [f32; FEATURE_LEN] = [ - -8.198236, -6.2657166, -5.4838185, -4.7586913, -4.417089, -4.142893, -3.9128504, -3.845928, - -3.6570904, -3.7234187, -3.8761342, -3.843891, -3.6904051, -3.7560658, -3.6986961, -3.650463, - -3.7004688, -3.5673213, -3.4989002, -3.477807, -3.458816, -3.4449239, -3.4013286, -3.3062613, - -3.2785568, -3.2332509, -3.198616, -3.2045264, -3.2087986, -3.257838, -3.3813767, -3.5340214, - -3.640868, -3.7268589, -3.773731, -3.8046672, -3.832901, -3.8711205, -3.990593, -4.4802895, - 92.3569, -]; - -const FEATURE_STDS: [f32; FEATURE_LEN] = [ - 5.166064, 4.9772096, 4.698896, 4.6306214, 4.634348, 4.641156, 4.6406765, 4.666367, 4.6505346, - 4.640021, 4.6374, 4.620099, 4.5963163, 4.562655, 4.5543604, 4.5669107, 4.56249, 4.5624127, - 4.5852995, 4.6001797, 4.592846, 4.5859227, 4.5834966, 4.626093, 4.626958, 4.6262894, 4.637006, - 4.683016, 4.726814, 4.7342896, 4.753227, 4.849723, 4.869435, 4.884483, 4.921327, 4.9592123, - 4.996619, 5.0448236, 5.072217, 5.0964394, 115.21369, -]; - -/// TEN VAD using ONNX Runtime and Rust-ported TEN feature preprocessing. -pub struct TenOnnxVad { - session: ort::session::Session, - downsampler: Downsampler48to16, - hop_accum: Vec, - sample_fifo: Vec, - feature_stack: [[f32; FEATURE_LEN]; CONTEXT], - states: [[f32; HIDDEN]; 4], - mel_filters: Vec<[f32; N_BINS]>, - fft: std::sync::Arc>, - fft_buffer: Vec, - last_probability: f32, - last_speech: bool, -} - -unsafe impl Send for TenOnnxVad {} - -impl TenOnnxVad { - /// Load TEN VAD ONNX model. - pub fn try_new(model_path: &str) -> Option { - if !std::path::Path::new(model_path).exists() { - tracing::warn!(target: "chanora_audio", path = model_path, "TEN VAD ONNX model not found"); - return None; - } - let session = match std::panic::catch_unwind(|| { - ort::session::Session::builder().and_then(|mut b| b.commit_from_file(model_path)) - }) { - Ok(Ok(session)) => session, - Ok(Err(error)) => { - tracing::warn!(target: "chanora_audio", %error, path = model_path, "TEN VAD ONNX model load failed"); - return None; - } - Err(_) => { - tracing::warn!(target: "chanora_audio", path = model_path, "TEN VAD ONNX Runtime panicked during load"); - return None; - } - }; - let mut fft_planner = FftPlanner::::new(); - let fft = fft_planner.plan_fft_forward(FFT_SIZE); - tracing::info!(target: "chanora_audio", path = model_path, "TEN VAD ONNX model loaded"); - Some(Self { - session, - downsampler: Downsampler48to16::default(), - hop_accum: Vec::with_capacity(HOP_16K + super::resampler::OUTPUT_FRAME_10MS), - sample_fifo: Vec::with_capacity(WINDOW_16K + HOP_16K), - feature_stack: [[0.0; FEATURE_LEN]; CONTEXT], - states: [[0.0; HIDDEN]; 4], - mel_filters: build_mel_filters(), - fft, - fft_buffer: vec![Complex32::ZERO; FFT_SIZE], - last_probability: 0.0, - last_speech: false, - }) - } - - fn process_hop(&mut self, hop: &[f32]) { - self.sample_fifo.extend_from_slice(hop); - let frame = if self.sample_fifo.len() >= WINDOW_16K { - let start = self.sample_fifo.len() - WINDOW_16K; - self.sample_fifo[start..].to_vec() - } else { - let mut padded = vec![0.0; WINDOW_16K - self.sample_fifo.len()]; - padded.extend_from_slice(&self.sample_fifo); - padded - }; - if self.sample_fifo.len() > WINDOW_16K { - let excess = self.sample_fifo.len() - WINDOW_16K; - self.sample_fifo.drain(..excess); - } - - let feature = compute_feature( - &self.mel_filters, - self.fft.as_ref(), - &mut self.fft_buffer, - &frame, - ); - self.feature_stack.copy_within(1..CONTEXT, 0); - self.feature_stack[CONTEXT - 1] = feature; - self.run_onnx(); - } - - fn run_onnx(&mut self) { - use ndarray::{Array, IxDyn}; - use ort::value::Value; - - let input: Vec = self.feature_stack.iter().flatten().copied().collect(); - let input_arr = match Array::from_shape_vec(IxDyn(&[1, CONTEXT, FEATURE_LEN]), input) { - Ok(v) => v, - Err(_) => return, - }; - let state_arrs = [0, 1, 2, 3] - .map(|idx| Array::from_shape_vec(IxDyn(&[1, HIDDEN]), self.states[idx].to_vec())); - let input_val = match Value::from_array(input_arr) { - Ok(v) => v, - Err(error) => { - tracing::warn!(target: "chanora_audio", %error, "TEN VAD input tensor error"); - return; - } - }; - let state_vals = match state_arrs { - [Ok(a), Ok(b), Ok(c), Ok(d)] => [a, b, c, d], - _ => return, - }; - let state_vals = match state_vals.map(Value::from_array) { - [Ok(a), Ok(b), Ok(c), Ok(d)] => [a, b, c, d], - _ => return, - }; - - let outputs = match self.session.run([ - (&input_val).into(), - (&state_vals[0]).into(), - (&state_vals[1]).into(), - (&state_vals[2]).into(), - (&state_vals[3]).into(), - ]) { - Ok(outputs) => outputs, - Err(error) => { - tracing::warn!(target: "chanora_audio", %error, "TEN VAD ONNX inference failed"); - return; - } - }; - - if let Ok((_, prob)) = outputs["output_1"].try_extract_tensor::() { - if let Some(&p) = prob.first() { - self.last_probability = p.clamp(0.0, 1.0); - self.last_speech = self.last_probability >= 0.5; - } - } - for (idx, name) in ["output_2", "output_3", "output_6", "output_7"] - .iter() - .enumerate() - { - if let Ok((_, state)) = outputs[*name].try_extract_tensor::() { - let copy_len = state.len().min(HIDDEN); - self.states[idx][..copy_len].copy_from_slice(&state[..copy_len]); - } - } - } -} - -fn compute_feature( - mel_filters: &[[f32; N_BINS]], - fft: &dyn rustfft::Fft, - fft_buffer: &mut [Complex32], - frame: &[f32], -) -> [f32; FEATURE_LEN] { - let power = power_spectrum(fft, fft_buffer, frame); - let mut feature = [0.0; FEATURE_LEN]; - for band in 0..MEL_BANDS { - let energy = mel_filters[band] - .iter() - .zip(power.iter()) - .map(|(w, p)| w * p) - .sum::() - / POWER_NORMALIZER; - let log_energy = (energy + EPS).ln(); - feature[band] = (log_energy - FEATURE_MEANS[band]) / (FEATURE_STDS[band] + EPS); - } - let pitch_hz = estimate_pitch_hz(frame); - feature[MEL_BANDS] = (pitch_hz - FEATURE_MEANS[MEL_BANDS]) / (FEATURE_STDS[MEL_BANDS] + EPS); - feature -} - -impl VoiceActivityDetector for TenOnnxVad { - fn process_10ms(&mut self, samples: &[f32]) -> VadOutput { - debug_assert_eq!(samples.len(), INPUT_FRAME_10MS); - let mut input = [0.0_f32; INPUT_FRAME_10MS]; - input.copy_from_slice(samples); - let downsampled = self.downsampler.process_frame_10ms(&input); - self.hop_accum.extend_from_slice(&downsampled); - while self.hop_accum.len() >= HOP_16K { - let hop: Vec = self.hop_accum[..HOP_16K].to_vec(); - self.hop_accum.drain(..HOP_16K); - self.process_hop(&hop); - } - VadOutput { - probability: self.last_probability, - speech: self.last_speech, - } - } -} - -fn hz_to_mel(hz: f32) -> f32 { - 2595.0 * (1.0 + hz / 700.0).log10() -} - -fn mel_to_hz(mel: f32) -> f32 { - 700.0 * (10.0_f32.powf(mel / 2595.0) - 1.0) -} - -fn build_mel_filters() -> Vec<[f32; N_BINS]> { - let low_mel = hz_to_mel(0.0); - let high_mel = hz_to_mel(8000.0); - let mut bins = [0_usize; MEL_BANDS + 2]; - for idx in 0..bins.len() { - let mel = idx as f32 * (high_mel - low_mel) / (MEL_BANDS as f32 + 1.0) + low_mel; - let hz = mel_to_hz(mel); - let mut bin = ((FFT_SIZE as f32 + 1.0) * hz / SAMPLE_RATE_16K).floor() as usize; - bin = bin.min(N_BINS - 1); - if idx > 0 && bin == bins[idx - 1] { - bin = (bin + 1).min(N_BINS - 1); - } - bins[idx] = bin; - } - - let mut filters = vec![[0.0_f32; N_BINS]; MEL_BANDS]; - for band in 0..MEL_BANDS { - let left = bins[band]; - let center = bins[band + 1].max(left + 1); - let right = bins[band + 2].max(center + 1).min(N_BINS - 1); - for (i, weight) in filters[band] - .iter_mut() - .enumerate() - .take(center.min(N_BINS)) - .skip(left) - { - *weight = (i - left) as f32 / (center - left) as f32; - } - for (i, weight) in filters[band] - .iter_mut() - .enumerate() - .take(right + 1) - .skip(center) - { - *weight = (right - i) as f32 / (right - center).max(1) as f32; - } - } - filters -} - -fn power_spectrum( - fft: &dyn rustfft::Fft, - fft_buffer: &mut [Complex32], - frame: &[f32], -) -> [f32; N_BINS] { - debug_assert_eq!(fft_buffer.len(), FFT_SIZE); - fft_buffer.fill(Complex32::ZERO); - for (idx, sample) in frame.iter().take(WINDOW_16K).enumerate() { - let hann = 0.5 - 0.5 * (2.0 * std::f32::consts::PI * idx as f32 / WINDOW_16K as f32).cos(); - fft_buffer[idx].re = f32_to_i16(*sample) as f32 * hann; - } - - fft.process(fft_buffer); - - let mut out = [0.0_f32; N_BINS]; - for (dst, bin) in out.iter_mut().zip(fft_buffer.iter()) { - *dst = bin.norm_sqr(); - } - out -} - -fn estimate_pitch_hz(frame: &[f32]) -> f32 { - let min_lag = (SAMPLE_RATE_16K / 400.0) as usize; - let max_lag = (SAMPLE_RATE_16K / 60.0) as usize; - let mut best_lag = 0_usize; - let mut best_corr = 0.0_f32; - for lag in min_lag..=max_lag.min(frame.len().saturating_sub(1)) { - let mut corr = 0.0_f32; - let mut energy = 0.0_f32; - for i in lag..frame.len() { - corr += frame[i] * frame[i - lag]; - energy += frame[i - lag] * frame[i - lag]; - } - let norm = if energy > 1.0e-8 { - corr / energy.sqrt() - } else { - 0.0 - }; - if norm > best_corr { - best_corr = norm; - best_lag = lag; - } - } - if best_lag == 0 || best_corr < 0.01 { - 0.0 - } else { - SAMPLE_RATE_16K / best_lag as f32 - } -} - -// --------------------------------------------------------------------------- -// Background worker — same pattern as SileroOnnxVadWorker so the realtime -// callback never blocks on STFT / pitch / ONNX inference. -// --------------------------------------------------------------------------- - -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64}; -use std::sync::Arc; -use std::thread::JoinHandle; - -/// Maximum number of 10 ms frames the worker may lag before the callback -/// treats its output as stale and uses WebRTC fallback instead. -const TEN_MAX_STALE_FRAMES: u64 = 8; - -struct TenFrameMessage { - seq: u64, - frame: [f32; INPUT_FRAME_10MS], -} - -/// Background TEN VAD worker. The realtime callback only enqueues 10 ms -/// frames and reads the latest probability atomically. -pub struct TenOnnxVadWorker { - tx: Option>, - latest_probability: Arc, - latest_processed_seq: Arc, - alive: Arc, - handle: Option>, -} - -impl TenOnnxVadWorker { - /// Start a background TEN worker if the model loads. - pub fn try_new(model_path: &str) -> Option { - let vad = TenOnnxVad::try_new(model_path)?; - let latest_probability = Arc::new(AtomicU32::new(0.0_f32.to_bits())); - let latest_processed_seq = Arc::new(AtomicU64::new(u64::MAX)); - let alive = Arc::new(AtomicBool::new(true)); - let (tx, rx) = std::sync::mpsc::sync_channel::(128); - let prob_arc = latest_probability.clone(); - let seq_arc = latest_processed_seq.clone(); - let alive_arc = alive.clone(); - - let handle = std::thread::Builder::new() - .name("chanora-ten-vad".to_string()) - .spawn(move || { - let mut vad = vad; - while alive_arc.load(std::sync::atomic::Ordering::Relaxed) { - let msg = match rx.recv() { - Ok(m) => m, - Err(_) => break, - }; - let mut frame_f32 = [0.0_f32; INPUT_FRAME_10MS]; - frame_f32.copy_from_slice(&msg.frame); - let out = VoiceActivityDetector::process_10ms(&mut vad, &frame_f32); - prob_arc.store( - out.probability.clamp(0.0, 1.0).to_bits(), - std::sync::atomic::Ordering::Relaxed, - ); - seq_arc.store(msg.seq, std::sync::atomic::Ordering::Relaxed); - } - }) - .ok()?; - - Some(Self { - tx: Some(tx), - latest_probability, - latest_processed_seq, - alive, - handle: Some(handle), - }) - } - - /// Best-effort enqueue of a 10 ms frame for background inference. - pub fn try_send(&self, seq: u64, frame: &[f32; INPUT_FRAME_10MS]) -> bool { - let Some(tx) = &self.tx else { - return false; - }; - tx.try_send(TenFrameMessage { seq, frame: *frame }).is_ok() - } - - /// Latest probability published by the background worker. - pub fn latest_probability(&self) -> f32 { - f32::from_bits( - self.latest_probability - .load(std::sync::atomic::Ordering::Relaxed), - ) - } - - /// True when the worker is too far behind to trust its output. - pub fn is_stale(&self, capture_seq: u64) -> bool { - let latest = self - .latest_processed_seq - .load(std::sync::atomic::Ordering::Relaxed); - latest == u64::MAX || capture_seq.saturating_sub(latest) > TEN_MAX_STALE_FRAMES - } -} - -impl Drop for TenOnnxVadWorker { - fn drop(&mut self) { - self.alive - .store(false, std::sync::atomic::Ordering::Relaxed); - drop(self.tx.take()); - if let Some(h) = self.handle.take() { - let _ = h.join(); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mel_filter_bank_has_expected_shape() { - let filters = build_mel_filters(); - assert_eq!(filters.len(), MEL_BANDS); - assert!(filters.iter().all(|f| f.iter().any(|&v| v > 0.0))); - } - - #[test] - fn preprocessing_produces_finite_features() { - let filters = build_mel_filters(); - let mut planner = FftPlanner::::new(); - let fft = planner.plan_fft_forward(FFT_SIZE); - let mut fft_buffer = vec![Complex32::ZERO; FFT_SIZE]; - let frame = vec![0.0_f32; WINDOW_16K]; - let feature = compute_feature(&filters, fft.as_ref(), &mut fft_buffer, &frame); - assert!(feature.iter().all(|v| v.is_finite())); - } -} diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 5f25c6a..611c6ab 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -145,7 +145,7 @@ pub(crate) fn publish_permission_state(permission: String, state: PermissionStat /// flood the diagnostic export during transient packet loss; /// users can still raise verbosity via `RUST_LOG=info`. const DEFAULT_LOG_FILTER: &str = - "info,tsproto::resend=error,tsproto::packet_codec=error,tsclientlib=error"; + "info,tsproto::resend=error,tsproto::packet_codec=error,tsclientlib=error,ts_bookkeeping::messages::s2c=error"; /// Initialise the bridge. Must be called once on Dart side before /// any other API call. Sets up panic logging. @@ -921,8 +921,6 @@ pub enum BridgeAudioBackend { pub enum BridgeVadBackend { /// Silero ONNX VAD. SileroOnnx, - /// TEN VAD. - TenVad, /// WebRTC fallback VAD. WebrtcVad, /// Debug energy VAD. @@ -1014,6 +1012,16 @@ pub struct BridgeAudioProcessingStats { pub callback_xruns: u64, /// Clipped samples. pub clipped_samples: u64, + /// Number of effectively silent processed capture frames. + pub zero_frames: u64, + /// Number of processed capture frames. + pub capture_frames: u64, + /// Input callbacks carrying 10 ms of audio. + pub callbacks_10ms: u64, + /// Input callbacks carrying 20 ms of audio. + pub callbacks_20ms: u64, + /// Input callbacks carrying other sizes. + pub callbacks_other: u64, /// Sonora enabled. pub sonora_enabled: bool, /// Platform voice processing enabled. @@ -1092,7 +1100,6 @@ impl From for chanora_core::VadBackend { fn from(backend: BridgeVadBackend) -> Self { match backend { BridgeVadBackend::SileroOnnx => Self::SileroOnnx, - BridgeVadBackend::TenVad => Self::TenVad, BridgeVadBackend::WebrtcVad => Self::WebrtcVad, BridgeVadBackend::EnergyDebug => Self::EnergyDebug, BridgeVadBackend::Disabled => Self::Disabled, @@ -1104,7 +1111,6 @@ impl From for BridgeVadBackend { fn from(backend: chanora_core::VadBackend) -> Self { match backend { chanora_core::VadBackend::SileroOnnx => Self::SileroOnnx, - chanora_core::VadBackend::TenVad => Self::TenVad, chanora_core::VadBackend::WebrtcVad => Self::WebrtcVad, chanora_core::VadBackend::EnergyDebug => Self::EnergyDebug, chanora_core::VadBackend::Disabled => Self::Disabled, @@ -1196,6 +1202,11 @@ impl From for BridgeAudioProcessingStats { output_underruns: stats.output_underruns, callback_xruns: stats.callback_xruns, clipped_samples: stats.clipped_samples, + zero_frames: stats.zero_frames, + capture_frames: stats.capture_frames, + callbacks_10ms: stats.callbacks_10ms, + callbacks_20ms: stats.callbacks_20ms, + callbacks_other: stats.callbacks_other, sonora_enabled: stats.sonora_enabled, platform_voice_processing_enabled: stats.platform_voice_processing_enabled, } @@ -1236,8 +1247,31 @@ pub fn export_diagnostics() -> String { let android_audio_yaml = chanora_audio::mobile_voice_backend::current_android_audio_diagnostics() .map(|d| d.to_yaml_fragment()); + let audio_health = runtime().block_on(async { session().audio_processing_stats().await.ok() }); let network_info = runtime().block_on(async { session().network_diagnostics_summary().await }); let protocol_events = runtime().block_on(async { session().drain_protocol_events().await }); + let android_audio_yaml = android_audio_yaml.map(|mut yaml| { + if let Some(stats) = audio_health { + yaml.push_str(&format!( + " health:\n\ + \x20\x20\x20\x20capture_frames: {}\n\ + \x20\x20\x20\x20zero_frames: {}\n\ + \x20\x20\x20\x20callbacks_10ms: {}\n\ + \x20\x20\x20\x20callbacks_20ms: {}\n\ + \x20\x20\x20\x20callbacks_other: {}\n\ + \x20\x20\x20\x20callback_xruns: {}\n\ + \x20\x20\x20\x20clipped_samples: {}\n", + stats.capture_frames, + stats.zero_frames, + stats.callbacks_10ms, + stats.callbacks_20ms, + stats.callbacks_other, + stats.callback_xruns, + stats.clipped_samples, + )); + } + yaml + }); match chanora_core::DiagnosticExport::from_sink(log_sink(), metadata) { Ok(exp) => exp .with_android_audio(android_audio_yaml) @@ -1506,6 +1540,11 @@ pub enum BridgeEvent { /// Target scope (server/channel/private/poke). target: BridgeMessageTarget, }, + /// Human-readable server activity surfaced from protocol bookkeeping events. + ServerActivity { + /// TeamSpeak-style activity line. + message: String, + }, /// Audio route changed (speaker/earpiece/BT/wired). AudioRouteChanged { /// The new audio route. @@ -1692,6 +1731,9 @@ impl From for BridgeEvent { message, target: target.into(), }, + chanora_core::SessionEvent::ServerActivity { message } => { + BridgeEvent::ServerActivity { message } + } chanora_core::SessionEvent::AudioRouteChanged { route } => { BridgeEvent::AudioRouteChanged { route: route.into(), @@ -1848,10 +1890,16 @@ pub async fn audio_processing_stats() -> Result BridgeAudioDeviceList { - let list = chanora_audio::list_audio_devices(); - BridgeAudioDeviceList { +pub async fn list_audio_devices() -> Result { + let (list, selected_input, selected_output) = runtime() + .spawn(async move { + let selected_input = session().preferred_input_device().await; + let selected_output = session().preferred_output_device().await; + let list = chanora_audio::list_audio_devices(); + (list, selected_input, selected_output) + }) + .await + .map_err(|e| task_join_error("list_audio_devices", e))?; + + Ok(BridgeAudioDeviceList { input_devices: list .input_devices .into_iter() .map(|d| BridgeAudioDevice { + is_selected: selected_input.as_ref().is_some_and(|id| id == &d.id), + id: d.id, name: d.name, + details: d.details, is_default: d.is_default, }) .collect(), @@ -1879,27 +1939,30 @@ pub fn list_audio_devices() -> BridgeAudioDeviceList { .output_devices .into_iter() .map(|d| BridgeAudioDevice { + is_selected: selected_output.as_ref().is_some_and(|id| id == &d.id), + id: d.id, name: d.name, + details: d.details, is_default: d.is_default, }) .collect(), - } + }) } -/// Set the preferred input device by name. Takes effect on next +/// Set the preferred input device by id. Takes effect on next /// `start_audio`. -pub async fn set_input_device(name: Option) -> Result<(), BridgeError> { +pub async fn set_input_device(id: Option) -> Result<(), BridgeError> { runtime() - .spawn(async move { session().set_input_device(name).await }) + .spawn(async move { session().set_input_device(id).await }) .await .map_err(|e| task_join_error("set_input_device", e))??; Ok(()) } -/// Set the preferred output device by name. -pub async fn set_output_device(name: Option) -> Result<(), BridgeError> { +/// Set the preferred output device by id. +pub async fn set_output_device(id: Option) -> Result<(), BridgeError> { runtime() - .spawn(async move { session().set_output_device(name).await }) + .spawn(async move { session().set_output_device(id).await }) .await .map_err(|e| task_join_error("set_output_device", e))??; Ok(()) @@ -1919,21 +1982,6 @@ pub async fn set_vad_model_path(path: String) -> Result<(), BridgeError> { Ok(()) } -/// Configure the TEN VAD ONNX model path. -pub async fn set_ten_vad_model_path(path: String) -> Result<(), BridgeError> { - if path.trim().is_empty() { - return Err(BridgeError::InvalidCommand( - "ten vad model path must not be empty".to_string(), - )); - } - runtime() - .spawn(async move { chanora_audio::vad::set_ten_model_path(&path) }) - .await - .map_err(|e| task_join_error("set_ten_vad_model_path", e))? - .map_err(|e| BridgeError::Unmapped(format!("set_ten_vad_model_path: {e}")))?; - Ok(()) -} - /// Enable or disable audio debug WAV dumping. pub async fn enable_audio_debug_wav_dump(enabled: bool) -> Result<(), BridgeError> { runtime() diff --git a/crates/chanora_bridge/src/frb_generated.rs b/crates/chanora_bridge/src/frb_generated.rs index 66e5e75..62c3299 100644 --- a/crates/chanora_bridge/src/frb_generated.rs +++ b/crates/chanora_bridge/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1433826599; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -560177922; // Section: executor @@ -743,7 +743,7 @@ fn wire__crate__api__list_audio_devices_impl( rust_vec_len_: i32, data_len_: i32, ) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { debug_name: "list_audio_devices", port: Some(port_), @@ -760,11 +760,14 @@ fn wire__crate__api__list_audio_devices_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::list_audio_devices())?; - Ok(output_ok) - })()) + move |context| async move { + transform_result_sse::<_, crate::BridgeError>( + (move || async move { + let output_ok = crate::api::list_audio_devices().await?; + Ok(output_ok) + })() + .await, + ) } }, ) @@ -1141,12 +1144,12 @@ fn wire__crate__api__set_input_device_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_name = >::sse_decode(&mut deserializer); + let api_id = >::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, crate::BridgeError>( (move || async move { - let output_ok = crate::api::set_input_device(api_name).await?; + let output_ok = crate::api::set_input_device(api_id).await?; Ok(output_ok) })() .await, @@ -1282,12 +1285,12 @@ fn wire__crate__api__set_output_device_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_name = >::sse_decode(&mut deserializer); + let api_id = >::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, crate::BridgeError>( (move || async move { - let output_ok = crate::api::set_output_device(api_name).await?; + let output_ok = crate::api::set_output_device(api_id).await?; Ok(output_ok) })() .await, @@ -1478,42 +1481,6 @@ fn wire__crate__api__set_release_tail_ms_impl( }, ) } -fn wire__crate__api__set_ten_vad_model_path_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "set_ten_vad_model_path", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_path = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, crate::BridgeError>( - (move || async move { - let output_ok = crate::api::set_ten_vad_model_path(api_path).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} fn wire__crate__api__set_transmit_mode_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1783,11 +1750,17 @@ impl SseDecode for crate::api::BridgeAudioBackend { impl SseDecode for crate::api::BridgeAudioDevice { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); let mut var_name = ::sse_decode(deserializer); + let mut var_details = ::sse_decode(deserializer); let mut var_isDefault = ::sse_decode(deserializer); + let mut var_isSelected = ::sse_decode(deserializer); return crate::api::BridgeAudioDevice { + id: var_id, name: var_name, + details: var_details, is_default: var_isDefault, + is_selected: var_isSelected, }; } } @@ -1859,6 +1832,11 @@ impl SseDecode for crate::api::BridgeAudioProcessingStats { let mut var_outputUnderruns = ::sse_decode(deserializer); let mut var_callbackXruns = ::sse_decode(deserializer); let mut var_clippedSamples = ::sse_decode(deserializer); + let mut var_zeroFrames = ::sse_decode(deserializer); + let mut var_captureFrames = ::sse_decode(deserializer); + let mut var_callbacks10Ms = ::sse_decode(deserializer); + let mut var_callbacks20Ms = ::sse_decode(deserializer); + let mut var_callbacksOther = ::sse_decode(deserializer); let mut var_sonoraEnabled = ::sse_decode(deserializer); let mut var_platformVoiceProcessingEnabled = ::sse_decode(deserializer); return crate::api::BridgeAudioProcessingStats { @@ -1879,6 +1857,11 @@ impl SseDecode for crate::api::BridgeAudioProcessingStats { output_underruns: var_outputUnderruns, callback_xruns: var_callbackXruns, clipped_samples: var_clippedSamples, + zero_frames: var_zeroFrames, + capture_frames: var_captureFrames, + callbacks_10ms: var_callbacks10Ms, + callbacks_20ms: var_callbacks20Ms, + callbacks_other: var_callbacksOther, sonora_enabled: var_sonoraEnabled, platform_voice_processing_enabled: var_platformVoiceProcessingEnabled, }; @@ -2147,6 +2130,12 @@ impl SseDecode for crate::api::BridgeEvent { }; } 12 => { + let mut var_message = ::sse_decode(deserializer); + return crate::api::BridgeEvent::ServerActivity { + message: var_message, + }; + } + 13 => { let mut var_route = ::sse_decode(deserializer); return crate::api::BridgeEvent::AudioRouteChanged { route: var_route }; } @@ -2291,10 +2280,9 @@ impl SseDecode for crate::api::BridgeVadBackend { let mut inner = ::sse_decode(deserializer); return match inner { 0 => crate::api::BridgeVadBackend::SileroOnnx, - 1 => crate::api::BridgeVadBackend::TenVad, - 2 => crate::api::BridgeVadBackend::WebrtcVad, - 3 => crate::api::BridgeVadBackend::EnergyDebug, - 4 => crate::api::BridgeVadBackend::Disabled, + 1 => crate::api::BridgeVadBackend::WebrtcVad, + 2 => crate::api::BridgeVadBackend::EnergyDebug, + 3 => crate::api::BridgeVadBackend::Disabled, _ => unreachable!("Invalid variant for BridgeVadBackend: {}", inner), }; } @@ -2544,13 +2532,12 @@ fn pde_ffi_dispatcher_primary_impl( 39 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), 40 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len), 41 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__set_ten_vad_model_path_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len), - 45 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len), + 43 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len), + 44 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -2609,8 +2596,11 @@ impl flutter_rust_bridge::IntoIntoDart impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioDevice { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ + self.id.into_into_dart().into_dart(), self.name.into_into_dart().into_dart(), + self.details.into_into_dart().into_dart(), self.is_default.into_into_dart().into_dart(), + self.is_selected.into_into_dart().into_dart(), ] .into_dart() } @@ -2697,6 +2687,11 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioProcessingStats { self.output_underruns.into_into_dart().into_dart(), self.callback_xruns.into_into_dart().into_dart(), self.clipped_samples.into_into_dart().into_dart(), + self.zero_frames.into_into_dart().into_dart(), + self.capture_frames.into_into_dart().into_dart(), + self.callbacks_10ms.into_into_dart().into_dart(), + self.callbacks_20ms.into_into_dart().into_dart(), + self.callbacks_other.into_into_dart().into_dart(), self.sonora_enabled.into_into_dart().into_dart(), self.platform_voice_processing_enabled .into_into_dart() @@ -2973,8 +2968,11 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeEvent { target.into_into_dart().into_dart(), ] .into_dart(), + crate::api::BridgeEvent::ServerActivity { message } => { + [12.into_dart(), message.into_into_dart().into_dart()].into_dart() + } crate::api::BridgeEvent::AudioRouteChanged { route } => { - [12.into_dart(), route.into_into_dart().into_dart()].into_dart() + [13.into_dart(), route.into_into_dart().into_dart()].into_dart() } _ => { unimplemented!(""); @@ -3170,10 +3168,9 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeVadBackend { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { Self::SileroOnnx => 0.into_dart(), - Self::TenVad => 1.into_dart(), - Self::WebrtcVad => 2.into_dart(), - Self::EnergyDebug => 3.into_dart(), - Self::Disabled => 4.into_dart(), + Self::WebrtcVad => 1.into_dart(), + Self::EnergyDebug => 2.into_dart(), + Self::Disabled => 3.into_dart(), _ => unreachable!(), } } @@ -3313,8 +3310,11 @@ impl SseEncode for crate::api::BridgeAudioBackend { impl SseEncode for crate::api::BridgeAudioDevice { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); ::sse_encode(self.name, serializer); + ::sse_encode(self.details, serializer); ::sse_encode(self.is_default, serializer); + ::sse_encode(self.is_selected, serializer); } } @@ -3368,6 +3368,11 @@ impl SseEncode for crate::api::BridgeAudioProcessingStats { ::sse_encode(self.output_underruns, serializer); ::sse_encode(self.callback_xruns, serializer); ::sse_encode(self.clipped_samples, serializer); + ::sse_encode(self.zero_frames, serializer); + ::sse_encode(self.capture_frames, serializer); + ::sse_encode(self.callbacks_10ms, serializer); + ::sse_encode(self.callbacks_20ms, serializer); + ::sse_encode(self.callbacks_other, serializer); ::sse_encode(self.sonora_enabled, serializer); ::sse_encode(self.platform_voice_processing_enabled, serializer); } @@ -3595,8 +3600,12 @@ impl SseEncode for crate::api::BridgeEvent { ::sse_encode(message, serializer); ::sse_encode(target, serializer); } - crate::api::BridgeEvent::AudioRouteChanged { route } => { + crate::api::BridgeEvent::ServerActivity { message } => { ::sse_encode(12, serializer); + ::sse_encode(message, serializer); + } + crate::api::BridgeEvent::AudioRouteChanged { route } => { + ::sse_encode(13, serializer); ::sse_encode(route, serializer); } _ => { @@ -3734,10 +3743,9 @@ impl SseEncode for crate::api::BridgeVadBackend { ::sse_encode( match self { crate::api::BridgeVadBackend::SileroOnnx => 0, - crate::api::BridgeVadBackend::TenVad => 1, - crate::api::BridgeVadBackend::WebrtcVad => 2, - crate::api::BridgeVadBackend::EnergyDebug => 3, - crate::api::BridgeVadBackend::Disabled => 4, + crate::api::BridgeVadBackend::WebrtcVad => 1, + crate::api::BridgeVadBackend::EnergyDebug => 2, + crate::api::BridgeVadBackend::Disabled => 3, _ => { unimplemented!(""); } diff --git a/crates/chanora_protocol/src/adapter.rs b/crates/chanora_protocol/src/adapter.rs index 928a0c9..04f51ab 100644 --- a/crates/chanora_protocol/src/adapter.rs +++ b/crates/chanora_protocol/src/adapter.rs @@ -659,17 +659,11 @@ async fn connection_task( .. } = &ev { - let own_client = con - .get_state() - .ok() - .map(|state| state.own_client); + let own_client = con.get_state().ok().map(|state| state.own_client); if own_client == Some(*client_id) { - let current_channel = con - .get_state() - .ok() - .and_then(|state| { - state.clients.get(client_id).map(|client| client.channel.0) - }); + let current_channel = con.get_state().ok().and_then(|state| { + state.clients.get(client_id).map(|client| client.channel.0) + }); if let Some(current_channel) = current_channel { let matched: Vec = pending_moves .iter() @@ -682,7 +676,9 @@ async fn connection_task( }) .collect(); for handle in matched { - if let Some((_, reply, _)) = pending_moves.remove(&handle) { + if let Some((_, reply, _)) = + pending_moves.remove(&handle) + { info!( target: "chanora_protocol", channel_id = current_channel, @@ -727,7 +723,9 @@ async fn connection_task( } } StreamItem::MessageResult(handle, result) => { - if let Some((_target_channel, reply, _deadline)) = pending_moves.remove(&handle) { + if let Some((_target_channel, reply, _deadline)) = + pending_moves.remove(&handle) + { let mapped = match result { Ok(()) => Ok(()), Err(cmd_err) => { @@ -814,36 +812,32 @@ async fn connection_task( channel_id, password, reply, - }) => { - match move_self_to(&mut con, channel_id, password.as_deref()) { - Ok(handle) => { - let deadline = std::time::Instant::now() + Duration::from_secs(3); - pending_moves.insert(handle, (channel_id, Some(reply), deadline)); - } - Err(e) => { - let _ = reply.send(Err(e)); - } + }) => match move_self_to(&mut con, channel_id, password.as_deref()) { + Ok(handle) => { + let deadline = std::time::Instant::now() + Duration::from_secs(3); + pending_moves.insert(handle, (channel_id, Some(reply), deadline)); } - } + Err(e) => { + let _ = reply.send(Err(e)); + } + }, Ok(Request::MoveToChannelNoWait { channel_id, password, - }) => { - match move_self_to(&mut con, channel_id, password.as_deref()) { - Ok(handle) => { - let deadline = std::time::Instant::now() + Duration::from_secs(3); - pending_moves.insert(handle, (channel_id, None, deadline)); - } - Err(e) => { - warn!( - target: "chanora_protocol", - error = %e, - channel_id, - "fire-and-forget client_move could not be queued" - ); - } + }) => match move_self_to(&mut con, channel_id, password.as_deref()) { + Ok(handle) => { + let deadline = std::time::Instant::now() + Duration::from_secs(3); + pending_moves.insert(handle, (channel_id, None, deadline)); } - } + Err(e) => { + warn!( + target: "chanora_protocol", + error = %e, + channel_id, + "fire-and-forget client_move could not be queued" + ); + } + }, Ok(Request::SetMuted { input, output, @@ -1261,10 +1255,7 @@ fn activity_channel_group_name( .map(|group| quoted(&group.name)) } -fn activity_server_group_name( - con: &Connection, - id: tsclientlib::ServerGroupId, -) -> Option { +fn activity_server_group_name(con: &Connection, id: tsclientlib::ServerGroupId) -> Option { con.get_state() .ok() .and_then(|state| state.server_groups.get(&id)) @@ -1282,7 +1273,11 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) -> use tsproto_types::Reason; match ev { - Event::PropertyAdded { id: PropertyId::Client(client_id), extra, .. } => { + Event::PropertyAdded { + id: PropertyId::Client(client_id), + extra, + .. + } => { if extra.reason.is_none() { return None; } @@ -1294,7 +1289,12 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) -> channel )) } - Event::PropertyRemoved { id: PropertyId::Client(_), old, extra, .. } => { + Event::PropertyRemoved { + id: PropertyId::Client(_), + old, + extra, + .. + } => { let PropertyValue::Client(client) = old else { return None; }; @@ -1305,12 +1305,9 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) -> Some(Reason::Clientdisconnect) => { Some(format!("{} disconnected (Leaving)", quoted(&client.name))) } - Some(Reason::ClientdisconnectServerShutdown) | Some(Reason::Serverstop) => { - Some(format!( - "{} disconnected (server shutdown)", - quoted(&client.name) - )) - } + Some(Reason::ClientdisconnectServerShutdown) | Some(Reason::Serverstop) => Some( + format!("{} disconnected (server shutdown)", quoted(&client.name)), + ), _ => Some(format!( "{} dropped (connection lost)", quoted(&client.name) @@ -1329,8 +1326,7 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) -> let client = activity_client(con, *client_id)?; let from = activity_channel_name(con, *from_channel_id)?; let to = activity_channel_name(con, client.channel)?; - if invoker.as_ref().map(|invoker| invoker.id) == Some(*client_id) || invoker.is_none() - { + if invoker.as_ref().map(|invoker| invoker.id) == Some(*client_id) || invoker.is_none() { Some(format!( "{} switched from channel {} to {}", quoted(&client.name),

8^a5YUWDMPC6O?C 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