radio: add activation controls, live VAD meters and signal diagnostics
This commit is contained in:
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()
|
||||
|
||||
+35
-14
@@ -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>
|
||||
|
||||
@@ -2784,10 +2784,11 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioConfigur
|
||||
JNIEXPORT jfloatArray JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioInputStatus(JNIEnv* env, jobject thiz) {
|
||||
(void)thiz;
|
||||
struct radio_audio_input_status status = radio_audio_get_input_status();
|
||||
jfloat values[] = {status.level_dbfs, (float)status.valid, (float)status.blocked};
|
||||
jfloatArray result = (*env)->NewFloatArray(env, 3);
|
||||
jfloat values[] = {status.level_dbfs, (float)status.valid, (float)status.blocked, status.vad_probability, (float)status.vad_valid,
|
||||
status.vad_gain_db, (float)status.vad_compressor_enabled};
|
||||
jfloatArray result = (*env)->NewFloatArray(env, 7);
|
||||
if (!result) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio input status allocation failed"); return NULL; }
|
||||
(*env)->SetFloatArrayRegion(env, result, 0, 3, values);
|
||||
(*env)->SetFloatArrayRegion(env, result, 0, 7, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user