You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
5.5 KiB
134 lines
5.5 KiB
/* Тест чистой логики 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; |
|
}
|
|
|