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