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

+40 -4
View File
@@ -102,6 +102,8 @@ static int g_vad_pcm_frames = 0;
static double g_vad_input_sum_sq = 0;
static float g_vad_input_rms = 0, g_vad_output_rms = 0;
static float g_vad_threshold = 0.5f;
static float g_vad_probability;
static uint64_t g_vad_updated_tb;
static int g_auto_hangover_ms = 200;
static int g_manual_ptt = 0; /* PTT override: зажата — детектор не вмешивается */
static float g_vad_win[RADIO_VAD_WINDOW_16K]; /* накопитель окон 16кГц */
@@ -131,6 +133,8 @@ static uint64_t g_tx_stats_tb = 0;
static struct {
uint64_t stats_tb, last_window_tb, total_windows, max_inference_tb, max_gap_tb;
unsigned windows, speech, busy, confirm_resets, errors;
unsigned clipped_samples, clipped_frames;
float input_sample_peak, output_sample_peak;
float min_probability, max_probability, max_input_rms, max_model_rms;
int last_busy;
} g_vad_diag;
@@ -566,6 +570,7 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int auto_
accfg.release_rate_per_sec, accfg.block_duration_ms, g_vad_compressor ? accfg.block_duration_ms : 0);
}
radio_audio_level_reset();
g_vad_probability = 0; g_vad_updated_tb = 0;
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0;
g_vad_input_rms = g_vad_output_rms = 0;
if (g_auto_mode) {
@@ -616,6 +621,7 @@ void radio_audio_capture_stop(void) {
if (g_vad) { silero_vad_destroy(g_vad); g_vad = NULL; }
g_auto_mode = 0;
radio_audio_level_reset();
g_vad_probability = 0; g_vad_updated_tb = 0;
g_manual_ptt = 0;
g_vad_win_len = 0;
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0;
@@ -900,6 +906,7 @@ static int radio_audio_auto_block(uint64_t now) {
(unsigned long long)(g_output_voice_end > g_output_played ? g_output_voice_end - g_output_played : 0),
g_manual_ptt, g_transmitting, g_prefetch_frames);
g_auto_blocked = busy;
g_vad_updated_tb = 0;
g_level_frames = 0; g_level_sum_sq = 0;
g_vad_win_len = g_vad_decimation = 0; g_vad_sum = 0;
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0;
@@ -920,6 +927,7 @@ void radio_audio_capture_discontinuity(uint64_t group_id) {
RADIO_AUDIO_ID, g_tx_pcm_frames, g_transmitting, g_vad_pcm_frames, audio_compressor_gain_db(g_vad_compressor));
radio_audio_flush_tx();
radio_audio_level_reset();
g_vad_updated_tb = 0;
g_vad_win_len = g_vad_decimation = 0; g_vad_sum = 0;
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0;
g_vad_input_rms = g_vad_output_rms = 0;
@@ -942,10 +950,13 @@ static void radio_audio_vad_sample(const int16_t* frame) {
g_vad_win[g_vad_win_len++] = g_vad_sum / (3.0f * 32768.0f);
g_vad_decimation = 0; g_vad_sum = 0;
if (g_vad_win_len != RADIO_VAD_WINDOW_16K) return;
double sum_sq = 0;
double sum_sq = 0, sum = 0;
unsigned zeros = 0;
float peak = 0;
for (int i = 0; i < RADIO_VAD_WINDOW_16K; ++i) {
sum_sq += (double)g_vad_win[i] * g_vad_win[i];
sum += g_vad_win[i];
if (g_vad_win[i] == 0) ++zeros;
if (fabsf(g_vad_win[i]) > peak) peak = fabsf(g_vad_win[i]);
}
float model_rms = sqrtf((float)(sum_sq / RADIO_VAD_WINDOW_16K));
@@ -965,11 +976,13 @@ static void radio_audio_vad_sample(const int16_t* frame) {
if (inference_tb > g_vad_diag.max_inference_tb) g_vad_diag.max_inference_tb = inference_tb;
if (gap_tb > g_vad_diag.max_gap_tb) g_vad_diag.max_gap_tb = gap_tb;
if (result != 0) {
g_vad_updated_tb = 0;
++g_vad_diag.errors;
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "VAD inference failed group=%016llx window=%llu rc=%d inference=%.1fms",
(unsigned long long)g_group_id, (unsigned long long)g_vad_diag.total_windows, result, inference_tb / 10.0);
}
if (radio_audio_auto_block(now)) return; /* RX мог начаться, пока Silero работал без общего mutex. */
if (result == 0) { g_vad_probability = probability; g_vad_updated_tb = now; }
struct radio_auto_fsm before = g_auto_fsm;
int busy = 0;
int64_t rx_age_ms = g_last_rx_tb ? (int64_t)((now - g_last_rx_tb) / 10) : -1;
@@ -995,8 +1008,11 @@ static void radio_audio_vad_sample(const int16_t* frame) {
inference_tb / 10.0, gap_tb / 10.0);
}
DEBUG_TRACE(DEBUG_CATEGORY_VAD, "VAD window=%llu reason=%s probability=%.4f model_rms=%.6f peak=%.6f "
"input_rms=%.6f agc_rms=%.6f confirm=%d talking=%d busy=%d manual=%d tx=%d inference=%.1fms gap=%.1fms",
(unsigned long long)g_vad_diag.total_windows, reason, probability, model_rms, peak, g_vad_input_rms, g_vad_output_rms,
"dc=%.6f zeros=%u/%d input_rms=%.6f agc_rms=%.6f gain=%.1fdB "
"confirm=%d talking=%d busy=%d manual=%d tx=%d inference=%.1fms gap=%.1fms",
(unsigned long long)g_vad_diag.total_windows, reason, probability, model_rms, peak,
sum / RADIO_VAD_WINDOW_16K, zeros, RADIO_VAD_WINDOW_16K, g_vad_input_rms, g_vad_output_rms,
audio_compressor_gain_db(g_vad_compressor),
g_auto_fsm.confirm_count, g_auto_fsm.talking, busy, g_manual_ptt, g_transmitting, inference_tb / 10.0, gap_tb / 10.0);
g_vad_diag.last_busy = busy;
if (probability >= g_vad_threshold) ++g_vad_diag.speech;
@@ -1008,6 +1024,14 @@ static void radio_audio_vad_sample(const int16_t* frame) {
if (model_rms > g_vad_diag.max_model_rms) g_vad_diag.max_model_rms = model_rms;
if (!g_vad_diag.stats_tb) g_vad_diag.stats_tb = now;
if (now - g_vad_diag.stats_tb >= RADIO_AUDIO_STATS_TB) {
if (g_vad_diag.clipped_samples) {
DEBUG_WARN(DEBUG_CATEGORY_VAD, "VAD compressor clipping group=%016llx span=%.1fms samples=%u frames=%u "
"input_peak=%.4f output_peak=%.4f gain=%.1fdB probability=%.3f..%.3f",
(unsigned long long)g_group_id, (now - g_vad_diag.stats_tb) / 10.0,
g_vad_diag.clipped_samples, g_vad_diag.clipped_frames, g_vad_diag.input_sample_peak,
g_vad_diag.output_sample_peak, audio_compressor_gain_db(g_vad_compressor),
g_vad_diag.min_probability, g_vad_diag.max_probability);
}
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "VAD summary group=%016llx span=%.1fms windows=%u audio=%ums speech=%u "
"busy=%u confirm_resets=%u errors=%u probability=%.3f..%.3f threshold=%.3f reason=%s "
"input_peak=%.1fdBFS model_peak=%.1fdBFS gain=%.1fdB clips=%u inference_max=%.1fms gap_max=%.1fms "
@@ -1023,6 +1047,8 @@ static void radio_audio_vad_sample(const int16_t* frame) {
g_vad_diag.windows = g_vad_diag.speech = g_vad_diag.busy = g_vad_diag.confirm_resets = g_vad_diag.errors = 0;
g_vad_diag.min_probability = g_vad_diag.max_probability = g_vad_diag.max_input_rms = g_vad_diag.max_model_rms = 0;
g_vad_diag.max_inference_tb = g_vad_diag.max_gap_tb = 0;
g_vad_diag.clipped_samples = g_vad_diag.clipped_frames = 0;
g_vad_diag.input_sample_peak = g_vad_diag.output_sample_peak = 0;
}
}
@@ -1033,11 +1059,13 @@ static void radio_audio_vad_feed(const int16_t* frame) {
for (int ch = 0; ch < g_capture_channels; ++ch) {
double sample = (double)frame[ch] / 32768.0;
g_vad_input_sum_sq += sample * sample;
if (fabs(sample) > g_vad_diag.input_sample_peak) g_vad_diag.input_sample_peak = (float)fabs(sample);
}
if (++g_vad_pcm_frames != RADIO_AUDIO_FRAME_SAMPLES) return;
int count = RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels;
g_vad_input_rms = sqrtf((float)(g_vad_input_sum_sq / count));
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0;
uint32_t clips_before = audio_compressor_clip_count(g_vad_compressor);
if (audio_compressor_process_frame(g_vad_compressor, g_vad_pcm, g_vad_pcm, count) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: VAD compressor frame failed count=%d — auto-PTT disabled until capture restart",
RADIO_AUDIO_ID, count);
@@ -1046,10 +1074,14 @@ static void radio_audio_vad_feed(const int16_t* frame) {
radio_audio_update_transmission();
return;
}
uint32_t clips = audio_compressor_clip_count(g_vad_compressor) - clips_before;
g_vad_diag.clipped_samples += clips;
if (clips) ++g_vad_diag.clipped_frames;
double sum_sq = 0;
for (int i = 0; i < count; ++i) {
double sample = (double)g_vad_pcm[i] / 32768.0;
sum_sq += sample * sample;
if (fabs(sample) > g_vad_diag.output_sample_peak) g_vad_diag.output_sample_peak = (float)fabs(sample);
}
g_vad_output_rms = sqrtf((float)(sum_sq / count));
for (int i = 0; i < count && !g_auto_blocked; i += g_capture_channels) radio_audio_vad_sample(g_vad_pcm + i);
@@ -1092,10 +1124,14 @@ static void radio_audio_level_feed(const int16_t* frame) {
struct radio_audio_input_status radio_audio_get_input_status(void) {
pthread_mutex_lock(&g_mtx);
struct radio_audio_input_status status = {g_level_dbfs, 0, 0};
struct radio_audio_input_status status = {.level_dbfs = g_level_dbfs, .vad_probability = g_vad_probability};
uint64_t now = get_time_tb();
status.valid = g_active && g_encoder && g_level_updated_tb && now - g_level_updated_tb <= 2000;
status.blocked = g_active && radio_audio_rx_busy(now);
status.vad_valid = g_active && g_encoder && g_auto_mode == RADIO_AUTO_VAD && !status.blocked &&
g_vad_updated_tb && now - g_vad_updated_tb <= 2000;
status.vad_gain_db = audio_compressor_gain_db(g_vad_compressor);
status.vad_compressor_enabled = audio_compressor_is_enabled(g_vad_compressor);
pthread_mutex_unlock(&g_mtx);
return status;
}
+4
View File
@@ -44,6 +44,10 @@ struct radio_audio_input_status {
float level_dbfs; /* RMS за 20мс после AEC, до компрессоров; тишина = -120dBFS. */
int valid; /* Есть PCM не старше 200мс. */
int blocked; /* Чужая речь / дренаж вывода / защитная пауза. */
float vad_probability; /* Вероятность речи 0..1 из последнего успешного окна Silero. */
int vad_valid; /* VAD работает, эфир свободен, оценка не старше 200мс. */
float vad_gain_db; /* Фактическое усиление отдельного компрессора VAD; 0 при выключенном. */
int vad_compressor_enabled;
};
/* Любой поток: короткий снимок под mutex, без вызовов GUI из аудиопотока. */
struct radio_audio_input_status radio_audio_get_input_status(void);