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.
338 lines
15 KiB
338 lines
15 KiB
/* Тест Silero VAD-обёртки (lib/silero_vad) поверх ONNX Runtime. |
|
* |
|
* Проверяет: |
|
* - загрузку модели и жизненный цикл (create/destroy/reset); |
|
* - обработку ошибок (пустой/несуществующий путь, NULL-аргументы); |
|
* - что на тишине вероятность речи близка к нулю (реальный инференс, не мусор); |
|
* - что путь pcm16 даёт тот же результат, что и float (конвертация /32768); |
|
* - что вероятность всегда лежит в [0;1]. |
|
* - что речь запускает авто-PTT, тишина останавливает, reset воспроизводит тот же поток. |
|
* |
|
* Модель ищется в argv[1], по умолчанию "../lib/silero_vad.onnx" (запуск из tests/). |
|
*/ |
|
#include <stdio.h> |
|
#include <string.h> |
|
#include <math.h> |
|
|
|
#include "../lib/silero_vad.h" |
|
#include "../lib/audio_compressor.h" |
|
#include "../lib/debug_config.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) |
|
|
|
/* Детерминированный «гласный»: модулированный гармонический комплекс (f0 ~120 Гц). */ |
|
static void gen_vowel(float* out, int n, int offset) { |
|
int i; |
|
for (i = 0; i < n; i++) { |
|
double t = (double)(offset + i) / 16000.0; |
|
double f0 = 120.0 + 20.0 * sin(2.0 * PI * 3.0 * t); |
|
double env = 0.5 + 0.5 * sin(2.0 * PI * 2.0 * t); |
|
double s = sin(2.0 * PI * f0 * t) |
|
+ 0.5 * sin(2.0 * PI * 2.0 * f0 * t) |
|
+ 0.3 * sin(2.0 * PI * 3.0 * f0 * t) |
|
+ 0.2 * sin(2.0 * PI * 4.0 * f0 * t); |
|
out[i] = (float)(s * env * 0.25); |
|
} |
|
} |
|
|
|
static int test_lifecycle_and_errors(const char* model_path) { |
|
silero_vad_t* vad; |
|
|
|
printf("[silero_vad] lifecycle + error paths\n"); |
|
|
|
vad = silero_vad_create(NULL); |
|
CHECK(vad == NULL, "create(NULL) should fail"); |
|
if (vad) silero_vad_destroy(vad); |
|
|
|
vad = silero_vad_create(""); |
|
CHECK(vad == NULL, "create(\"\") should fail"); |
|
|
|
vad = silero_vad_create("/nonexistent/silero_vad.onnx"); |
|
CHECK(vad == NULL, "create(bad path) should fail"); |
|
|
|
vad = silero_vad_create(model_path); |
|
CHECK(vad != NULL, "create(%s) should succeed", model_path); |
|
if (!vad) return 1; |
|
|
|
silero_vad_reset(vad); |
|
silero_vad_reset(vad); /* повторный reset безопасен */ |
|
silero_vad_destroy(vad); |
|
silero_vad_destroy(NULL); /* destroy(NULL) безопасен */ |
|
return 0; |
|
} |
|
|
|
static int test_silence(const char* model_path) { |
|
silero_vad_t* vad; |
|
float zeros[SILERO_VAD_WINDOW_SAMPLES]; |
|
float prob = -1.0f; |
|
float max_prob = 0.0f; |
|
int i, rc; |
|
|
|
printf("[silero_vad] silence -> prob ~ 0\n"); |
|
memset(zeros, 0, sizeof(zeros)); |
|
|
|
vad = silero_vad_create(model_path); |
|
CHECK(vad != NULL, "create failed"); |
|
if (!vad) return 1; |
|
|
|
for (i = 0; i < 32; i++) { |
|
rc = silero_vad_process(vad, zeros, &prob); |
|
CHECK(rc == 0, "process(%d) rc=%d", i, rc); |
|
CHECK(prob >= 0.0f && prob <= 1.0f, "prob out of range: %f", prob); |
|
if (prob > max_prob) max_prob = prob; |
|
} |
|
printf(" silence max_prob=%.4f\n", max_prob); |
|
CHECK(max_prob < 0.2f, "silence probability too high: %f", max_prob); |
|
|
|
silero_vad_destroy(vad); |
|
return 0; |
|
} |
|
|
|
static int test_pcm16_matches_float(const char* model_path) { |
|
silero_vad_t* vad; |
|
int16_t pcm[SILERO_VAD_WINDOW_SAMPLES]; |
|
float samples[SILERO_VAD_WINDOW_SAMPLES]; |
|
float prob_f = -1.0f, prob_i = -1.0f; |
|
int rc; |
|
|
|
printf("[silero_vad] pcm16 path == float path\n"); |
|
|
|
vad = silero_vad_create(model_path); |
|
CHECK(vad != NULL, "create failed"); |
|
if (!vad) return 1; |
|
|
|
gen_vowel(samples, SILERO_VAD_WINDOW_SAMPLES, 0); |
|
for (int i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) { |
|
pcm[i] = (int16_t)(samples[i] * 32768.0f); |
|
samples[i] = (float)pcm[i] / 32768.0f; /* Сравниваем один сигнал после квантования. */ |
|
} |
|
|
|
rc = silero_vad_process(vad, samples, &prob_f); |
|
CHECK(rc == 0, "process(float) rc=%d", rc); |
|
|
|
silero_vad_reset(vad); /* одинаковое состояние перед вторым прогоном */ |
|
|
|
rc = silero_vad_process_pcm16(vad, pcm, &prob_i); |
|
CHECK(rc == 0, "process_pcm16 rc=%d", rc); |
|
|
|
printf(" float prob=%.5f pcm16 prob=%.5f\n", prob_f, prob_i); |
|
CHECK(fabsf(prob_f - prob_i) < 1e-4f, "pcm16 != float: %f vs %f", prob_i, prob_f); |
|
|
|
silero_vad_destroy(vad); |
|
return 0; |
|
} |
|
|
|
static int test_default_model(void) { |
|
silero_vad_t* vad; |
|
float zeros[SILERO_VAD_WINDOW_SAMPLES]; |
|
float prob = -1.0f; |
|
int rc; |
|
|
|
printf("[silero_vad] create_default (embedded model)\n"); |
|
memset(zeros, 0, sizeof(zeros)); |
|
|
|
vad = silero_vad_create_default(); |
|
CHECK(vad != NULL, "create_default failed"); |
|
if (!vad) return 1; |
|
|
|
rc = silero_vad_process(vad, zeros, &prob); |
|
CHECK(rc == 0, "process rc=%d", rc); |
|
CHECK(prob >= 0.0f && prob <= 1.0f, "prob out of range: %f", prob); |
|
CHECK(prob < 0.2f, "silence prob too high: %f", prob); |
|
printf(" embedded silence prob=%.4f\n", prob); |
|
|
|
silero_vad_destroy(vad); |
|
return 0; |
|
} |
|
|
|
static int test_null_args(const char* model_path) { |
|
silero_vad_t* vad; |
|
float samples[SILERO_VAD_WINDOW_SAMPLES]; |
|
float prob; |
|
|
|
printf("[silero_vad] NULL-arg guards\n"); |
|
memset(samples, 0, sizeof(samples)); |
|
|
|
vad = silero_vad_create(model_path); |
|
CHECK(vad != NULL, "create failed"); |
|
if (!vad) return 1; |
|
|
|
CHECK(silero_vad_process(NULL, samples, &prob) != 0, "process(NULL vad) should fail"); |
|
CHECK(silero_vad_process(vad, NULL, &prob) != 0, "process(NULL samples) should fail"); |
|
CHECK(silero_vad_process(vad, samples, NULL) != 0, "process(NULL prob) should fail"); |
|
CHECK(silero_vad_process_pcm16(vad, NULL, &prob) != 0, "process_pcm16(NULL) should fail"); |
|
|
|
silero_vad_destroy(vad); |
|
return 0; |
|
} |
|
|
|
/* Речевая запись обязательна: тесты на тишине не замечают неверный вход модели. */ |
|
static void test_speech(const char* model_path) { |
|
enum { SPEECH_WINDOWS = 32, SILENCE_WINDOWS = 64 }; |
|
uint8_t bytes[SPEECH_WINDOWS * SILERO_VAD_WINDOW_SAMPLES * 2]; |
|
float reference[SPEECH_WINDOWS]; |
|
int16_t pcm[SILERO_VAD_WINDOW_SAMPLES]; |
|
float silence[SILERO_VAD_WINDOW_SAMPLES] = {0}; |
|
printf("[silero_vad] speech -> auto-PTT -> silence -> reset (%s)\n", model_path ? "file" : "embedded"); |
|
FILE* file = fopen(TEST_SILERO_SPEECH_PATH, "rb"); |
|
CHECK(file != NULL, "cannot open speech fixture: %s", TEST_SILERO_SPEECH_PATH); |
|
if (!file) return; |
|
size_t size = fread(bytes, 1, sizeof(bytes), file); |
|
int extra = fgetc(file); |
|
fclose(file); |
|
CHECK(size == sizeof(bytes) && extra == EOF, "invalid speech fixture size: %zu", size); |
|
if (size != sizeof(bytes) || extra != EOF) return; |
|
|
|
silero_vad_t* vad = model_path ? silero_vad_create(model_path) : silero_vad_create_default(); |
|
CHECK(vad != NULL, "speech detector create failed"); |
|
if (!vad) return; |
|
for (int pass = 0; pass < 2; pass++) { |
|
silero_vad_reset(vad); |
|
struct radio_vad_fsm fsm; |
|
radio_vad_fsm_reset(&fsm); |
|
uint64_t now = 1; |
|
float max_prob = 0; |
|
int starts = 0, stops = 0; |
|
for (int window = 0; window < SPEECH_WINDOWS + SILENCE_WINDOWS; window++) { |
|
float probability = -1; |
|
int rc; |
|
if (window < SPEECH_WINDOWS) { |
|
for (int i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) { |
|
size_t offset = ((size_t)window * SILERO_VAD_WINDOW_SAMPLES + i) * 2; |
|
int value = bytes[offset] | ((int)bytes[offset + 1] << 8); |
|
pcm[i] = (int16_t)(value >= 32768 ? value - 65536 : value); |
|
} |
|
rc = silero_vad_process_pcm16(vad, pcm, &probability); |
|
} else rc = silero_vad_process(vad, silence, &probability); |
|
CHECK(rc == 0, "speech process pass=%d window=%d rc=%d", pass, window, rc); |
|
if (rc != 0) { silero_vad_destroy(vad); return; } |
|
CHECK(probability >= 0 && probability <= 1, "speech probability out of range: %f", probability); |
|
if (window < SPEECH_WINDOWS) { |
|
if (!pass) reference[window] = probability; |
|
else CHECK(fabsf(reference[window] - probability) < 1e-5f, "reset changed speech window=%d", window); |
|
if (probability > max_prob) max_prob = probability; |
|
} |
|
now += 320; |
|
int transition = radio_vad_fsm_update(&fsm, probability, 0.5f, 2, 2000, 0, now); |
|
if (transition == 1 && window < SPEECH_WINDOWS) starts++; |
|
if (transition == -1 && window >= SPEECH_WINDOWS) stops++; |
|
} |
|
printf(" pass=%d speech max_prob=%.5f starts=%d silence_stops=%d talking=%d\n", pass, max_prob, starts, stops, fsm.talking); |
|
CHECK(max_prob > 0.8f && starts > 0, "speech did not activate auto-PTT"); |
|
CHECK(stops > 0 && !fsm.talking, "silence did not stop auto-PTT"); |
|
} |
|
silero_vad_destroy(vad); |
|
} |
|
|
|
/* Реальная цепочка 48кГц/20мс AGC → mono 16кГц/512 → Silero → авто-PTT; фон не должен включать передачу. */ |
|
static void test_compressed_speech(float amplitude, int max_gain_db) { |
|
enum { SPEECH_SAMPLES = 16384, BLOCKS = 200, NOISE_FRAMES = 48000 }; |
|
uint8_t bytes[SPEECH_SAMPLES * 2]; |
|
FILE* file = fopen(TEST_SILERO_SPEECH_PATH, "rb"); |
|
CHECK(file != NULL, "cannot open compressor speech fixture"); |
|
if (!file) return; |
|
size_t size = fread(bytes, 1, sizeof(bytes), file); |
|
fclose(file); |
|
CHECK(size == sizeof(bytes), "invalid compressor speech fixture size=%zu", size); |
|
if (size != sizeof(bytes)) return; |
|
struct audio_compressor* ac = audio_compressor_create(); |
|
silero_vad_t* vad = silero_vad_create_default(); |
|
CHECK(ac && vad, "compressed speech create failed"); |
|
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; cfg.rise_rate_per_sec = 2; cfg.target_level = 0.25f; |
|
int rc = audio_compressor_configure(ac, &cfg); |
|
CHECK(rc == 0, "compressed speech configure failed"); |
|
if (rc != 0) { audio_compressor_destroy(ac); silero_vad_destroy(vad); return; } |
|
float reference[BLOCKS], window[SILERO_VAD_WINDOW_SAMPLES]; |
|
int16_t pcm[960]; |
|
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); |
|
uint32_t noise = 1; |
|
float peak = 0, noise_peak = 0; |
|
int window_len = 0, window_idx = 0, starts = 0, stops = 0; |
|
for (int block = 0; block < BLOCKS; ++block) { |
|
for (int i = 0; i < 960; ++i) { |
|
int offset = block * 960 + i - NOISE_FRAMES; |
|
noise = noise * 1664525u + 1013904223u; |
|
int sample = (int)(noise >> 27) - 16; |
|
if (offset >= 0 && offset < SPEECH_SAMPLES * 3) { |
|
int index = offset / 3 * 2; |
|
int value = bytes[index] | ((int)bytes[index + 1] << 8); |
|
sample += (int)((value >= 32768 ? value - 65536 : value) * amplitude); |
|
} |
|
if (sample > 32767) sample = 32767; |
|
if (sample < -32768) sample = -32768; |
|
pcm[i] = (int16_t)sample; |
|
} |
|
rc = audio_compressor_process_frame(ac, pcm, pcm, 960); |
|
CHECK(rc == 0, "compressed speech frame failed block=%d", block); |
|
for (int i = 0; i < 960; i += 3) { |
|
window[window_len++] = ((float)pcm[i] + pcm[i + 1] + pcm[i + 2]) / (3.0f * 32768.0f); |
|
if (window_len != SILERO_VAD_WINDOW_SAMPLES) continue; |
|
float probability = -1; |
|
rc = silero_vad_process(vad, window, &probability); |
|
CHECK(rc == 0 && probability >= 0 && probability <= 1, "compressed speech process failed window=%d", window_idx); |
|
if (!pass) reference[window_idx] = probability; |
|
else CHECK(fabsf(reference[window_idx] - probability) < 1e-5f, "compressed reset changed window=%d", window_idx); |
|
int end_frame = block * 960 + i + 3; |
|
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); |
|
if (transition > 0) starts++; |
|
if (transition < 0) stops++; |
|
window_len = 0; ++window_idx; |
|
} |
|
} |
|
printf(" pass=%d speech_peak=%.5f noise_peak=%.5f starts=%d stops=%d gain=%.1fdB clips=%u\n", pass, |
|
peak, noise_peak, starts, stops, audio_compressor_gain_db(ac), audio_compressor_clip_count(ac)); |
|
CHECK(noise_peak < 0.5f, "background triggered compressed VAD"); |
|
CHECK(peak > 0.8f && starts > 0, "compressed speech did not activate auto-PTT"); |
|
CHECK(stops > 0 && !fsm.talking, "compressed background did not stop auto-PTT"); |
|
CHECK(audio_compressor_gain_db(ac) <= max_gain_db + 0.001f, "compressor exceeded max gain"); |
|
} |
|
audio_compressor_destroy(ac); silero_vad_destroy(vad); |
|
} |
|
|
|
int main(int argc, char** argv) { |
|
const char* model_path = (argc > 1) ? argv[1] : "../lib/silero_vad.onnx"; |
|
|
|
debug_config_init(); |
|
debug_set_level(DEBUG_LEVEL_INFO); |
|
debug_set_category_level(DEBUG_CATEGORY_VAD, DEBUG_LEVEL_INFO); |
|
|
|
printf("Silero VAD test, model: %s\n", model_path); |
|
|
|
test_lifecycle_and_errors(model_path); |
|
test_silence(model_path); |
|
test_pcm16_matches_float(model_path); |
|
test_null_args(model_path); |
|
test_default_model(); |
|
test_speech(model_path); |
|
test_speech(NULL); |
|
test_compressed_speech(1.0f, 15); |
|
test_compressed_speech(0.03f, 15); |
|
test_compressed_speech(0.03f, 30); |
|
|
|
if (g_failures == 0) { |
|
printf("TEST PASSED\n"); |
|
return 0; |
|
} |
|
printf("TEST FAILED (%d failures)\n", g_failures); |
|
return 1; |
|
}
|
|
|