/* Тест чистой логики VAD авто-PTT рации (radio_vad.h): ресемплер 48k→16k и * фильтр-гистерезис + машина состояний. Без ONNX Runtime и без аудио-устройств — * детерминированные синтетические последовательности. */ #include #include #include #include "../src/radio/radio_vad.h" #define PI 3.14159265358979323846 static int g_failures = 0; #define CHECK(cond, ...) do { \ if (!(cond)) { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_failures++; } \ } while (0) static int near(float a, float b, float eps) { return fabsf(a - b) <= eps; } /* ── ресемплер ── */ static void test_resample(void) { printf("[radio_vad] resampler 48k -> 16k\n"); /* тишина → тишина */ { int16_t in[960] = {0}; float out[RADIO_VAD_FRAME_SAMPLES_16K]; radio_vad_resample(in, 1, out); for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(out[i] == 0.0f, "silence out[%d]=%f", i, out[i]); } /* DC 0.5*32768 → выход ~0.5 (box-фильтр сохраняет DC) */ { int16_t in[960]; float out[RADIO_VAD_FRAME_SAMPLES_16K]; for (int i = 0; i < 960; i++) in[i] = 16384; radio_vad_resample(in, 1, out); for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(near(out[i], 0.5f, 0.001f), "DC out[%d]=%f", i, out[i]); } /* стерео L=R → тот же результат, что моно */ { int16_t mono[960]; int16_t stereo[1920]; float om[RADIO_VAD_FRAME_SAMPLES_16K], os[RADIO_VAD_FRAME_SAMPLES_16K]; for (int i = 0; i < 960; i++) { mono[i] = (int16_t)(sin(2.0 * PI * 1000.0 * i / 48000.0) * 20000.0); } for (int i = 0; i < 960; i++) { stereo[2*i] = mono[i]; stereo[2*i+1] = mono[i]; } radio_vad_resample(mono, 1, om); radio_vad_resample(stereo, 2, os); for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(near(om[i], os[i], 1e-4f), "stereo!=mono out[%d] %f vs %f", i, om[i], os[i]); } /* синус 1кГц: амплитуда сохраняется (box-фильтр даёт ~0.6% ослабления) */ { int16_t in[960]; float out[RADIO_VAD_FRAME_SAMPLES_16K]; float peak = 0.0f; for (int i = 0; i < 960; i++) in[i] = (int16_t)(sin(2.0 * PI * 1000.0 * i / 48000.0) * 30000.0); radio_vad_resample(in, 1, out); for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) { float a = fabsf(out[i]); if (a > peak) peak = a; } CHECK(peak > 0.85f && peak <= 1.0f, "1kHz peak=%.3f (ожидали ~0.91)", peak); } } /* ── машина состояний ── */ static void test_fsm(void) { printf("[radio_vad] fsm (confirm / hangover / busy)\n"); struct radio_vad_fsm f; const float thr = 0.5f; const int confirm = 2; const uint64_t hangover = 2000; /* 200мс */ uint64_t now = 0; /* подтверждение: 2 окна выше порога → старт */ radio_vad_fsm_reset(&f); now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 0, "confirm#1 должен молчать"); now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 1, "confirm#2 должен дать START"); CHECK(f.talking == 1, "после старта talking=1"); /* одиночный пик не запускает передачу */ radio_vad_fsm_reset(&f); now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 0, "одиночный пик не стартует"); now += 320; CHECK(radio_vad_fsm_update(&f, 0.1f, thr, confirm, hangover, 0, now) == 0, "сброс подтверждения"); /* hangover: после старта тишина ~200мс → STOP */ radio_vad_fsm_reset(&f); now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now); now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 1, "старт"); { int stops = 0; for (int i = 0; i < 10; i++) { now += 320; int a = radio_vad_fsm_update(&f, 0.1f, thr, confirm, hangover, 0, now); if (a == -1) { stops++; break; } CHECK(a == 0, "hangover: до ~200мс не стопим"); } CHECK(stops == 1, "hangover должен дать STOP"); CHECK(f.talking == 0, "после STOP talking=0"); } /* busy: чужой разговор гасит нашу передачу сразу */ radio_vad_fsm_reset(&f); now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now); now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now); CHECK(f.talking == 1, "старт перед busy"); now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 1, now) == -1, "busy должен дать STOP"); /* busy блокирует старт */ radio_vad_fsm_reset(&f); for (int i = 0; i < 5; i++) { now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 1, now) == 0, "busy блокирует старт"); } CHECK(f.talking == 0, "busy: не стартуем"); } int main(void) { test_resample(); test_fsm(); if (g_failures == 0) { printf("TEST PASSED\n"); return 0; } printf("TEST FAILED (%d failures)\n", g_failures); return 1; }