复盘笔记...为什么一定要五个字...
机器人语音前端工程笔记
本文不是一篇算法论文,也不是标准 AEC benchmark,而是一篇机器人实机语音链路的工程经验记录。
我本身是嵌入式软件工程师,不是专业音频算法方向出身。这篇文章主要记录我在机器人项目中遇到的问题、尝试过的处理方式,以及当前阶段的结果。
文中的数据都来自我当前的硬件环境和机器人链路,主要用于说明调试方向和模块组合思路,不代表通用设备上的绝对性能。
把这套链路、测试现象和当前方案整理出来,一方面希望能给同样在做机器人语音前端的人提供一些参考,另一方面也希望获得更多建议。如果有音频算法、AEC、语音增强或实时音频系统方向的朋友看到其中不严谨、不合理,或者有更好的处理方式,欢迎直接指出。
机器人语音交互里最尴尬的一幕,往往不是识别模型不够强,而是前端音频已经乱了:机器人刚开始说话,麦克风就把它自己的声音又送回了 ASR。
更麻烦的是,真实用户不会总是等机器人完全说完才开口。很多时候,扬声器还在播放,人已经开始说话。这时麦克风里会同时出现机器人声音、用户人声、房间反射、USB 声卡底噪,以及少量硬件杂音。
如果只做普通“降噪”,系统其实是盲的:它不知道哪些声音来自扬声器,哪些声音来自用户。
结果通常会在两个坏状态之间摇摆:要么回声压得很干净,但人声也被一起切掉;要么人声保住了,但机器人自己的声音又混进 ASR。
这套实时音频前端要解决的,就是这个夹缝里的问题:在机器人外放、用户插话和复杂声学环境同时存在时,尽量压住残留回声,同时保住真正的人声。

这不是单纯的降噪问题
当前主路线是:
WebRTC AEC -> URES -> DTLN 主增强 -> final guard -> robot_virtual_microphone
WebRTC AEC 负责吃掉大头回声。它拿到播放参考流,知道扬声器理论上正在播什么。
但现实里的回声不是一条干净的复制信号:扬声器有非线性失真,房间有反射,USB 声卡有底噪,人声还可能同时插进来。AEC 之后仍会有残留,也会遇到双讲判断的灰区。
所以我在 AEC 后面加了自研的 URES。
URES 的全名可以理解成 User-Centric Residual Echo Suppression。名字里最重要的是 User-Centric:它不是为了把所有声音压到最安静,而是优先保留用户说话。
它会综合 far-end 活跃度、近端迹象、残留回声分数、双讲风险、输出包络和冷启动状态,决定当前应该强压、轻压、还是放过人声。
我把 URES 的独立核心拆成了 MIT 开源项目:
https://github.com/lIlIll1II1ll/ures
开源版本只保留通用的实时后处理核心、C++ API、WAV demo 和 smoke test,不包含这台机器人上的 ALSA 配置、虚拟声卡服务、DTLN 权重、运行日志或私有数据。
DTLN 主增强则像最后的整理层。它不是裸接在麦克风上,也不是替代整套 AEC。当前这条线保留了 WebRTC AEC 和 URES 的控制基础,再用 DTLN 做流式神经增强,减少双讲和非线性残留里的毛刺。
当前项目里接入的 DTLN-AEC,参考的是 Westhausen / Meyer 的 Dual-Signal Transformation LSTM Network AEC 论文和 breizhn/DTLN-aec。
对应地址放在文末参考里:GitHub 仓库和论文地址都列出来,没实际引用进当前实现的调研路线就不写进正文。
这点很关键:大模型或神经网络不是万金油,实时系统里更重要的是它被放在什么位置、什么时候该相信它、什么时候不能让它乱救。
一个小但很值钱的测试结果
这次测试的报告文件是:
data/reports/audio_comparison_20260424_225923.json
data/reports/audio_comparison_20260424_225923.png
几组数字比较有代表性。
只有扬声器播放、没人说话时:
原始麦克风 RMS: -30.73 dBFS
虚拟麦输出 RMS: -77.68 dBFS
抑制量: 46.95 dB
这说明机器人自己说话时,虚拟麦基本不会把扬声器内容原封不动交给上层 ASR。
双讲时:
原始麦克风 RMS: -26.20 dBFS
虚拟麦输出 RMS: -35.48 dBFS
抑制量: 9.28 dB
dropout_count: 6
onset_loss: 约 20ms
这组数字没有“完美”。人声不是录音棚级别,仍然能听到处理痕迹,也有少量断续。但主观听感是可接受的,语义能保住,AI 识别也能用。对机器人实时交互来说,这是比“指标漂亮但开口丢字”更重要的结果。
静音和背景段也有明显下降:
原始麦克风 RMS: -69.34 dBFS
虚拟麦输出 RMS: -105.10 dBFS
抑制量: 35.76 dB
这意味着系统不是只会在大声播放时工作,低能量背景里也有稳定的门控和残留抑制。
真正难的是冷启动
最开始我也一度把注意力放在“稳态压不压得住”。后来发现,真正难的是前几百毫秒。
真实对话里,人不会等系统热好。机器人刚发声,人就可能插话。这个时候 AEC 的参考流、采集流、滤波器、DTLN 状态和播放队列都在刚进入工作区。
如果处理太保守,开头几个字会被吞;如果放得太开,扬声器就混进来。
所以现在系统里有几层冷启动处理:
- route-aware delay prior:按设备和路线记住上次稳定的回声时延。
- 初始化音频校准:初始化按钮不只是“播一段音频”,也是在给下次启动攒时延先验。
- startup guard:只在开头短窗口兜底,避免 AEC 未稳定时直接漏声。
- DTLN warmup wet mix:DTLN 主增强不是第一帧就全量接管,而是平滑进入目标比例。
- reference delay 对齐:播放缓冲改变时,同步修正 DTLN 看到的参考流。
之前测试脚本会自动裁掉开头看起来“不稳定”的一小段。这个设计对看稳态有用,但对这个项目反而误导,因为冷启动双讲正是我们要解决的问题。
现在脚本已经改成默认保留完整开头;只有显式加 --trim-startup-noise 时,才生成稳态报告。
实时性不是把缓冲调到 0
这套系统内部按 10ms 音频帧运行。WebRTC AEC、URES、虚拟声卡、DTLN 在线增强和播放队列,都围绕这个节拍协作。
一开始我也想把缓冲压得很低。实时区间可以做到:
虚拟扬声器入口: 10-30ms
入口最长等待: 10-30ms
真实扬声器启动缓冲: 0-20ms
但在这台机器上,极限实时档会带来扬声器刺声、爆破感和状态抖动。最后这条路被保留下来,但只作为“实验模式(极限低延迟)”,不作为日常交付状态。
当前更稳的日常模式是普通区间:
虚拟扬声器入口: 20-60ms
入口最长等待: 15-50ms
真实扬声器启动缓冲: 10-40ms
这比系统级兼容档 40-120ms / 25-100ms / 20-80ms 低不少。以当前常见的 60/50/40ms 稳定点算,相比系统级上限 120/100/80ms,三段缓冲分别少了 60ms / 50ms / 40ms。
更重要的是,它不是固定死的。播放稳定控制器会观察队列深度、欠流、边界波动和漏声风险:不稳时加缓冲,稳定一段时间后再尝试回收一档。
这里有一个很容易踩坑的点:播放缓冲不是只影响“听起来晚不晚”,它还会影响声学回灌和参考流之间的相对时延。
所以系统里做了一个 playout delay ledger:
有效 AEC delay =
当前路线的 delay prior
+ 真实扬声器播放缓冲相对校准参考的变化
虚拟扬声器入口缓冲会同时延后 reference 和 playback,所以不计入相对回声 delay;真实扬声器启动缓冲会改变扬声器真正出声时间,所以必须进入 AEC 和 DTLN 的时延账本。
这个联动修好后,系统稳定性明显提升。否则会出现一种很烦的现象:同样设备、同样位置,上一轮很好,下一轮突然漏声。不是算法“玄学”,而是播放策略变了,AEC/DTLN 却不知道。
URES 的价值在哪里
如果只看模型名字,大家容易把注意力放在 WebRTC 或 DTLN 上。
但这套链路里,URES 才是最像“产品工程经验”的部分。
它不是一个单独模型,也不是一个神秘的大函数。它更像是把几块小能力拼成的一台实时“小调音台”:每 10ms 看一次播放参考、麦克风、人声迹象和残留风险,然后决定这一帧到底该压多少、保多少。
可以粗略写成这样:
URES =
播放参考观察
+ 麦克风近端观察
+ 回声相关性判断
+ 双讲/近端/远端状态机
+ 人声优先保护分数
+ 残留回声压制
+ 轻量噪声/电流声门控
+ 人声补偿和输出平滑
第一块是播放参考观察。系统会看扬声器参考流有多活跃,也就是 far-end 当前是不是在说话、音量大概在哪、有没有明显的播放边界。这个信息很重要,因为机器人自己正在播什么,系统是知道的。
第二块是麦克风近端观察。这里不只看麦克风音量,还会结合 VAD、短时能量、包络变化和近端概率。目的不是简单判断“有没有声音”,而是判断“这个声音有没有可能是用户”。
第三块是回声相关性判断。只有麦克风里出现了声音还不够,还要看它和播放参考像不像。URES 会看当前帧相关性,也会看带一点延迟搜索的峰值相关性。这样可以区分两类声音:一类是和扬声器参考高度相似的残留回声,另一类是和参考没那么同步的人声。
第四块是状态机。真正的难点不是某一帧,而是连续几百毫秒里的状态变化。URES 会把这些观察量揉成几个更稳定的状态:
far_end_leak_score
echo_like_score
nearend_dominant_confidence
double_talk_risk
这些名字听起来像指标,但在工程上它们更像几个“交通灯”。它们告诉后面的处理:现在更像纯回声、近端主导、双讲灰区,还是冷启动不确定区。
第五块是人声优先保护。URES 里有一个核心思想:压制强度不能只由“像不像回声”决定,还要由“用户会不会被误伤”决定。
所以它会形成一个类似 preserve_preference 的保护倾向:如果更像真人声,就别一刀切;如果更像扬声器残留,就压得更坚定;如果是双讲灰区,就宁愿保守一点,不让句头句尾被硬切。
第六块才是残留回声压制本身。它不是整段声音统一乘一个固定增益,而是参考播放强度、相关性和当前样本能量做动态压制。低能量残留、强相关残留、静音里的电流声和播放漏声,会被更积极地压下去。
第七块是把声音重新“收拾得能用”。因为只压不补,声音会变小、变碎、变干。URES 后面还会做轻量的人声补偿、近端存在感提升、输出平滑和瞬态毛刺处理。这样做不是为了把声音美化成播客录音,而是为了让 ASR 听得稳定,人耳听起来也不至于断断续续。
所以 URES 做的不是单纯残留抑制,而是一套决策:
现在更像纯回声?
现在更像真人声?
现在是双讲灰区?
当前是否处在冷启动?
近端人声是不是太弱,容易被误当成残留?
DTLN 输出是不是过度压低,是否会触发硬切换?
这些问题没有一个单独阈值能解决。之前也试过更激进的保护:人声更大了,但回声又漏出来;也试过更强的压制:背景干净了,但句头句尾会缺。
最后能用的版本,是一组分层控制:AEC 做主消除,URES 做人声优先的残留判断,DTLN 做神经整理,final guard 只兜底。
换句话说,URES 的价值不是“我又发明了一个 AEC”,而是把 AEC、神经增强和真实硬件之间那段最容易翻车的灰区接住。
这种分层的好处是系统不会在两个极端之间来回摆。
还有哪些不完美
这版不是“完美 AEC”。我不想把它包装成那种听起来像发布会的东西。
现在仍然有几个边界:
- 人声不是完全干净,能听出处理感。
- 双讲里仍可能有轻微残留或短暂 dropout。
- 原始麦如果被近距离直吹,会出现爆破音或瞬态过载,算法不能凭空还原被硬件削掉的波形。
- USB 扬声器无播放时也有轻微滋滋底噪,这是声卡/功放/供电链路问题,不是 AEC 可以消掉的。
但它已经进入了一个实用区间:
机器人可以实时播放,麦克风可以同时采集,人声虽然不如录音棚干净,但听感可接受,AI 识别也能用。
这对机器人来说已经是一个很重要的门槛。
小结
这次工程的收获不是“换了一个更强模型”,而是把几个原本割裂的东西接到了一起:
ALSA 直通
虚拟扬声器 / 虚拟麦克风
WebRTC AEC
自研 URES
DTLN 在线增强
动态播放缓冲
AEC/DTLN 时延账本
冷启动 Warm Start
中文音频对比报告
最终目标很简单:让机器人不是只能“说完再听”,而是可以一边说话,一边听见人。
这听起来像一个小能力,但落到真实硬件上,它牵扯到声卡、缓冲、调度、参考流、双讲、冷启动和人声保护。
真正把它做顺之后,系统才开始像一个可以持续迭代的实时语音前端,而不是一堆各自为战的音频处理模块。
参考与致谢
- URES 开源核心:https://github.com/lIlIll1II1ll/ures
- DTLN-AEC 开源实现:https://github.com/breizhn/DTLN-aec
- DTLN-AEC 论文:https://arxiv.org/abs/2010.14337
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)