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

+127 -7
View File
@@ -17,7 +17,7 @@
#include "../lib/silero_vad.h"
#include "../lib/audio_compressor.h"
#include "../lib/debug_config.h"
#include "../src/radio/radio_vad.h"
#include "../src/radio/radio_auto.h"
#define PI 3.14159265358979323846
@@ -198,8 +198,8 @@ static void test_speech(const char* model_path) {
if (!vad) return;
for (int pass = 0; pass < 2; pass++) {
silero_vad_reset(vad);
struct radio_vad_fsm fsm;
radio_vad_fsm_reset(&fsm);
struct radio_auto_fsm fsm;
radio_auto_fsm_reset(&fsm);
uint64_t now = 1;
float max_prob = 0;
int starts = 0, stops = 0;
@@ -223,7 +223,7 @@ static void test_speech(const char* model_path) {
if (probability > max_prob) max_prob = probability;
}
now += 320;
int transition = radio_vad_fsm_update(&fsm, probability, 0.5f, 2, 2000, 0, now);
int transition = radio_auto_fsm_update(&fsm, probability, 0.5f, 0.5f, 2, 2000, 0, now);
if (transition == 1 && window < SPEECH_WINDOWS) starts++;
if (transition == -1 && window >= SPEECH_WINDOWS) stops++;
}
@@ -260,8 +260,8 @@ static void test_compressed_speech(float amplitude, int max_gain_db) {
printf("[silero_vad] independent AGC speech amplitude=%.3f max_gain=%ddB + low noise\n", amplitude, max_gain_db);
for (int pass = 0; pass < 2; ++pass) {
audio_compressor_reset(ac); silero_vad_reset(vad);
struct radio_vad_fsm fsm;
radio_vad_fsm_reset(&fsm);
struct radio_auto_fsm fsm;
radio_auto_fsm_reset(&fsm);
uint32_t noise = 1;
float peak = 0, noise_peak = 0;
int window_len = 0, window_idx = 0, starts = 0, stops = 0;
@@ -293,7 +293,7 @@ static void test_compressed_speech(float amplitude, int max_gain_db) {
int speech = end_frame > NOISE_FRAMES && end_frame <= NOISE_FRAMES + SPEECH_SAMPLES * 3;
if (speech && probability > peak) peak = probability;
if (end_frame <= NOISE_FRAMES && probability > noise_peak) noise_peak = probability;
int transition = radio_vad_fsm_update(&fsm, probability, 0.5f, 2, 2000, 0, (uint64_t)(window_idx + 1) * 320);
int transition = radio_auto_fsm_update(&fsm, probability, 0.5f, 0.5f, 2, 2000, 0, (uint64_t)(window_idx + 1) * 320);
if (transition > 0) starts++;
if (transition < 0) stops++;
window_len = 0; ++window_idx;
@@ -309,6 +309,111 @@ static void test_compressed_speech(float amplitude, int max_gain_db) {
audio_compressor_destroy(ac); silero_vad_destroy(vad);
}
/* Диагностика: один и тот же PCM повторяется без reset, виден эффект истории AGC/Silero. */
static void probe_repeated_speech(float amplitude, int noise_amplitude, int max_gain_db) {
enum { FRAMES = 153600, REPEATS = 4, SPEECH_START = 61440, SPEECH_FRAMES = 16384 * 3 };
int16_t speech[16384], pcm[960];
FILE* file = fopen(TEST_SILERO_SPEECH_PATH, "rb");
CHECK(file != NULL, "probe fixture open"); if (!file) return;
size_t size = fread(speech, sizeof(*speech), 16384, file); fclose(file);
CHECK(size == 16384, "probe fixture size"); if (size != 16384) return;
struct audio_compressor* ac = audio_compressor_create();
silero_vad_t* vad = silero_vad_create_default();
CHECK(ac && vad, "probe create");
if (!ac || !vad) { audio_compressor_destroy(ac); silero_vad_destroy(vad); return; }
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1; cfg.block_duration_ms = 20; cfg.lookback_ms = 200;
cfg.max_gain_db = max_gain_db < 0 ? 0 : max_gain_db; cfg.rise_rate_per_sec = 4; cfg.target_level = 0.25f;
CHECK(audio_compressor_configure(ac, &cfg) == 0, "probe configure");
audio_compressor_set_enabled(ac, max_gain_db >= 0);
float window[512]; int used = 0;
for (int repeat = 0; repeat < REPEATS; ++repeat) {
uint32_t random = 1, clips = audio_compressor_clip_count(ac);
float peak_probability = 0, probability_sum = 0, noise_peak = 0, gain_at_start = 0;
int speech_windows = 0, high_windows = 0;
for (int pos = 0; pos < FRAMES; pos += 960) {
for (int i = 0; i < 960; ++i) {
random = random * 1664525u + 1013904223u;
int value = noise_amplitude * ((int)(random >> 16) - 32768) / 32768;
int offset = pos + i - SPEECH_START;
if (offset >= 0 && offset < SPEECH_FRAMES) value += (int)(speech[offset / 3] * amplitude);
pcm[i] = (int16_t)(value > 32767 ? 32767 : value < -32768 ? -32768 : value);
}
if (pos == SPEECH_START) gain_at_start = audio_compressor_gain_db(ac);
CHECK(audio_compressor_process_frame(ac, pcm, pcm, 960) == 0, "probe process frame");
for (int i = 0; i < 960; i += 3) {
window[used++] = ((float)pcm[i] + pcm[i + 1] + pcm[i + 2]) / (3 * 32768.0f);
if (used != 512) continue;
used = 0; float probability = 0;
CHECK(silero_vad_process(vad, window, &probability) == 0, "probe VAD");
int end = pos + i + 3;
if (end > SPEECH_START && end <= SPEECH_START + SPEECH_FRAMES) {
if (probability > peak_probability) peak_probability = probability;
probability_sum += probability; ++speech_windows;
if (probability >= 0.8f) ++high_windows;
} else if (end <= SPEECH_START && probability > noise_peak) noise_peak = probability;
}
}
printf("VAD_PROBE amplitude=%.2f noise=%d gain_limit=%d repeat=%d start_gain=%.1f clips=%u speech_peak=%.4f "
"speech_mean=%.4f high=%d/%d noise_peak=%.4f\n", amplitude, noise_amplitude, max_gain_db, repeat,
gain_at_start, audio_compressor_clip_count(ac) - clips, peak_probability, probability_sum / speech_windows,
high_windows, speech_windows, noise_peak);
}
audio_compressor_destroy(ac); silero_vad_destroy(vad);
}
/* PCM mono s16le/48кГц: две модели получают одинаковый выход AGC; сброс отличается только в указанном окне. */
static void probe_stream(const char* path, unsigned reset_window, int max_gain_db) {
FILE* file = fopen(path, "rb");
CHECK(file != NULL, "stream open: %s", path); if (!file) return;
struct audio_compressor* ac = audio_compressor_create();
silero_vad_t* history = silero_vad_create_default();
silero_vad_t* reset = silero_vad_create_default();
CHECK(ac && history && reset, "stream create");
if (!ac || !history || !reset) goto done;
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1; cfg.block_duration_ms = 20; cfg.lookback_ms = 200;
cfg.max_gain_db = max_gain_db < 0 ? 0 : max_gain_db; cfg.rise_rate_per_sec = 4; cfg.target_level = 0.25f;
int rc = audio_compressor_configure(ac, &cfg);
CHECK(rc == 0, "stream compressor configure"); if (rc != 0) goto done;
audio_compressor_set_enabled(ac, max_gain_db >= 0);
uint8_t bytes[1920]; int16_t pcm[960]; float window[512];
unsigned used = 0, index = 0;
size_t count;
while ((count = fread(bytes, 1, sizeof(bytes), file)) != 0) {
CHECK(count == sizeof(bytes), "stream requires full 20ms frames, tail=%zu", count);
if (count != sizeof(bytes)) break;
for (int i = 0; i < 960; ++i) {
int value = bytes[2*i] | ((int)bytes[2*i+1] << 8);
pcm[i] = (int16_t)(value >= 32768 ? value - 65536 : value);
}
rc = audio_compressor_process_frame(ac, pcm, pcm, 960);
CHECK(rc == 0, "stream compressor frame"); if (rc != 0) break;
for (int i = 0; i < 960; i += 3) {
window[used++] = ((float)pcm[i] + pcm[i+1] + pcm[i+2]) / (3 * 32768.0f);
if (used != 512) continue;
used = 0;
if (index == reset_window) silero_vad_reset(reset);
float p = 0, q = 0;
int a = silero_vad_process(history, window, &p), b = silero_vad_process(reset, window, &q);
CHECK(a == 0 && b == 0 && isfinite(p) && isfinite(q), "stream inference window=%u", index);
if (a || b) goto done;
if (index < reset_window) CHECK(fabsf(p - q) < 1e-6f, "stream models differ before reset window=%u", index);
double energy = 0;
for (int j = 0; j < 512; ++j) energy += window[j] * window[j];
printf("VAD_STREAM gain_limit=%d window=%u time_ms=%u reset_window=%u history=%.6f reset=%.6f "
"rms=%.6f gain=%.2f clips=%u\n", max_gain_db, index, index * 32, reset_window, p, q,
sqrt(energy / 512), audio_compressor_gain_db(ac), audio_compressor_clip_count(ac));
++index;
}
}
CHECK(!ferror(file), "stream read failed");
CHECK(index > reset_window, "stream ended before reset window=%u total=%u", reset_window, index);
done:
fclose(file); audio_compressor_destroy(ac); silero_vad_destroy(history); silero_vad_destroy(reset);
}
int main(int argc, char** argv) {
const char* model_path = (argc > 1) ? argv[1] : "../lib/silero_vad.onnx";
@@ -318,6 +423,21 @@ int main(int argc, char** argv) {
printf("Silero VAD test, model: %s\n", model_path);
if (argc > 2 && !strcmp(argv[2], "--probe")) {
const int gains[] = {-1, 1, 15, 30}, noises[] = {0, 300, 2000};
for (int a = 0; a < 2; ++a) for (int n = 0; n < 3; ++n) for (int g = 0; g < 4; ++g)
probe_repeated_speech(a ? 1.0f : 0.1f, noises[n], gains[g]);
return g_failures ? 1 : 0;
}
if (argc > 2 && !strcmp(argv[2], "--probe-stream")) {
unsigned reset_window = 0; char extra;
if (argc != 5 || sscanf(argv[4], "%u%c", &reset_window, &extra) != 1 || reset_window > 1000000) {
fprintf(stderr, "Usage: %s model --probe-stream mono48k.s16le reset_window\n", argv[0]); return 1;
}
const int gains[] = {-1, 1, 15};
for (int i = 0; i < 3; ++i) probe_stream(argv[3], reset_window, gains[i]);
return g_failures ? 1 : 0;
}
test_lifecycle_and_errors(model_path);
test_silence(model_path);
test_pcm16_matches_float(model_path);