radio: add activation controls, live VAD meters and signal diagnostics

This commit is contained in:
evgeny committed 2026-10-09 16:11:11 +02:00
1 parent fddbba217d
commit 7f3247bfcc
23 files changed
+818 -182

No files matched your search

@@ -214,7 +214,8 @@ object NativeLib {
fun radioAudioStop() { nativeRadioAudioStop() }
fun radioCaptureStart(groupId: Long, channels: Int, mode: Int): Boolean = nativeRadioCaptureStart(groupId, channels, mode)
fun radioConfigure(requestId: Long, settings: RadioAutoSettings): Boolean = nativeRadioConfigure(requestId, settings.nativeValues())
fun radioInputStatus(): FloatArray = nativeRadioInputStatus() // уровень dBFS, valid, RX-блокировка
// dBFS, valid, RX-блокировка, вероятность речи 0..1, vadValid, усиление VAD в dB, компрессор VAD включён.
fun radioInputStatus(): FloatArray = nativeRadioInputStatus()
fun radioCaptureStop() { nativeRadioCaptureStop() }
fun radioTransmitting(): Boolean = nativeRadioTransmitting()
fun radioAutoMode(): Int = nativeRadioAutoMode()
@@ -584,7 +584,7 @@ private fun DeveloperSettingsTab(vm: ChatViewModel, onDirty: (Boolean) -> Unit,
dismissButton = { TextButton(onClick = { showClearDb = false }) { Text(stringResource(R.string.cancel)) } })
}
/* Настройка детектора использует тот же native RMS, что и фактическая передача. */
/* Измерители показывают фактические RMS и вероятность речи из native-детекторов. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean = false) {
@@ -596,6 +596,9 @@ internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean =
var saveError by remember { mutableStateOf(false) }
var resumed by remember { mutableStateOf(false) }
var level by remember { mutableStateOf<Float?>(null) }
var vadProbability by remember { mutableStateOf<Float?>(null) }
var vadGain by remember { mutableStateOf<Float?>(null) }
var vadCompressorEnabled by remember { mutableStateOf(false) }
var blocked by remember { mutableStateOf(false) }
var active by remember { mutableStateOf(engine.isActive()) }
var micPermission by remember { mutableStateOf(MicrophonePermission.granted()) }
@@ -607,12 +610,15 @@ internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean =
onPauseOrDispose { resumed = false; engine.setLevelMonitoring(false) }
}
LaunchedEffect(resumed) {
if (!resumed) { level = null; return@LaunchedEffect }
if (!resumed) { level = null; vadProbability = null; vadGain = null; return@LaunchedEffect }
while (true) {
val status = NativeLib.radioInputStatus()
active = engine.isActive()
micPermission = MicrophonePermission.granted()
level = if (active && status[1] != 0f) status[0] else null
vadProbability = if (active && status[4] != 0f) status[3] else null
vadGain = if (active && status[4] != 0f) status[5] else null
vadCompressorEnabled = status[6] != 0f
blocked = status[2] != 0f
delay(100)
}
@@ -661,19 +667,34 @@ internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean =
Text("−90 dBFS", style = MaterialTheme.typography.bodySmall)
Text("0", style = MaterialTheme.typography.bodySmall)
}
if (draft.mode == RadioAutoSettings.LEVEL) {
LabeledSlider(stringResource(R.string.radio_level_on), draft.levelOn.toFloat(), -90f..0f, 89, "${draft.levelOn} dBFS",
{ if (!saving) draft = draft.copy(levelOn = it.toInt()) })
LabeledSlider(stringResource(R.string.radio_level_off), draft.levelOff.toFloat(), -90f..0f, 89, "${draft.levelOff} dBFS",
{ if (!saving) draft = draft.copy(levelOff = it.toInt()) })
} else {
LabeledSlider(stringResource(R.string.speech_threshold), draft.vadThreshold.toFloat(), 10f..95f, 84, "${draft.vadThreshold}%",
{ if (!saving) draft = draft.copy(vadThreshold = it.toInt()) })
if (!compact) LabeledSlider(stringResource(R.string.vad_max_gain), draft.vadGain.toFloat(), 1f..60f, 58, "${draft.vadGain} dB",
{ if (!saving) draft = draft.copy(vadGain = it.toInt()) })
if (!compact) Text(stringResource(R.string.vad_compressor_hint), style = MaterialTheme.typography.bodySmall)
LabeledSlider(stringResource(R.string.radio_level_on), draft.levelOn.toFloat(), -90f..0f, 89, "${draft.levelOn} dBFS",
{ if (!saving) draft = draft.copy(levelOn = it.toInt()) })
LabeledSlider(stringResource(R.string.radio_level_off), draft.levelOff.toFloat(), -90f..0f, 89, "${draft.levelOff} dBFS",
{ if (!saving) draft = draft.copy(levelOff = it.toInt()) })
HorizontalDivider()
Text(stringResource(R.string.vad_button), style = MaterialTheme.typography.titleSmall)
Text(vadProbability?.let { stringResource(R.string.radio_vad_current, it * 100f) } ?: stringResource(when {
!saved.enabled || saved.mode != RadioAutoSettings.VAD -> R.string.radio_vad_enable
blocked -> R.string.radio_vad_busy
else -> R.string.radio_vad_unavailable
}), style = MaterialTheme.typography.bodySmall)
Canvas(Modifier.fillMaxWidth().height(20.dp)) {
drawRect(background)
vadProbability?.let { drawRect(Color(0xFF27AE60), size = Size(it.coerceIn(0f, 1f) * size.width, size.height)) }
val thresholdX = draft.vadThreshold / 100f * size.width
drawLine(Color(0xFFE67E22), Offset(thresholdX, 0f), Offset(thresholdX, size.height), 3.dp.toPx())
}
if (!compact) LabeledSlider(stringResource(R.string.silence_delay), draft.hangoverMs.toFloat(), 50f..2000f, 38,
LabeledSlider(stringResource(R.string.speech_threshold), draft.vadThreshold.toFloat(), 10f..95f, 84, "${draft.vadThreshold}%",
{ if (!saving) draft = draft.copy(vadThreshold = it.toInt()) })
Text(when {
vadGain == null -> stringResource(R.string.radio_vad_gain_unavailable)
!vadCompressorEnabled -> stringResource(R.string.radio_vad_compressor_off)
else -> stringResource(R.string.radio_vad_gain_current, vadGain!!)
}, style = MaterialTheme.typography.bodySmall)
LabeledSlider(stringResource(R.string.vad_max_gain), draft.vadGain.toFloat(), 1f..60f, 58, "${draft.vadGain} dB",
{ if (!saving) draft = draft.copy(vadGain = it.toInt()) })
if (!compact) Text(stringResource(R.string.vad_compressor_hint), style = MaterialTheme.typography.bodySmall)
LabeledSlider(stringResource(R.string.silence_delay), draft.hangoverMs.toFloat(), 50f..2000f, 38,
stringResource(R.string.milliseconds, draft.hangoverMs), { if (!saving) draft = draft.copy(hangoverMs = it.toInt()) })
if (draft.levelOff >= draft.levelOn) Text(stringResource(R.string.radio_level_invalid), color = MaterialTheme.colorScheme.error)
if (saveError) Text(stringResource(R.string.radio_auto_save_failed), color = MaterialTheme.colorScheme.error)
@@ -407,6 +407,13 @@
<string name="radio_level_allow_mic">"Разрешить доступ к микрофону"</string>
<string name="radio_level_unavailable">"Уровень микрофона недоступен"</string>
<string name="radio_level_current">"Текущий уровень: %1$.1f dBFS"</string>
<string name="radio_vad_current">"Вероятность речи VAD: %1$.1f%%"</string>
<string name="radio_vad_enable">"Включите автопередачу в режиме VAD для оценки вероятности речи"</string>
<string name="radio_vad_busy">"Оценка VAD приостановлена: эфир занят"</string>
<string name="radio_vad_unavailable">"Оценка вероятности речи VAD недоступна"</string>
<string name="radio_vad_gain_current">"Текущее усиление компрессора VAD: %1$+.1f dB"</string>
<string name="radio_vad_gain_unavailable">"Текущее усиление компрессора VAD: —"</string>
<string name="radio_vad_compressor_off">"Компрессор VAD выключен (0 dB)"</string>
<string name="radio_level_on">"Порог включения (оранжевый):"</string>
<string name="radio_level_off">"Порог отключения (синий):"</string>
<string name="radio_level_invalid">"Порог отключения должен быть ниже порога включения"</string>
@@ -406,6 +406,13 @@
<string name="radio_level_allow_mic">"Allow microphone access"</string>
<string name="radio_level_unavailable">"Microphone level unavailable"</string>
<string name="radio_level_current">"Current level: %1$.1f dBFS"</string>
<string name="radio_vad_current">"VAD speech probability: %1$.1f%%"</string>
<string name="radio_vad_enable">"Enable automatic transmission in VAD mode to see speech probability"</string>
<string name="radio_vad_busy">"VAD measurement paused: channel busy"</string>
<string name="radio_vad_unavailable">"VAD speech probability unavailable"</string>
<string name="radio_vad_gain_current">"Current VAD compressor gain: %1$+.1f dB"</string>
<string name="radio_vad_gain_unavailable">"Current VAD compressor gain: —"</string>
<string name="radio_vad_compressor_off">"VAD compressor off (0 dB)"</string>
<string name="radio_level_on">"Start threshold (orange):"</string>
<string name="radio_level_off">"Stop threshold (blue):"</string>
<string name="radio_level_invalid">"The stop threshold must be lower than the start threshold"</string>