135 lines
5.5 KiB
C
135 lines
5.5 KiB
C
/* Тест чистой логики VAD авто-PTT рации (radio_vad.h): ресемплер 48k→16k и
|
|
* фильтр-гистерезис + машина состояний. Без ONNX Runtime и без аудио-устройств —
|
|
* детерминированные синтетические последовательности.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#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;
|
|
}
|