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.
219 lines
8.8 KiB
219 lines
8.8 KiB
/* Тест обёртки эхоподавления lib/speex_aec (SpeexDSP mdf). |
|
* |
|
* Проверяет: |
|
* - жизненный цикл и NULL/невалидные аргументы (create/destroy/reset/process); |
|
* - линию задержки: passthrough на старте, наполнение до delay_frames, учёт дрейфа; |
|
* - накопление захвата/рендера чанками произвольного размера (480/960); |
|
* - собственно подавление эха: синтетический far-end (шум) + near-end (тон), |
|
* после адаптации остаток эха должен быть заметно меньше исходного эха. |
|
*/ |
|
#include <stdio.h> |
|
#include <string.h> |
|
#include <math.h> |
|
|
|
#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<frame) should fail"); |
|
CHECK(speex_aec_create(RATE, FS, FILTER, -3) != NULL, "create(delay=-3) should clamp, not fail"); |
|
|
|
CHECK(speex_aec_process_capture(NULL, pcm, FS, out) == 0, "process(NULL vad) should return 0"); |
|
CHECK(speex_aec_process_capture(a, NULL, FS, out) == 0, "process(NULL pcm) should return 0"); |
|
CHECK(speex_aec_process_capture(a, pcm, FS, NULL) == 0, "process(NULL out) should return 0"); |
|
CHECK(speex_aec_delay_fill(NULL) == 0, "delay_fill(NULL) should be 0"); |
|
|
|
speex_aec_feed_playback(NULL, pcm, FS); /* не должно падать */ |
|
speex_aec_reset(NULL); /* не должно падать */ |
|
speex_aec_destroy(NULL); /* не должно падать */ |
|
|
|
speex_aec_destroy(a); |
|
return 0; |
|
} |
|
|
|
static int test_delay_line(void) { |
|
speex_aec_t* a; |
|
int16_t in[FS], out[FS]; |
|
int16_t pcm[FS]; |
|
int i, n; |
|
|
|
printf("[speex_aec] delay line + chunk accumulation\n"); |
|
for (i = 0; i < FS; i++) pcm[i] = (int16_t)(i & 0x7ff); |
|
|
|
a = speex_aec_create(RATE, FS, FILTER, 3); |
|
CHECK(a != NULL, "create failed"); |
|
if (!a) return 1; |
|
|
|
CHECK(speex_aec_delay_fill(a) == 0, "initial fill=%d (expect 0)", speex_aec_delay_fill(a)); |
|
|
|
/* захват до рендера: passthrough (линия пуста) */ |
|
memset(in, 0x11, sizeof(in)); |
|
n = speex_aec_process_capture(a, in, FS, out); |
|
CHECK(n == FS, "process_capture returned %d (expect %d)", n, FS); |
|
CHECK(memcmp(in, out, sizeof(in)) == 0, "passthrough expected at empty line"); |
|
|
|
/* рендер чанками по 480 → накопление до кадра */ |
|
speex_aec_feed_playback(a, pcm, 480); |
|
CHECK(speex_aec_delay_fill(a) == 0, "fill=%d after 480 (expect 0)", speex_aec_delay_fill(a)); |
|
speex_aec_feed_playback(a, pcm + 480, 480); |
|
CHECK(speex_aec_delay_fill(a) == 1, "fill=%d after 960 (expect 1)", speex_aec_delay_fill(a)); |
|
|
|
/* захват чанками по 480 → 0, потом кадр */ |
|
n = speex_aec_process_capture(a, in, 480, out); |
|
CHECK(n == 0, "capture 480 should accumulate (returned %d)", n); |
|
n = speex_aec_process_capture(a, in + 480, 480, out); |
|
CHECK(n == FS, "capture 960 should emit frame (returned %d)", n); |
|
|
|
/* докормить рендер до глубины 3 */ |
|
speex_aec_feed_playback(a, pcm, FS); |
|
speex_aec_feed_playback(a, pcm, FS); |
|
CHECK(speex_aec_delay_fill(a) == 3, "fill=%d (expect 3)", speex_aec_delay_fill(a)); |
|
|
|
/* обработка кадра с полной линией уменьшает её на 1 */ |
|
n = speex_aec_process_capture(a, in, FS, out); |
|
CHECK(n == FS, "process_capture returned %d", n); |
|
CHECK(speex_aec_delay_fill(a) == 2, "fill=%d after capture (expect 2)", speex_aec_delay_fill(a)); |
|
|
|
speex_aec_reset(a); |
|
CHECK(speex_aec_delay_fill(a) == 0, "fill=%d after reset (expect 0)", speex_aec_delay_fill(a)); |
|
|
|
speex_aec_destroy(a); |
|
return 0; |
|
} |
|
|
|
static int test_echo_suppression(void) { |
|
speex_aec_t* a; |
|
int16_t* render_hist; |
|
int16_t speech[FS], cap[FS], out[FS]; |
|
int k, i, n; |
|
double echo_pow = 0.0, resid_pow = 0.0; |
|
double dt_speech_pow = 0.0, dt_out_pow = 0.0, dt_cross = 0.0; |
|
int measured = 0, dt_measured = 0; |
|
|
|
printf("[speex_aec] echo suppression (far-end noise, near-end tone)\n"); |
|
|
|
render_hist = (int16_t*)u_malloc((uint32_t)(N_FRAMES * FS) * sizeof(int16_t)); |
|
CHECK(render_hist != NULL, "OOM render_hist"); |
|
if (!render_hist) return 1; |
|
for (k = 0; k < N_FRAMES; k++) |
|
for (i = 0; i < FS; i++) render_hist[k * FS + i] = aec_noise(); |
|
|
|
a = speex_aec_create(RATE, FS, FILTER, DELAY_FRAMES); |
|
CHECK(a != NULL, "create failed"); |
|
if (!a) { u_free(render_hist); return 1; } |
|
|
|
for (k = 0; k < N_FRAMES; k++) { |
|
for (i = 0; i < FS; i++) { |
|
double t = (double)(k * FS + i) / RATE; |
|
speech[i] = (int16_t)(4000.0 * sin(2.0 * M_PI * 440.0 * t)); |
|
} |
|
|
|
/* фазы: [0,ADAPT) — только эхо (адаптация); [DT_START,MEAS_START) — речь+эхо; |
|
* [MEAS_START,N) — только эхо (замер чистого подавления). */ |
|
int double_talk = (k >= 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; |
|
}
|
|
|