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

/* Тест обёртки эхоподавления 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;
}