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)