SPEEXDSP / AEC
Acoustic Echo Cancellation

让扬声器的声音,不再回到通话里

这是一篇从声学模型出发、一路走进 libspeexdsp/mdf.c 的实现导读:不仅讲 Speex 做了什么,也讲它为什么这样做,以及每个工程选择解决了哪类真实问题。

MDF / AUMDF 频域 NLMS 连续可变学习率 TWO_PATH 残余回声估计
目标:保留近端语音,减去扬声器回声
01 / WHAT IS AEC

回声消除不是“把声音调小”,而是在估计一条看不见的声学路径

远端声音从扬声器播放,经机壳、桌面、墙壁和空气传播后进入麦克风。AEC 已知扬声器参考信号 x(n),要在线估计房间冲激响应 h(n),再从麦克风中减去估计回声。

麦克风里实际有什么?

d(n) = s(n) + h(n) * x(n) + v(n)近端语音 + 扬声器经声学路径产生的回声 + 噪声

h(n) 不是常数:移动手机、开门、改变音量,都会让路径发生变化。因此 AEC 必须持续适应,而不能只在出厂时测一次。

AEC 输出什么?

e(n) = d(n) − ŷ(n)
ŷ(n) = ŵ(n) * x(n)用自适应滤波器 ŵ 生成估计回声 ŷ,再做相减

理想情况下 ŵ ≈ h,于是回声被抵消,输出主要剩下近端语音和背景噪声。

INTERACTIVE SIGNAL VIEW

观察三种通话场景中的参考、麦克风与输出

扬声器参考 x(n) 麦克风 d(n) AEC 输出 e(n)
当前场景仅远端讲话
滤波器状态稳定跟踪
输出重点回声接近静音

难点不在“相减”,而在如何得到一个可信的 ŷ(n)

01

回声尾很长

房间反射可持续数百毫秒。时域逐抽头卷积和更新会消耗大量乘加。

02

双讲会污染学习

近端讲话时,误差不再只代表回声模型错误;若把近端语音当梯度,滤波器会学偏。

03

语音频谱不均匀

低频能量强、高频能量弱,固定步长会让强频带过冲、弱频带收敛太慢。

04

路径随时会变化

设备移动或扬声器音量变化后,既要快速重新收敛,又不能把瞬时噪声发布到输出。

05

循环卷积会混叠

FFT 天然计算循环卷积;不加约束就会产生跨块绕回的伪影,破坏线性声学模型。

06

真实设备会削波

输入饱和、非线性失真、时钟漂移和参考未对齐都可能让线性自适应算法发散。

02 / PRINCIPLE

算法本质:把 AEC 当作在线“系统辨识”

滤波器不是在记一段声音,而是在逼近“扬声器到麦克风”的传递函数。每一帧都用误差修正权重:远端参考与误差越相关,越说明当前声学模型还有可学习的偏差。

从 LMS 到 NLMS

LMS 沿 x·e 的方向更新权重;NLMS 再除以输入能量,避免远端信号忽大忽小时步长也跟着失控。

w ← w + μ · e · x / (‖x‖² + ε)μ 决定“相信这次误差多少”,ε 防止静音时除零。

双讲为什么危险?

仅远端讲话时,误差主要来自模型不准;双讲时,误差里大量是近端语音。算法必须判断哪些误差像“残余回声”,哪些更像“新出现的近端内容”。

Speex 的关键选择:不依赖一个硬性的“双讲 / 非双讲”开关,而是连续调节每个频点的学习率。
03 / MDF & AUMDF

把一条很长的时域滤波器,拆成许多短块放到频域里算

Speex 使用 Multidelay Block Frequency Domain Adaptive Filter。设帧长为 B,内部 FFT 长度固定为 N = 2B,滤波器尾长 L 被拆成 M = ceil(L/B) 个块。

一条长而逐渐衰减的房间冲激响应h[0 … L−1]
直达声和早期反射通常集中在前部,后面是逐步衰减的混响尾。尾长越长,普通时域滤波器每个采样点要做的工作越多。
TIME DOMAIN
每采样点 ≈ O(L)

滤波器越长,卷积和梯度更新的乘加数量近似线性增长。

PARTITIONED FREQUENCY DOMAIN
短 FFT + M 个频域块

单次 FFT 长度保持为 2B;增加尾长主要增加分块数,适合在可控帧延迟下处理长回声路径。

为什么还要做“约束”?

FFT 乘法给出循环卷积,而真实声学路径是线性卷积。Speex 把权重 IFFT 回时域,清零不该存在的半段,再 FFT 回去,相当于把权重投影回合法的线性卷积支撑区间。

AUMDF 如何省计算?

标准 MDF 可每帧约束所有块;Speex 的 AUMDF 变体每帧始终约束第 0 块,再轮转约束一个其他块。关键的前部块最及时,其他块摊到多帧完成。

04 / SPEEX SOURCE

speex_echo_cancellation() 一帧里到底做了什么

主流程不是单一公式,而是一条带保护、统计和发布策略的流水线。背景滤波器负责持续学习,前景滤波器负责稳定输出;帧尾计算出的 power_1 和误差频谱会供下一帧开头更新权重。

01 / CONDITION

DC notch + 预加重

清除直流漂移,压低语音低频主导,让频带学习更均衡;输出端再去加重。

02 / BUFFER

参考移位与 FFT

维护 M+1 个远端频域帧,当前帧进入 X[0],历史依次后移。

03 / FOREGROUND

计算稳定路径

前景权重生成实际候选输出,并得到残差能量 Sff。

04 / ADAPT

更新背景 W

power_1 × prop[j] × X*E 形成频域梯度;饱和时冻结。

05 / CONSTRAIN

AUMDF 约束

第 0 块每帧处理,其他块轮转,抑制循环卷积绕回。

06 / COMPARE

计算背景残差

得到 See 和两滤波器输出差 Dbf,进入显著性判决。

07 / PUBLISH

晋升或回退

背景显著更好则复制到前景;显著变坏则用前景覆盖背景。

08 / LEARN NEXT

估 leak 与新学习率

统计 Rf/Yf/Xf、计算 RER 与逐频点 power_1,供下一帧使用。

VARIABLE LEARNING RATE

双讲不是“硬冻结”,而是连续降低可疑频带的学习率

Speex 观察误差谱变化 Eh 是否跟随估计回声谱变化 Yh,用递归线性回归得到泄漏估计:

η ≈ Pey / Pyyη 大:误差中仍有较多“像回声”的成分;η 小:误差更可能来自近端语音或噪声。

随后把 η·|Ŷ(k)|² 与误差谱、远端功率谱结合,形成每个频点的更新权重 power_1(k)。

0.42
18%
不同频点的相对学习率示意图横轴为频率,纵轴为相对学习率。泄漏估计增大时学习率提高,近端干扰增强时学习率降低。00.330.661.0低频高频
当前误差较像残余回声,且近端干扰较弱:允许背景滤波器以中等速度继续学习。
TWO_PATH FILTER

一套权重探索,一套权重负责出声

前景 foreground

稳定输出

默认被用户听到。只有背景在统计上显著更好时才更新。

残差能量 Sff:62
⇄显著性判决
Sff / See / Dbf

背景 W

持续学习

每帧按频域梯度更新,允许更积极地追踪新路径。

残差能量 See:82
启动:前景负责稳定输出,背景从零开始学习。背景即使暂时很差,也不会立刻污染通话。

按块比例 prop[j]

初始化时前块学习快、后块学习慢,前后约一个数量级;基本收敛后,再按每块权重能量的平方根动态分配,并加下限避免弱块完全停学。

解决:真实房间冲激通常“前重后轻”,所有块同速会浪费适应能力并放大尾部噪声。

启动阶段与稳态阶段

尚未适应时,Speex 使用受限的临时学习率尽快建立基本模型;满足累计适应量和泄漏条件后,切换到基于 η、RER、逐频点功率的稳态学习率。

解决:冷启动需要快,但稳态和双讲阶段需要谨慎;单一固定步长无法同时满足。
AUMDF SCHEDULE

第 0 块每帧约束,其他块轮转处理

if (j == 0 || cancel_count % (M−1) == j−1)概念示意:这里用 M=8 展示轮转。第 0 块代表最靠前、通常最重要的声学路径部分。

第 1 帧:约束 W₀ 和 W₁。前部块每帧都保持合法,其余成本被分摊。

05 / WHY THIS DESIGN

每个“复杂机制”背后,都是一个普通公式解决不了的工程问题

Speex AEC 的价值不只在 MDF。本质上,它把算力、收敛速度、双讲鲁棒性、输出稳定性和定点实现放在同一套权衡中。

现实问题Speex 的实现选择直接收益
长回声尾导致时域计算过重MDF 分块 + 长度 2B 的短 FFT在 10–20 ms 帧处理下扩展到数百毫秒尾长,尾长主要通过块数 M 扩展。
FFT 循环卷积绕回MDF 支撑约束 + AUMDF 交替调度维持线性卷积模型,同时把全块约束的成本分摊到多帧。
语音频带能量差异大逐频点平滑功率 power[k] 与归一化强频带不过冲,弱频带仍能学习,提升收敛稳定性。
回声路径能量前重后轻分块比例 prop[j]把更多更新能力给直达声和早反射,又不让混响尾完全停止跟踪。
双讲时误差被近端语音污染leak 回归 + RER + 连续可变学习率避免硬双讲开关的误判和跳变;可疑时渐进减小更新,而不是非黑即白。
持续学习可能短暂学坏前景 / 背景 TWO_PATH + 显著性晋升 / 回退把“探索”与“发布”分开,背景异常不会立即进入用户听到的输出。
切换滤波器可能产生咔嗒声Hann 窗平滑交叉过渡前景晋升时减少块边界和输出切换伪影。
削波和数值异常会产生错误梯度饱和冻结、能量 sanity check、自动 reset避免异常帧持续污染权重;发散时能回到可恢复状态。
线性 AEC 后仍会有残余回声speex_echo_get_residual() 输出残余谱给 preprocess让后置抑制器按频带继续压低剩余回声,形成“线性消除 + 非线性后处理”链路。
一句话理解: MDF/AUMDF 负责“算得动”,归一化与可变学习率负责“别学错”,TWO_PATH 负责“学错也别马上播出去”,保护与残余抑制负责“真实设备上还能活下来”。
06 / INTEGRATION

应用层接入很短,但效果取决于你是否把参考信号送对了

最重要的输入不是“随便一段远端音频”,而是实际送往扬声器、并与麦克风帧时间对齐的参考。AEC 不能替应用层弥补无限的固定延迟、持续漂移或严重非线性。

int sample_rate = 16000;
int frame_size = sample_rate / 100; // 10 ms
int filter_length = sample_rate * 300 / 1000;

SpeexEchoState *echo =
    speex_echo_state_init(frame_size, filter_length);

speex_echo_ctl(echo,
    SPEEX_ECHO_SET_SAMPLING_RATE, &sample_rate);

SpeexPreprocessState *den =
    speex_preprocess_state_init(frame_size, sample_rate);

speex_preprocess_ctl(den,
    SPEEX_PREPROCESS_SET_ECHO_STATE, echo);

while (running) {
    // mic: 麦克风帧;far: 实际送往扬声器的对齐参考帧
    speex_echo_cancellation(echo, mic, far, out);
    speex_preprocess_run(den, out);
}
frame_sizeAPI 注释建议每帧约 10–20 ms。短帧延迟低,但调度和 FFT 次数更多。
filter_lengthAPI 注释通常建议 100–500 ms。应覆盖固定声学延迟和有效混响尾,但不是越长越好。
rec / play / outrec 是近端麦克风混合信号,play 是送扬声器的远端参考,out 是线性 AEC 输出。
残余回声抑制把同一个 echo state 交给 preprocess,后处理器可调用残余谱估计继续抑制线性 AEC 没消干净的部分。

四个最常见的效果上限

Δt参考未对齐

若真实延迟超出滤波器覆盖范围,算法看不到正确对应关系;先做延迟测量与缓存对齐。

↯扬声器非线性

削波、功放压缩和机壳共振会产生参考中不存在的谐波,线性滤波器无法精确建模。

≠Hz采集 / 播放时钟漂移

两端采样时钟不同会让对齐缓慢滑动,需要时钟漂移补偿或重采样策略。

M×尾长盲目拉满

更长的 filter_length 增加块数、内存与每帧频域乘法;应按实际尾长留适量余量。

源码阅读地图

SpeexEchoState_所有缓冲、统计量、前景 / 背景权重和学习率状态。
speex_echo_state_init_mc计算 N、M,分配内存,初始化窗、prop、预加重和 notch 参数。
weighted_spectral_mul_conj计算带 power_1 与 prop[j] 的共轭频谱梯度。
mdf_adjust_prop按各 MDF 块的权重能量动态分配适应比例。
speex_echo_cancellation单帧主流程:调理、FFT 延迟线、更新、约束、双路径和 leak 估计。
speex_echo_get_residual对估计回声加 Hann 窗、取功率谱,再按 leak 缩放供后处理器使用。
FINAL MODEL

Speex AEC 是一套“持续学习,但谨慎发布”的长回声路径估计器

它用 MDF 把长滤波器搬到频域,用 AUMDF 控制约束成本,用逐频点归一化和泄漏驱动的学习率对抗彩色语音与双讲,再用双路径把激进学习和稳定输出隔离。最终,残余回声谱还能交给 preprocess 继续处理。

对齐参考DC / 预加重MDF 分块频域 NLMSAUMDF 约束leak + RERTWO_PATH残余抑制