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.
 
 
 
 
 
 

169 lines
6.7 KiB

// 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 <string.h>
#include <speex/speex_echo.h>
#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;
}