170 lines
6.7 KiB
C
170 lines
6.7 KiB
C
// 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;
|
||
}
|