/* Тест обёртки эхоподавления lib/speex_aec (SpeexDSP mdf). * * Проверяет: * - жизненный цикл и NULL/невалидные аргументы (create/destroy/reset/process); * - линию задержки: passthrough на старте, наполнение до delay_frames, учёт дрейфа; * - накопление захвата/рендера чанками произвольного размера (480/960); * - собственно подавление эха: синтетический far-end (шум) + near-end (тон), * после адаптации остаток эха должен быть заметно меньше исходного эха. */ #include #include #include #include "../lib/speex_aec.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #define RATE 48000 #define FS 960 /* 20 мс */ #define FILTER 14400 /* хвост 300 мс */ #define DELAY_FRAMES 2 #define ECHO_GAIN 0.5 #define N_FRAMES 160 #define ADAPT_FRAMES 100 /* фаза адаптации: только far-end */ #define DT_START (ADAPT_FRAMES) #define DT_FRAMES 20 /* double-talk: речь + эхо */ #define MEAS_START (DT_START + DT_FRAMES) static int g_failures = 0; #define CHECK(cond, ...) do { \ if (!(cond)) { \ printf(" FAIL: " __VA_ARGS__); printf("\n"); \ g_failures++; \ } \ } while (0) /* Детерминированный «шум» far-end (LCG). */ static uint32_t g_rng = 0x12345678; static int16_t aec_noise(void) { g_rng = g_rng * 1664525u + 1013904223u; return (int16_t)(((g_rng >> 16) & 0x3fff) - 0x1fff); } static int test_lifecycle_and_null(void) { speex_aec_t* a; int16_t pcm[FS], out[FS]; printf("[speex_aec] lifecycle + NULL guards\n"); memset(pcm, 0, sizeof(pcm)); a = speex_aec_create(RATE, FS, FILTER, DELAY_FRAMES); CHECK(a != NULL, "create failed"); if (!a) return 1; CHECK(speex_aec_create(0, FS, FILTER, DELAY_FRAMES) == NULL, "create(rate=0) should fail"); CHECK(speex_aec_create(RATE, 0, FILTER, DELAY_FRAMES) == NULL, "create(frame=0) should fail"); CHECK(speex_aec_create(RATE, FS, FS - 1, DELAY_FRAMES) == NULL, "create(filter= DT_START && k < MEAS_START); for (i = 0; i < FS; i++) { int echo_idx = k - DELAY_FRAMES; int16_t echo = echo_idx >= 0 ? (int16_t)(ECHO_GAIN * render_hist[echo_idx * FS + i]) : 0; cap[i] = double_talk ? (int16_t)(speech[i] + echo) : echo; } n = speex_aec_process_capture(a, cap, FS, out); CHECK(n == FS, "process_capture returned %d (expect %d)", n, FS); speex_aec_feed_playback(a, render_hist + k * FS, FS); if (double_talk) { for (i = 0; i < FS; i++) { dt_speech_pow += (double)speech[i] * speech[i]; dt_out_pow += (double)out[i] * out[i]; dt_cross += (double)speech[i] * out[i]; } dt_measured += FS; } else if (k >= MEAS_START) { for (i = 0; i < FS; i++) { int echo_idx = k - DELAY_FRAMES; int16_t echo = echo_idx >= 0 ? (int16_t)(ECHO_GAIN * render_hist[echo_idx * FS + i]) : 0; echo_pow += (double)echo * echo; resid_pow += (double)out[i] * out[i]; } measured += FS; } } { double echo_rms = sqrt(echo_pow / measured); double resid_rms = sqrt(resid_pow / measured); double db = echo_rms > 0 ? 20.0 * log10(echo_rms / (resid_rms > 1.0 ? resid_rms : 1.0)) : 0.0; printf(" echo-only suppression: echo_rms=%.1f resid_rms=%.1f => %.1f dB\n", echo_rms, resid_rms, db); CHECK(echo_rms > 100.0, "echo too weak to measure (echo_rms=%.1f)", echo_rms); CHECK(resid_pow < 0.05 * echo_pow, "echo not suppressed: resid=%.0f echo=%.0f", resid_pow, echo_pow); } { double corr = sqrt(dt_speech_pow * dt_out_pow); double sim = corr > 0.0 ? dt_cross / corr : 0.0; printf(" double-talk speech preservation: correlation=%.3f\n", sim); CHECK(sim > 0.7, "near-end speech distorted: correlation=%.3f", sim); } speex_aec_destroy(a); u_free(render_hist); return 0; } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_AEC, DEBUG_LEVEL_INFO); printf("SpeexDSP AEC test\n"); test_lifecycle_and_null(); test_delay_line(); test_echo_suppression(); if (g_failures == 0) { printf("TEST PASSED\n"); return 0; } printf("TEST FAILED (%d failures)\n", g_failures); return 1; }