From 745ec3dbd2f27430bcb24a529febe4adf58130c9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 29 Sep 2026 22:35:08 +0300 Subject: [PATCH] android radio: preserve output route on reconfigure, link input/output settings --- .../com/utun/chat/data/RadioAudioEngine.kt | 17 ++++++++++- .../com/utun/chat/viewmodel/ChatViewModel.kt | 29 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt index 7ad470ce..abe854f7 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt @@ -126,6 +126,7 @@ class RadioAudioEngine { active = false releaseLocked(false) active = true + router.setAutoHeadset(true) /* дефолт «авто» только на свежий старт/смену группы */ setupAudio() } @@ -157,7 +158,6 @@ class RadioAudioEngine { /* Маршрут выхода: автоследование за гарнитурой (воткнули → HEADSET, вынули → SPEAKER). */ router.fallbackRoute = AudioRoute.SPEAKER - router.setAutoHeadset(true) router.start() // ── AudioRecord (захват) с откатом стерео→моно ── @@ -382,6 +382,7 @@ class RadioAudioEngine { val ch = captureChannels val frame = FRAME_SAMPLES * ch val buf = ShortArray(frame) + var frameN = 0L while (active) { val rec = audioRecord ?: break if (rec.recordingState != AudioRecord.RECORDSTATE_RECORDING) { @@ -404,6 +405,12 @@ class RadioAudioEngine { } read += n } + /* диагностика крос-девайс входа/выхода: куда реально роутится захват + уровень сигнала */ + if (frameN < 10 || frameN % 100L == 0L) { + LogManager.addLog("DEBUG", "RadioAudio", + "cap routed=${rec.routedDevice?.type ?: "-"} frame=$frameN rmsDb=${rmsDb(buf)} input=${inputSource()} route=${route()}") + } + frameN++ if ((talking || vadMode) && !NativeLib.radioAudioFeed(groupId, buf)) { LogManager.addLog("WARN", "RadioAudio", "radioAudioFeed returned false") } @@ -411,6 +418,14 @@ class RadioAudioEngine { LogManager.addLog("DEBUG", "RadioAudio", "capture loop done") } + /** RMS захваченного кадра в dBFS (0 = полная шкала; тишина ~ -90). */ + private fun rmsDb(buf: ShortArray): Int { + var sum = 0L + for (s in buf) sum += s.toLong() * s.toLong() + val rms = Math.sqrt(sum.toDouble() / buf.size) + return if (rms < 1.0) -120 else (20.0 * Math.log10(rms / 32768.0)).toInt() + } + private fun playLoop() { val frame = FRAME_SAMPLES * PLAY_CHANNELS val buf = ShortArray(frame) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index f4faf7d2..3b408ece 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -860,22 +860,47 @@ class ChatViewModel : ViewModel() { radioAudio.talkEnd() } - /** Ручной выбор выхода рации (динамик/гарнитура). */ + /** Ручной выбор выхода рации (динамик/гарнитура) — синхронизирует вход. */ fun setRadioRoute(r: AudioRoute) { if (!_radioActive.value) return _radioAuto.value = false radioAudio.setRoute(r) _radioRoute.value = radioAudio.route() + val linkedInput = when (r) { + AudioRoute.SPEAKER -> RadioAudioEngine.INPUT_BUILTIN + AudioRoute.HEADSET -> when { + _wiredInputAvailable.value -> RadioAudioEngine.INPUT_WIRED + _btInputAvailable.value -> RadioAudioEngine.INPUT_BT + else -> null + } + else -> null + } + if (linkedInput != null && linkedInput != _radioInputSource.value) { + _radioInputSource.value = linkedInput + viewModelScope.launch { + ChatApplication.instance.configProvider.setRadioInputSource(linkedInput) + radioAudio.reconfigure() + } + } LogManager.addLog("INFO", "VM", "setRadioRoute -> ${radioAudio.route()}") } - /** Ручной выбор входного устройства рации (выключает авто). */ + /** Ручной выбор входного устройства рации — синхронизирует выход. */ fun setRadioInputSource(src: Int) { if (!_radioActive.value) return val clamped = src.coerceIn(RadioAudioEngine.INPUT_AUTO, RadioAudioEngine.INPUT_BT) if (clamped == _radioInputSource.value) return _radioAuto.value = false _radioInputSource.value = clamped + val linkedRoute = when (clamped) { + RadioAudioEngine.INPUT_BUILTIN -> AudioRoute.SPEAKER + RadioAudioEngine.INPUT_WIRED, RadioAudioEngine.INPUT_BT -> AudioRoute.HEADSET + else -> null + } + if (linkedRoute != null) { + radioAudio.setRoute(linkedRoute) + _radioRoute.value = radioAudio.route() + } viewModelScope.launch { ChatApplication.instance.configProvider.setRadioInputSource(clamped) radioAudio.reconfigure()