// 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 без канселлера. // // Рендер принимает чанки любого размера; захват — ровно один полный кадр. // Частые аномалии учитываются счётчиками; владелец публикует сводку вне audio callback. #include "speex_aec.h" #include "debug_config.h" #include "mem.h" #include #include #include #define AEC_ID "speex_aec" struct speex_aec { SpeexEchoState* st; int mic_channels, render_channels; 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; 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++; } memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples * a->render_channels, frame, (size_t)a->frame_samples * a->render_channels * sizeof(int16_t)); a->count++; } speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) { return speex_aec_create_mc(sample_rate, frame_samples, filter_samples, delay_frames, 1, 1); } /* Независимые каналы референса: противофазное стерео не усредняется. */ speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames, int mic_channels, int render_channels) { speex_aec_t* a; int rate; if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples || mic_channels < 1 || mic_channels > 2 || render_channels < 1 || render_channels > 2 || frame_samples > INT_MAX / render_channels || frame_samples > INT_MAX / mic_channels || (delay_frames > 0 && delay_frames > INT_MAX / frame_samples / render_channels)) { 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_mc(frame_samples, filter_samples, mic_channels, render_channels); if (!a->st) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: echo state allocation failed", AEC_ID); u_free(a); return NULL; } 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->mic_channels = mic_channels; a->render_channels = render_channels; 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 * render_channels), sizeof(int16_t)); a->play_acc = (int16_t*)u_calloc((uint32_t)(frame_samples * render_channels), sizeof(int16_t)); if (!a->line || !a->play_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); 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 mic=%d render=%d", AEC_ID, sample_rate, frame_samples, filter_samples, a->depth, mic_channels, render_channels); 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); } 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; 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 * a->render_channels; while (count > 0) { take = fs - a->play_len; if (take > count) take = count; memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t)); a->play_len += take; pcm += take; count -= take; if (a->play_len == fs) { 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; if (!a || !pcm || !out || count <= 0) return 0; fs = a->frame_samples; if (count != fs * a->mic_channels) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: capture requires one frame samples=%d expected=%d", AEC_ID, count, fs * a->mic_channels); return 0; } if (a->count == a->depth) { const int16_t* ref = a->line + (size_t)a->head * fs * a->render_channels; speex_echo_cancellation(a->st, pcm, ref, out); a->head = (a->head + 1) % a->depth; a->count--; } else { /* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */ memcpy(out, pcm, (size_t)count * sizeof(int16_t)); a->underruns++; } return count; } int speex_aec_delay_fill(const speex_aec_t* a) { return a ? a->count : 0; } void speex_aec_get_stats(const speex_aec_t* a, uint32_t* overruns, uint32_t* underruns) { *overruns = a ? a->overruns : 0; *underruns = a ? a->underruns : 0; }