From f71aff3e11687d5cb4d20636cb623bd51568a7bb Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 21:58:02 +0100 Subject: [PATCH] Document Android radio automatic capture and background window contracts --- .../com/utun/chat/data/RadioAutoSettings.kt | 17 ++++++++++++++--- .../java/com/utun/chat/data/RadioVadWindow.kt | 14 +++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAutoSettings.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAutoSettings.kt index 386cba15..7f6d1e99 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAutoSettings.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAutoSettings.kt @@ -1,6 +1,14 @@ package com.utun.chat.data -/** Один снимок режима и связанных порогов; значения режимов совпадают с radio_audio.h. */ +/** Сохранённые параметры автопередачи; enabled отделён от выбранного детектора (VAD/LEVEL). + * OFF используется только для захвата без автоматики и не является сохраняемым mode. + * valid проверяет весь снимок, включая параметры невыбранного детектора; nativeValues сам проверку не выполняет. + * Диапазоны совпадают с реестром chat_setting.c, номера режимов — с radio_audio.h. + * @param levelOn порог включения по уровню, -90..0 dBFS. + * @param levelOff порог выключения по уровню, -90..0 dBFS, строго ниже levelOn. + * @param hangoverMs тишина до автоматического отбоя, 50..2000 мс. + * @param vadThreshold порог вероятности речи Silero, 10..95 процентов. + * @param vadGain предел усиления отдельного компрессора копии PCM для VAD, 1..60 dB; не усиление передаваемого PCM. */ data class RadioAutoSettings( val enabled: Boolean = false, val mode: Int = VAD, @@ -14,11 +22,14 @@ data class RadioAutoSettings( fun valid(): Boolean = mode in VAD..LEVEL && levelOn in -90..0 && levelOff in -90..0 && levelOff < levelOn && hangoverMs in 50..2000 && vadThreshold in 10..95 && vadGain in 1..60 - // Порядок фиксирован контрактом nativeRadioConfigure; вся группа применяется одним заданием uasync. + /** Новая копия [enabled, mode, levelOn, levelOff, hangoverMs, vadThreshold, vadGain] для nativeRadioConfigure. + * Native проверяет и применяет снимок целиком в uasync, результат приходит отдельно по request ID. */ fun nativeValues(): IntArray = intArrayOf(if (enabled) 1 else 0, mode, levelOn, levelOff, hangoverMs, vadThreshold, vadGain) } -/** Решение о захвате одинаково для VAD/уровня; измеритель не включает автопередачу. */ +/** Для уже проверенных settings: null — захват не нужен, OFF — только PTT/измеритель, VAD/LEVEL — с автоматикой. + * Неактивная рация исключает любой захват. Автоматика требует включённый экран и разрешение фонового окна; + * ручной PTT обходит эти два ограничения, измеритель работает только при включённом экране и не включает автоматику. */ internal fun radioCaptureMode(active: Boolean, screenOn: Boolean, autoAllowed: Boolean, settings: RadioAutoSettings, manual: Boolean, monitor: Boolean): Int? { if (!active) return null diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioVadWindow.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioVadWindow.kt index 8483d666..d2c1d32c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioVadWindow.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioVadWindow.kt @@ -1,7 +1,12 @@ package com.utun.chat.data -/** Окно VAD расходуется только в фоне; все изменения выполняются в radio-audio-io. */ +/** Фоновое окно обоих детекторов автопередачи (VAD и уровня), длительность задаёт durationMs. + * В foreground открытое окно не ограничено временем; возвращение в приложение восстанавливает полный бюджет, + * следующий уход в фон даёт новое окно целиком. Повторная установка того же foreground/enabled его не продлевает. + * Вызывающий передаёт nowMs в монотонной шкале SystemClock.elapsedRealtime, отдельно учитывает экран и готовность аудио. + * Изменения и timeLeftMs — только в radio-audio-io; capture-поток может читать volatile deadlineMs и isAllowed. */ class RadioVadWindow(private val durationMs: Long) { + /** 0 — закрыто, Long.MAX_VALUE — открыто без ограничения foreground, иначе исключающая граница в elapsedRealtime. */ @Volatile var deadlineMs = 0L private set var foreground = false @@ -9,6 +14,7 @@ class RadioVadWindow(private val durationMs: Long) { private var enabled = false private var remainingMs = durationMs + /** Смена видимости приложения: вход в foreground восстанавливает бюджет, выход запускает его отсчёт. */ fun setForeground(value: Boolean, nowMs: Long) { if (foreground == value) return if (value) remainingMs = durationMs @@ -16,24 +22,30 @@ class RadioVadWindow(private val durationMs: Long) { updateDeadline(nowMs) } + /** Только переход false→true открывает новое окно; true→false закрывает его. */ fun setEnabled(value: Boolean, nowMs: Long) { if (enabled == value) return enabled = value if (value) restart(nowMs) else close() } + /** Восстановить полный бюджет (например, экран включился); выключенную настройку enabled не включает. */ fun restart(nowMs: Long) { remainingMs = durationMs updateDeadline(nowMs) } + /** Немедленно закрыть окно (например, экран погас), сохранив enabled и foreground. + * Повторный setEnabled(true) не откроет его; нужны restart либо новый переход в foreground. */ fun close() { remainingMs = 0L deadlineMs = 0L } + /** На самой границе deadline окно уже истекло; разрешение экрана/PTT проверяется отдельно. */ fun isAllowed(nowMs: Long): Boolean = nowMs < deadlineMs + /** Остаток в фоне или полный бюджет открытого foreground; для закрытого/истёкшего окна — 0. Только io-поток. */ fun timeLeftMs(nowMs: Long): Long = if (deadlineMs == Long.MAX_VALUE) remainingMs else (deadlineMs - nowMs).coerceAtLeast(0L)