Overview项目概述
The robot-side audio stack for a robot-to-operator intercom: a multichannel frequency-domain Kalman adaptive filter with Hammerstein saturation-basis modeling of loudspeaker nonlinearity, decision-directed Wiener residual suppression, minimum-statistics noise reduction, and low-latency UDP transport — about 3,700 lines of Python on a Rockchip RK3588S board, plus the 15-scene simulator and calibration toolchain built to prove it works.机器人端的音频软件栈,服务于机器人与操作员之间的对讲:多通道频域卡尔曼自适应滤波器、用 Hammerstein 饱和基建模扬声器非线性、判决引导的维纳残余抑制、基于最小值统计的降噪,以及低延迟 UDP 传输 —— 约 3,700 行 Python,运行在瑞芯微 RK3588S 开发板上,外加为验证它而专门搭建的 15 场景仿真器与标定工具链。
The Constraint约束条件
A robot needs to talk and listen at the same time. Its speaker sits roughly 15 cm from its microphone, so without echo cancellation the operator hears their own voice coming back.机器人需要同时说话和聆听。它的扬声器距麦克风只有约 15 cm,因此如果不做回声消除,操作员会听到自己的声音被送回来。
The obvious fix — mute the microphone while the far end is talking — turns the link half-duplex, which is unusable for a remote operator. It also posts a beautiful score on the standard metric, and avoiding that trap is where most of this project’s measurement effort went.最直观的办法——远端讲话时把麦克风静音——会把链路变成半双工,这对远程操作员来说根本没法用。而它偏偏能在业界标准指标上拿到一个漂亮的分数;本项目在测量上投入的大部分精力,就是为了绕开这个陷阱。
I built the robot-side (B-end) audio stack: adaptive echo cancellation, residual suppression, steady-state noise reduction, comfort noise, and low-latency UDP transport — about 3,700 lines of Python across 16 modules on a LubanCat-4 (Rockchip RK3588S), with numpy and scipy only. The harder half of the work was not the filter. It was the measurement infrastructure that can tell real echo cancellation apart from a fancy noise gate, and the physical calibration that says when the answer is “move the speaker,” not “tune a parameter.”我负责机器人端(B 端)的整套音频栈:自适应回声消除、残余抑制、稳态降噪、舒适噪声,以及低延迟 UDP 传输 —— 在 LubanCat-4(瑞芯微 RK3588S)上共约 3,700 行 Python、16 个模块,只依赖 numpy 与 scipy。这项工作更难的那一半并不是滤波器本身,而是能把真正的回声消除与"花哨的噪声门"区分开的测量设施,以及能告诉你"答案是把扬声器挪开"而不是"再调一个参数"的物理标定。
The Physics Sets the Budget物理条件决定了预算上限
Before any algorithm work, a calibration pass measures what the hardware actually makes possible. The algorithm operates on top of a physical budget it cannot exceed:在动手做算法之前,先用一轮标定测清楚硬件到底允许你做到什么程度。算法只能在一个它无法逾越的物理预算之上工作:
- Echo return loss is the free suppression that geometry gives you. On this chassis the budget was roughly 14–17 dB (speaker–microphone ≈ 15–20 cm, target talker at 1 m). Everything the algorithm does is on top of that number.回声返回衰减(ERL)是几何结构白送给你的抑制量。在这个机身上该预算大约是 14~17 dB(扬声器与麦克风相距约 15~20 cm,目标说话人在 1 m 处)。算法所做的一切都是叠加在这个数字之上的。
- Loudspeaker saturation caps what a linear filter can cancel at about −20·log₁₀(distortion). At 8% effective distortion that ceiling is 21 dB, full stop.扬声器饱和把线性滤波器能消掉的部分限制在约 −20·log₁₀(失真) 处。在 8% 有效失真下,这个天花板就是 21 dB,没有余地。
- Magnitude-squared coherence gives a second, independent read on the same ceiling. It is documented as a lower bound, because when the analysis window is shorter than the impulse response it under-reads badly — a true 29 dB ceiling read as 3 dB through a 170 ms window.幅度平方相干函数给出对同一天花板的第二个独立读数。它在文档里被明确标注为下界,因为当分析窗短于冲激响应时它会严重低估 —— 真实 29 dB 的天花板,用 170 ms 的窗读出来只有 3 dB。
- Distortion has to be measured with speech-like material, not a sine. Sine has 3 dB crest factor and speech has 13 dB, and distortion is a peak phenomenon, so sine-based THD under-reads. Farina swept-sine is the only trustworthy method here, because it separates the harmonic impulse responses in time.失真必须用类语音信号测,不能用正弦。正弦的峰值因数是 3 dB,语音是 13 dB,而失真是一种峰值现象,所以基于正弦的 THD 会明显低估。Farina 指数扫频是这里唯一可信的方法,因为它把各次谐波的冲激响应在时间上分开了。
| Measurement | Decision it drives |
|---|---|
| Echo return loss (ERL) | Below 5 dB: go fix the mechanical layout, do not tune algorithms |
| Echo delay | Whether the partition count covers the reverberation tail |
| Effective distortion, speech-referred | The physical ceiling for linear AEC; selects the nonlinear basis |
| Coherence ceiling, lower bound | Cross-check on the above; both low means a hardware problem |
| Clock drift (ppm) | Single-card should read ≈ 0; nonzero means something is resampling |
| xrun count | Nonzero must be fixed first; everything downstream is invalid |
| 测量项 | 它所决定的事 |
|---|---|
| 回声返回衰减(ERL) | 低于 5 dB:先去修机械布局,不要调算法参数 |
| 回声时延 | 分区数是否能覆盖混响拖尾 |
| 语音口径的有效失真 | 线性 AEC 的物理天花板;决定非线性基的选择 |
| 相干天花板(下界) | 对上一项的交叉校验;两者都低则是硬件问题 |
| 时钟漂移(ppm) | 单声卡应为 ≈ 0;非零说明某处在重采样 |
| xrun 计数 | 非零必须先修;否则下游所有结果都不作数 |
Architecture系统架构
Two endpoints over UDP port 47000 on a LAN. The A-end is a PC with a closed-back headset and runs no echo cancellation at all — the headset makes speaker-to-microphone coupling negligible, and that is a stated architectural decision rather than an omission. The B-end robot runs the entire chain.局域网上两个端点,走 UDP 47000 端口。A 端是一台配封闭式耳机的 PC,完全不跑回声消除 —— 耳机让扬声器到麦克风的耦合可以忽略,这是一个被明确写下来的架构决策,而不是遗漏。B 端机器人跑完整的处理链。
- Transport is raw 48 kHz mono 16-bit PCM at 768 kbps, with no codec. No codec lookahead latency (Opus costs ≥ 20 ms), no VAD or DTX — speech codecs stop transmitting during pauses and substitute comfort noise, which destroys reference continuity and mutes the robot’s actual environment — and no AGC, noise suppression, or limiting, because any compression breaks the premise that the echo is a linear function of the reference.传输是 48 kHz 单声道 16 位裸 PCM,768 kbps,不用任何编解码器。没有编解码器的前瞻延迟(Opus 要 ≥ 20 ms);没有 VAD/DTX —— 语音编解码器会在停顿时停止发送并填入舒适噪声,这既破坏参考信号的连续性,也把机器人所在环境的真实声音掩掉了;也没有 AGC、降噪或限幅,因为任何压缩都会破坏"回声是参考信号的线性函数"这个整套系统赖以成立的前提。
- The jitter buffer has fixed depth and drops rather than letting latency grow, because in a conversation latency never comes back down and people notice a 150 ms round trip.抖动缓冲区采用固定深度,宁可丢包也不让延迟增长 —— 因为在对话里延迟一旦涨上去就再也降不回来,而人对 150 ms 的往返时延是有感知的。
- Packet loss is concealed with silence, not extrapolation. Extrapolated samples would be treated as a true reference and would poison the filter.丢包用静音填补,而不是外推。外推出来的采样会被当成真实的参考信号,从而污染滤波器。
- Sequence numbers give loss and reorder counts — the first line of evidence when someone says the intercom sounds choppy. Late packets are dropped rather than reordered.序列号提供丢包与乱序计数 —— 当有人说"对讲卡顿"时,这是第一手证据。迟到的包直接丢弃,不做重排。
The Single-Stream Decision单流设计:最关键的一个决定
Playback and capture happen in one callback of one duplex stream, on one sound card, and the reference signal is the exact array handed to the codec in that same callback. Three things then fall out for free:播放与采集发生在同一块声卡、同一个双工流的同一个回调里,而参考信号就是在那个回调中交给编解码芯片的那个数组本身。于是有三件事白拿:
- One card means one crystal, which means zero clock drift. The pipeline contains no drift compensation at all because none is needed; an earlier two-device configuration had cost 8–9 dB of cancellation.一块声卡就是一个晶振,也就意味着零时钟漂移。整条流水线里没有任何漂移补偿代码,因为根本不需要;而早先的双设备配置曾让消除量损失 8~9 dB。
- The microphone and reference samples are hardware-aligned. Two separate streams — one input, one output — would have independent start times and buffer states, giving an unknown and drifting offset. Using the same card is not sufficient; it has to be one duplex stream.麦克风采样与参考采样在硬件层面就是对齐的。两个独立的流——一个输入、一个输出——会有各自的启动时刻和缓冲状态,从而带来一个未知且持续漂移的偏移。用同一块声卡还不够,必须是同一个双工流。
- Everything network-related happens upstream of the reference tap, so jitter buffering, packet-loss concealment, and buffer stretching cannot break the correspondence between microphone and reference.所有与网络相关的处理都发生在参考信号抽取点的上游,因此抖动缓冲、丢包补偿和缓冲拉伸都不可能破坏麦克风与参考之间的对应关系。
- Three threads, because two separate incidents proved each boundary necessary. The audio callback does nothing but read the far end, write output, pick the microphone channel, and enqueue — the AEC alone was ~2.8 ms of a 5.33 ms block budget. A dedicated sender thread does nothing but sendto(), which is a blocking syscall; blocking inside the worker backed up the processing queue and dropped blocks that were never counted as xruns.三个线程,因为有两次独立的事故分别证明了每条边界都是必要的。音频回调只做四件事:读远端、写输出、挑麦克风通道、入队 —— 仅 AEC 本身就占掉 5.33 ms 分块预算里的约 2.8 ms。另有一个专门的发送线程只调用 sendto(),这是一个会阻塞的系统调用;把它放在工作线程里阻塞,会让处理队列堆积并丢块,而这些丢块从来不被计为 xrun。
Why a Kalman Filter为什么选卡尔曼滤波器
The filter is a partitioned block frequency-domain adaptive filter using overlap-save, with K partitions × P Hammerstein branches, driven by a state-space model in which process noise represents echo-path variation and observation noise represents near-end speech plus the noise floor.滤波器是采用 overlap-save 的分块频域自适应滤波器(PBFDAF),有 K 个分区 × P 个 Hammerstein 分支,由一个状态空间模型驱动:过程噪声代表回声路径的变化,观测噪声代表近端语音加底噪。
- The Kalman gain is the optimal trade-off between those two noise terms, which is exactly a per-frequency-bin, per-partition adaptive step size.卡尔曼增益是这两个噪声项之间的最优权衡,而它恰好就是一个"逐频点、逐分区"的自适应步长。
- That makes double-talk protection intrinsic to the model. When the near end speaks, the observation-noise PSD rises and the gain contracts automatically — there is no external double-talk detector to false-trigger.这让双讲保护内生于模型之中。近端一开口,观测噪声功率谱上升,增益就自动收缩 —— 不存在一个会误触发的外置双讲检测器。
- Observation noise must subtract explained variance. Using the residual power directly as near-end power creates a death spiral: before convergence the residual is mostly echo, so the filter concludes the near end is talking, the gain collapses, and it never converges.观测噪声必须减掉已被解释的方差。直接把残差功率当成近端功率会造成死亡螺旋:收敛前残差里主要是回声,于是滤波器判断近端在说话,增益随之崩塌,从此再也收敛不了。
- State uncertainty can never exceed the prior. In the multichannel case a weakly-excited branch receives almost no observation, so process noise pushes its covariance to the ceiling; that branch then turns hyper-aggressive and absorbs gradient noise. I watched a nonlinear branch hold over 90% of filter energy while overall cancellation went negative. The fix is to clip the covariance at the prior.状态不确定度永远不能超过先验。在多通道情形下,一个激励很弱的分支几乎收不到观测,于是过程噪声把它的协方差一路推到上限;这个分支随后变得极度激进,开始吸收梯度噪声。我亲眼看到一个非线性分支占据了 90% 以上的滤波器能量,而整体消除量却变成了负数。解决办法是把协方差夹在先验处。
- Gradient projection cannot be skipped but can be amortised. Unconstrained overlap-save updates grow a non-causal tail, and full constraint costs an FFT pair per partition, so the code rotates through a few partitions per block and covers all of them over K blocks.梯度投影不能省,但可以摊开。无约束的 overlap-save 更新会长出一条非因果尾巴,而完整约束的代价是每个分区一对 FFT,所以代码每块只轮转处理几个分区,在 K 块之内覆盖全部分区。
- A shadow filter that distinguishes an abrupt path change from double-talk is implemented, validated, and shipped disabled. It works — zero false triggers across every double-talk scene — but it buys +0.4 dB for +40% CPU, so it ships off with the measurement written down next to it.一个用于区分"回声路径突变"与"双讲"的影子滤波器已经实现、验证并随包发布,但默认关闭。它确实有效 —— 在所有双讲场景上零误触发 —— 但它只换来 +0.4 dB,代价是 CPU +40%,所以它以关闭状态交付,旁边写着支撑这个决定的测量数据。
The Nonlinearity Story非线性问题的来龙去脉
Loudspeaker distortion is a memoryless nonlinearity cascaded with a linear room response. Expand the nonlinearity over fixed basis functions and the whole path becomes an equivalent multichannel linear system: each basis function is a virtual reference channel with its own linear filter, the expansion coefficients get absorbed into the filters, and harmonic distortion is cancelled at the linear filtering stage instead of being left for the residual suppressor to scrape off. That is what makes it possible to exceed the linear distortion ceiling at all.扬声器失真可以看成一个无记忆非线性级联一个线性房间响应。把这个非线性在一组固定基函数上展开,整条通路就等价成一个多通道线性系统:每个基函数都成为一路带有自己线性滤波器的虚拟参考通道,展开系数被吸收进滤波器里,而谐波失真在线性滤波这一级就被消掉了,不必留给残余抑制器去刮。这正是"能够突破线性失真天花板"的根本原因。
- The obvious basis is x, x², x³. On speech it fails catastrophically: I measured the block-RMS peak of x³ at 70,000× that of x. Crest factor explodes with order, and speech already carries 13 dB peak-to-average, so one loud syllable gets amplified a thousandfold. Scalar normalization cannot hold that.最直观的基是 x、x²、x³。在语音上它会灾难性地失效:我实测 x³ 的分块 RMS 峰值达到 x 的 70,000 倍。峰值因数随阶数爆炸增长,而语音本身就有 13 dB 的峰均比,于是一个响亮的音节会被放大上千倍。标量归一化压根压不住。
- Worse, the polynomial orders are highly collinear on speech — correlation-matrix condition numbers of 1e9 to 1e11, at which point Cholesky orthogonalization is numerically dead and amplifies noise by three orders of magnitude. The result was nonlinear branches absorbing 95% of filter energy while overall cancellation went negative.更糟的是,多项式各阶在语音上高度共线 —— 相关矩阵条件数达到 1e9 到 1e11,在这个量级上 Cholesky 正交化在数值上已经死了,反而会把噪声放大三个数量级。结果就是非线性分支吸走 95% 的滤波器能量,整体消除量变成负数。
- Better normalization and better orthogonalization were both tried, and both failed. The fix was not a numerical patch but a re-examination of the physical premise: speaker distortion is saturation, not polynomial.更好的归一化和更好的正交化都试过了,两条路都失败。真正的解决办法不是数值补丁,而是重新审视物理前提:扬声器失真的本质是饱和,不是多项式。
- Spanning the nonlinearity with saturation functions directly — σ·tanh(β·x/σ), with σ a slowly-varying reference RMS — gives a basis that is bounded by construction and keeps the same crest factor as x. Different β covers mild compression through near-hard clipping, and even-order asymmetry uses de-meaned tanh². Orthogonalization is then scalar Gram-Schmidt with slowly-varying coefficients, deliberately not Cholesky — sidestepping the conditioning problem at its root instead of fighting it.直接用饱和函数张成这个非线性 —— σ·tanh(β·x/σ),其中 σ 是缓变的参考信号 RMS —— 得到的基在构造上就是有界的,并且保持与 x 相同的峰值因数。不同的 β 覆盖从轻度压缩到近乎硬削波的范围,偶次不对称则用去均值的 tanh²。正交化随后采用系数缓变的标量 Gram-Schmidt,刻意不用 Cholesky —— 从根上绕开条件数问题,而不是硬着头皮跟它斗。
- The regression test for this uses a non-stationary signal on purpose. Polynomial bases fail on the syllabic dynamics of speech; stationary noise has constant per-block RMS and cannot reveal the bug at all.针对这一点的回归测试刻意使用非平稳信号。多项式基是在语音的音节动态上失效的;平稳噪声每块 RMS 恒定,根本暴露不出这个缺陷。
Measuring Honestly诚实地做测量
ERLE, the standard echo-cancellation metric, rewards muting the microphone. A gate that mutes whenever the far end talks posts spectacular ERLE and is useless, and for a while I was unknowingly tuning toward exactly that. The harness therefore reports four numbers instead of one:ERLE 这个业界标准的回声消除指标,奖励的是"把麦克风静音"。一个只要远端说话就静音的门电路,能刷出极其漂亮的 ERLE,却完全不可用;而我有一段时间正是在不知不觉地朝那个方向调参。因此评测框架报告的是四个数字,而不是一个:
- ERLE during far-end single-talk — conventional echo attenuation. Necessary, but not sufficient.远端单讲时的 ERLE —— 传统意义上的回声衰减。必要,但不充分。
- Near-end SDR during double-talk, which penalizes echo leakage and near-end damage in a single number. This is the real full-duplex metric.双讲时的近端 SDR,它用一个数字同时惩罚"回声泄漏"和"近端受损"。这才是真正的全双工指标。
- Noise attenuation and speech damage, separated by gain replay. These cannot be separated by segmenting the timeline, because during speech segments the noise is still present and both numbers come out contaminated. The suppressor is the same linear time-varying system for every component, so I log the final per-frame gain and replay that trajectory onto the ground-truth noise track and the ground-truth speech track separately. Two clean, mutually uncontaminated numbers.噪声衰减与语音损伤,用增益回放(gain replay)分离出来。这两个量无法靠切分时间轴来分离,因为在语音段里噪声依然存在,两个数字都会被互相污染。而抑制器对每个分量而言是同一个线性时变系统,所以我把最终的逐帧增益记录下来,再把这条增益轨迹分别回放到纯净噪声轨和纯净语音轨上。得到两个干净且互不污染的数字。
- Short-time curves plus dip depth and re-convergence time, because averages hide transients.除了均值,还报告短时曲线、谷底深度与重收敛时间,因为平均值会把瞬态藏起来。
- Far-end-silent segments are excluded from the dip statistic. When the far end stops, the microphone holds only noise floor while the output still carries comfort noise, and short-time ERLE plunges to −20 dB. That is not a cancellation failure — the denominator went away. Left in, it would have poisoned the worst-case number on every scene.远端静音段被排除在谷底统计之外。远端一停,麦克风里只剩底噪,而输出里还有舒适噪声,短时 ERLE 会直坠到 −20 dB。那不是消除失败 —— 是分母消失了。若不剔除,它会污染每个场景的最差值。
The Scene Suite场景集的设计
Fifteen scenes (A–O), each targeting one specific failure mode rather than just being “harder.” Room impulse response, loudspeaker nonlinearity, near-end speech, and background noise are independently switchable. Three details that took actual domain judgement:共 15 个场景(A~O),每一个都针对一种特定的失效模式,而不只是"更难"。房间冲激响应、扬声器非线性、近端语音与背景噪声可以独立开关。有三个细节真正需要领域判断:
- Fan noise, not white noise, for the noise-reduction test. Minimum statistics latches onto long-term spectral stationarity, and fan blade-passage harmonics get caught cleanly. White noise is also stationary but spectrally flat, so suppressing it changes neither the numbers nor the perception measurably — the test cannot tell you anything.降噪测试用风扇噪声,不用白噪声。最小值统计抓的是长时频谱平稳性,而风扇叶片通过频率的谐波会被干净地捕捉到。白噪声虽然也平稳,但频谱平坦,抑制它既不会明显改变数字也不会明显改变听感 —— 这样的测试什么也说明不了。
- Office transients are rendered as a separate channel from fan noise, because their correct handling is opposite. Fan noise is steady and should be suppressed; keyboard, chair scrape, and paper rustle are non-stationary live-room sound and should be kept, because an operator who hears nothing from the robot’s environment assumes the link died. Minimum statistics naturally suppresses one and not the other, which is behaviour to preserve rather than a limitation to fix.办公室瞬态被渲染成与风扇噪声分开的一路通道,因为二者的正确处理方式恰好相反。风扇噪声稳定,应当被抑制;而键盘声、椅子刮地声、翻纸声属于非平稳的现场声音,应当被保留 —— 因为一个从机器人那头什么都听不到的操作员,会以为链路断了。最小值统计天然会抑制前者而不抑制后者,这是需要保护的正确行为,而不是需要修掉的局限。
- Continuous path drift and abrupt path change are separate scenes, because they test opposite things. Abrupt change tests re-convergence speed, continuous drift tests steady-state tracking, and they pull the process-noise scale in opposite directions. Averaging them into one “time-varying” scene would have hidden the trade-off entirely.路径连续漂移与路径突变被拆成两个场景,因为它们检验的是相反的东西。突变考察重收敛速度,连续漂移考察稳态跟踪能力,而两者把过程噪声尺度往相反方向拉。把它们平均进一个"时变"场景,会把这个取舍彻底藏起来。
- Five of the six tools need no hardware at all, which is itself a workflow decision: iterate the algorithm on a PC, and go to hardware only for the things that require air.六个工具里有五个完全不需要硬件,这本身就是一个工作流决策:算法在 PC 上迭代,只有真正"需要空气"的事情才上硬件。
Results结果
Full 15-scene suite, 12 s per scene, delivered configuration (512-sample block, 24 partitions, linear basis). Reproduced by running the shipped code.完整 15 场景、每场景 12 秒、交付配置(512 采样分块、24 个分区、线性基)。以下数据由运行交付代码复现。
- Scene H is the interesting row: measured effective distortion of 9.15% implies a hard physical ceiling of 21 dB for a linear canceller, and the system reaches 33.5 dB. Exceeding that ceiling is the design intent — Hammerstein modeling cancels harmonics in the linear stage and the residual suppressor takes the rest.场景 H 是最有意思的一行:实测有效失真 9.15%,意味着线性消除器的硬物理天花板是 21 dB,而系统达到了 33.5 dB。突破这个天花板正是设计意图 —— Hammerstein 建模在线性级就把谐波消掉了,剩下的交给残余抑制器。
- Hardware-free numeric self-checks gate the build: WOLA perfect-reconstruction error at −312 dB against a −100 dB threshold, Kalman convergence of 102 dB on a synthetic three-tap path against a 40 dB threshold, and a basis crest factor of 3 against a fail-above-200 tripwire.无需硬件的数值自检为构建把关:WOLA 完美重构误差 −312 dB(阈值 −100 dB)、在合成三抽头路径上的卡尔曼收敛量 102 dB(阈值 40 dB)、基的峰值因数为 3(超过 200 即判失败)。
| Metric | Value |
|---|---|
| Average ERLE, far-end single-talk | 26.8 dB |
| Average near-end SDR, double-talk | −3.6 dB |
| Average noise attenuation | 15.3 dB |
| Worst short-time dip | −3.5 dB |
| Longest re-convergence | 3.50 s |
| CPU, median per block | 1.053 ms / 10.67 ms budget — 9.9% of one core |
| CPU, 99th percentile | 1.207 ms |
| Algorithmic latency | 21.3 ms |
| Filter tail coverage | 256 ms |
| Double-talk false triggers | 0 |
| 指标 | 数值 |
|---|---|
| 平均 ERLE(远端单讲) | 26.8 dB |
| 平均近端 SDR(双讲) | −3.6 dB |
| 平均噪声衰减 | 15.3 dB |
| 最差短时谷底 | −3.5 dB |
| 最长重收敛时间 | 3.50 s |
| CPU,每块中位数 | 1.053 ms / 10.67 ms budget — 9.9% of one core |
| CPU,99 分位 | 1.207 ms |
| 算法延迟 | 21.3 ms |
| 滤波器拖尾覆盖 | 256 ms |
| 双讲误触发 | 0 |
What the Residual Suppressor Costs残余抑制器的代价
Paired ablation, measured at a 256-sample block and otherwise identical settings, so these numbers belong to that configuration rather than the one above:成对消融实验,在 256 采样分块下测量、其余设置完全相同,因此下面这组数字属于那个配置,而不是上一节的配置:
- Residual suppression buys 10.1 dB of ERLE and pays 2.7 dB of near-end SDR for it. It is not free.残余抑制换来 10.1 dB 的 ERLE,为此付出 2.7 dB 的近端 SDR。它不是白拿的。
- Stated the other way: tuning against ERLE alone converges to a half-duplex system. Having the trade-off measured in both directions is the entire point of the metric suite — showing the cost of your own feature is a credibility signal, not a weakness.换个说法:只按 ERLE 调参,最终一定会收敛到一个半双工系统。把这个取舍在两个方向上都测出来,正是整套指标体系的意义所在 —— 敢于展示自己所做功能的代价,是可信度的体现,而不是弱点。
| Configuration | Avg ERLE | Avg near-end SDR | CPU median |
|---|---|---|---|
| Linear Kalman only | 15.7 dB | +0.3 dB | 0.477 ms |
| Full pipeline | 25.8 dB | −2.4 dB | 0.837 ms |
| Delta | +10.1 dB | −2.7 dB | +0.36 ms |
| 配置 | 平均 ERLE | 平均近端 SDR | CPU 中位数 |
|---|---|---|---|
| 仅线性卡尔曼 | 15.7 dB | +0.3 dB | 0.477 ms |
| 完整流水线 | 25.8 dB | −2.4 dB | 0.837 ms |
| 差值 | +10.1 dB | −2.7 dB | +0.36 ms |
What Broke, and What That Taught Me出过什么问题,以及它教会了我什么
- Three bugs, one root cause. Sweep deconvolution reporting THD above 100% (physically impossible), a minimum-statistics noise floor reading ~22 dB low, and a diverging shadow-filter NLMS all shared one conceptual error: a regularization denominator written as an absolute constant instead of relative to signal power. Deconvolution used 1e-8 against a median spectral power of about 7e4 — a relative term of 1.4e-13, which is to say no regularization at all. Generalizing that into a rule also explained why the main Kalman filter was immune: its denominator carries the observation-noise PSD as a natural floor.三个 bug,一个根因。扫频解卷积报出超过 100% 的 THD(物理上不可能)、最小值统计的噪声底噪偏低约 22 dB、影子滤波器的 NLMS 发散 —— 三者共享同一个概念性错误:正则化分母被写成了一个绝对常数,而不是相对于信号功率的量。解卷积用的是 1e-8,而频谱功率的中位数约为 7e4 —— 相对量只有 1.4e-13,等于完全没有正则化。把它总结成一条规则之后,也顺带解释了为什么主卡尔曼滤波器免疫:它的分母里带着观测噪声功率谱,天然构成一个下限。
- A parameter that was hiding a bug. The over-subtraction factor had been tuned to 48, roughly 25× above its principled range, and the output sounded fine. It was silently compensating for a 22.6 dB bias in the noise estimator — taking an N-frame minimum of a raw exponentially-distributed periodogram has expectation 1/N of the true mean, which at N = 192 is 22.6 dB low. Once the estimator was unbiased, 48 became destructive. I now treat a parameter far outside its principled range as a symptom, not a setting.一个正在掩盖 bug 的参数。过减因子曾被调到 48,大约是其合理范围的 25 倍,而输出听起来还挺正常。它其实是在悄悄补偿噪声估计器里 22.6 dB 的偏差 —— 对一个服从指数分布的原始周期图取 N 帧最小值,其期望是真值的 1/N,在 N = 192 时就是偏低 22.6 dB。一旦估计器变成无偏,48 立刻变得破坏性极强。现在我把"远远超出合理范围的参数"当成症状,而不是设定。
- Two bugs that suppressed their own evidence. A recorder’s queue-full handler was a bare pass, so recordings developed hundreds of milliseconds of holes while health reporting showed zero drops — the zero-sample fraction went from a 0.75% hardware baseline to 8.47% with no counter moving. And a jitter buffer that force-refilled on underrun bought a full prefill of silence per underrun, compounding to measured 650 ms gaps that were never counted as packet loss. A bug that hides its own evidence costs more than a bug that crashes; every drop path in the final code increments a counter.两个会抹掉自身证据的 bug。录音器的队列满处理写成了一句裸 pass,于是录音里出现了长达数百毫秒的空洞,而健康统计始终报告零丢弃 —— 零采样占比从 0.75% 的硬件基线涨到 8.47%,却没有任何计数器动一下。另一个是抖动缓冲在下溢时强制重新预填,于是每次下溢都白买了一整个预填时长的静音,连续下溢叠加后实测出现 650 ms 的空隙,而这些从不被计为丢包。一个会隐藏自身证据的 bug,比一个直接崩溃的 bug 代价高得多;最终代码里每一条丢弃路径都会让计数器加一。
- Power supply, not mechanical resonance. On the Raspberry Pi platform the linear ceiling was pinned near 6 dB. The first plausible hypothesis was a high-Q structural mode through the shared chassis, which would genuinely be uncancellable by any linear filter. The actual cause was board supply noise: connecting the HAT’s independent supply dropped the microphone noise floor from −26.1 to −42.3 dBFS. In embedded audio the power rail deserves suspicion before the algorithm does.是供电,不是机械共振。在树莓派平台上,线性天花板被钉在 6 dB 附近。第一个看起来合理的假设是共享机身上的高 Q 结构模态 —— 这种东西确实是任何线性滤波器都消不掉的。但真正的原因是板级供电噪声:接上扩展板独立供电后,麦克风底噪从 −26.1 dBFS 降到 −42.3 dBFS。在嵌入式音频里,电源轨比算法更值得先被怀疑。
- Knowing when to stop. I tried to bring a ReSpeaker 2-Mic HAT to the RK3588S board. The 40-pin header carries no I2S signals, the kernel lacks the codec driver, and no I2S device-tree overlays exist in the vendor SDK. I2C did enumerate the codec at 0x18 after enabling the right pinmux overlay — a tempting partial success — but audio transport is physically impossible without I2S. I ruled it out permanently and fell back to the onboard ES8388 codec over the 3.5 mm TRRS jack, which gave single-card duplex and a noise floor 35 dB below the Pi baseline.知道什么时候该停。我曾尝试把 ReSpeaker 2-Mic 扩展板移植到 RK3588S 板上。40 针排针上没有任何 I2S 信号,内核缺少该编解码芯片的驱动,厂商 SDK 里也不存在 I2S 设备树覆盖。在启用正确的 pinmux 覆盖后,I2C 确实在 0x18 枚举到了芯片 —— 一个很诱人的局部成功 —— 但没有 I2S,音频数据传输在物理上就不可能。我把这条路永久排除,转而回退到板载 ES8388 编解码芯片、走 3.5 mm TRRS 接口,由此拿到了单声卡双工,底噪比树莓派基线低 35 dB。
- Why the intercom sounded worse than the loopback test. Not a network bug and not an algorithm bug: sparse far-end speech provides far less filter excitation than a continuous test signal, and an adaptive filter only learns while it is being driven. The test was easier than reality, which is harder to notice than a crash — and it is why the calibration signal is speech-shaped noise with a syllabic envelope rather than a looped speech clip.为什么真实对讲听起来比环回测试差。既不是网络 bug 也不是算法 bug:稀疏的远端语音给滤波器提供的激励,远少于连续的测试信号,而自适应滤波器只在被驱动的时候才学习。测试比现实更容易 —— 这比一个崩溃更难被发现 —— 这也正是标定信号选用带音节包络的语音整形噪声、而不是循环播放一段语音的原因。
What I’d Do Differently如果重来,我会做哪些改变
The largest single improvement in the whole project was not algorithmic. Connecting an independent power supply improved the noise floor by 16 dB, and moving the speaker took ERL from 5.8 dB to 20.6 dB. No parameter change I made came close to either. I started believing the interesting work was in the filter; I finished believing the filter is the last 30% and the acoustics and electronics are the first 70%.整个项目里最大的单项提升并不来自算法。接上独立供电让底噪改善了 16 dB,而挪动扬声器把 ERL 从 5.8 dB 提到了 20.6 dB。我做过的任何参数改动都远远比不上这两件事。开始时我以为有意思的工作都在滤波器里;结束时我相信滤波器是最后的 30%,声学与电子学才是最前面的 70%。
- Build the measurement infrastructure first. The simulator and benchmark harness arrived partway through, and every tuning decision made before the metrics existed had to be revisited.先把测量设施建起来。仿真器与评测框架是做到一半才出现的,而在指标存在之前做出的每一个调参决定,后来都不得不重做一遍。
- Get to real hardware measurements earlier. A long stretch of simulation work rested on assumptions about the acoustic path that physical measurement later contradicted. Simulation is for iterating on things you have already grounded, not for discovering what the ground is.更早接触真实硬件测量。有很长一段仿真工作建立在对声学通路的假设之上,而这些假设后来被物理测量推翻了。仿真适合用来迭代你已经落地过的东西,而不适合用来发现"地在哪里"。
- Write the parameter justification comments while deciding, not at the end. Reconstructing “why is this 1.75?” months later costs far more than the ten seconds it would have taken at the time.在做决定的当时就写下参数的依据注释,而不是最后补。几个月后再去重建"这个 1.75 是怎么来的",代价远高于当时花掉的那十秒。
- Ask “is this reading even physically possible?” first, not fifth. Several days went into hardware hypotheses — mechanical resonance, codec limitations — that turned out to be tool bugs.把"这个读数在物理上可能吗?"放在第一步,而不是第五步。我曾把好几天花在硬件假设上——机械共振、编解码芯片限制——最后发现都是工具本身的 bug。
Scope, and What I Am Not Claiming范围,以及我不声称的部分
The honest boundaries of this work:这项工作诚实的边界:
- All quoted cancellation figures come from the simulation suite, not from measurements on the robot. Hardware bring-up and calibration were completed on two platforms (the Raspberry Pi 4B, then the LubanCat-4). The final program is embedded in the robot and runs on the LubanCat-4 board installed on it. The final end-to-end test between the operator and the robot was carried out by colleagues, not by me, so no on-robot figures are quoted here.所有引用的消除量数字都来自仿真场景集,而非在机器人上实测。硬件调试与标定已在两个平台(先是树莓派 4B,后是 LubanCat-4)上完成。最终程序已嵌入机器人,运行在装于机器人上的 LubanCat-4 板上。操作员与机器人之间最后的双端测试由同事完成,不是我做的,因此这里不引用任何机器人实测数字。
- CPU figures are from a development machine, not measured on target. The RK3588S is considerably faster than the Pi 4 the harness extrapolates from, but I have no measured on-target number and will not invent one.CPU 数字来自开发机,而非在目标板上实测。RK3588S 比评测框架用来外推的树莓派 4 快得多,但我手上没有目标板上的实测值,也不会编一个出来。
- The residual suppressor has an ONNX inference hook and a defined three-feature vector, and the training-data generator and external-baseline comparison harness exist — but no trained model ships here. DTLN-aec (MIT) was used for baseline comparison only, never integrated.残余抑制器里有一个 ONNX 推理挂载点和一个已定义的三维特征向量,训练数据生成器与外部基线对比框架也都存在 —— 但这里不包含任何已训练的模型。DTLN-aec(MIT 许可)仅用于基线对比,从未被集成进来。
- The A-end runs no echo cancellation by design. The system depends on the operator wearing closed-back headphones; that is a documented architectural assumption, not a gap, but it means this is not “echo cancellation at both ends.”A 端按设计不跑任何回声消除。系统依赖操作员佩戴封闭式耳机;这是一个被写进文档的架构假设,而不是缺口,但也因此,这套系统不能被描述成"两端都做回声消除"。