Files
utun2/lib/speex_aec.c
T

170 lines
6.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}