// speex_aec.c — C-обёртка над SpeexDSP mdf.c (акустическое эхоподавление). // // API см. в speex_aec.h. Ключевые детали реализации: // - собственная линия задержки рендера на depth кадров (SpeexDSP даёт только // фиксированные 2 кадра — PLAYBACK_DELAY в mdf.c); // - синхронный speex_echo_cancellation(rec, play_delayed, out) с выравниванием // рендер↔захват по линии задержки; // - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр, // недозаполнение (старт/underflow) → passthrough без канселлера. // // Захват и рендер приходят чанками разного размера (480/960), поэтому обе // стороны накапливаются до полного кадра (frame_samples) внутри обёртки. #include "speex_aec.h" #include "debug_config.h" #include "mem.h" #include #include #define AEC_ID "speex_aec" struct speex_aec { SpeexEchoState* st; int frame_samples; /* сэмплов в кадре (960 @48кГц) */ int depth; /* глубина линии задержки в кадрах (>=1) */ int16_t* line; /* кольцо depth*frame_samples */ int head; /* индекс самого старого кадра в line */ int count; /* кадров в линии (0..depth) */ int16_t* play_acc; /* аккумулятор рендера до кадра */ int play_len; int16_t* cap_acc; /* аккумулятор захвата до кадра */ int cap_len; uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */ uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */ }; /* Положить полный рендер-кадр в линию задержки (вытесняя старейший при переполнении). */ static void aec_enqueue(speex_aec_t* a, const int16_t* frame) { if (a->count == a->depth) { a->head = (a->head + 1) % a->depth; a->count--; a->overruns++; if (a->overruns == 1 || (a->overruns & 0xff) == 0) { DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: delay line overflow (render faster than capture) overruns=%u", AEC_ID, a->overruns); } } memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples, frame, (size_t)a->frame_samples * sizeof(int16_t)); a->count++; } speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) { speex_aec_t* a; int rate; if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d", AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames); return NULL; } a = (speex_aec_t*)u_calloc(1, sizeof(*a)); if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for state", AEC_ID); return NULL; } a->st = speex_echo_state_init(frame_samples, filter_samples); rate = sample_rate; if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate); speex_echo_state_destroy(a->st); u_free(a); return NULL; } a->frame_samples = frame_samples; a->depth = delay_frames > 0 ? delay_frames : 1; a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples), sizeof(int16_t)); a->play_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); a->cap_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); if (!a->line || !a->play_acc || !a->cap_acc) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID); if (a->line) u_free(a->line); if (a->play_acc) u_free(a->play_acc); if (a->cap_acc) u_free(a->cap_acc); speex_echo_state_destroy(a->st); u_free(a); return NULL; } DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: created rate=%d frame=%d filter=%d delay=%d frames", AEC_ID, sample_rate, frame_samples, filter_samples, a->depth); return a; } void speex_aec_destroy(speex_aec_t* a) { if (!a) return; speex_echo_state_destroy(a->st); u_free(a->line); u_free(a->play_acc); u_free(a->cap_acc); u_free(a); } void speex_aec_reset(speex_aec_t* a) { if (!a) return; speex_echo_state_reset(a->st); a->head = 0; a->count = 0; a->play_len = 0; a->cap_len = 0; DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID); } void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) { int fs, take; if (!a || !pcm || count <= 0) return; fs = a->frame_samples; take = count; if (a->play_len + count > fs) take = fs - a->play_len; memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t)); a->play_len += take; if (a->play_len < fs) return; aec_enqueue(a, a->play_acc); a->play_len = 0; } int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) { int fs, take; if (!a || !pcm || !out || count <= 0) return 0; fs = a->frame_samples; take = count; if (a->cap_len + count > fs) take = fs - a->cap_len; memcpy(a->cap_acc + a->cap_len, pcm, (size_t)take * sizeof(int16_t)); a->cap_len += take; if (a->cap_len < fs) return 0; if (a->count == a->depth) { const int16_t* ref = a->line + (size_t)a->head * fs; speex_echo_cancellation(a->st, a->cap_acc, ref, out); a->head = (a->head + 1) % a->depth; a->count--; } else { /* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */ if (a->underruns == 0 || (a->underruns & 0xff) == 0) { DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: delay line underflow (render slower than capture) underruns=%u fill=%d/%d", AEC_ID, a->underruns, a->count, a->depth); } memcpy(out, a->cap_acc, (size_t)fs * sizeof(int16_t)); a->underruns++; } a->cap_len = 0; return fs; } int speex_aec_delay_fill(const speex_aec_t* a) { return a ? a->count : 0; }