XVF3800 的 DOA: 从四个麦克风到一圈指示灯

2026-10-26 07:15:23 吴昊 5


读 AUDIO_MGR_SELECTED_AZIMUTHS 时,最常见的两个疑问是:为什么有时返回 NAN?为什么同一个人说话,读到的角度会来回跳几度?答案不在「DOA 算法」里,而在它上游的三波束结构和下游的三层过滤。


一句话结论:XVF3800 的 DOA 不是独立模块,而是波束成形的副产品——每个波束自带一个方位角。固件把这 4 个原始角与 4 个语音能量做 VAD 和三层过滤,才合成「说话人方向」。所以读角度必须同时看语音能量,并且必须接受 NAN 是正常输出(表示当前没有说话人),而不是错误码。


一、波束结构:DOA 的物理来源

四个 PDM 麦克风经 AEC 逐路消除回声后,进入波束成形。这里不是「一个波束 + 一个测向器」,而是三个波束并行工作:

两个 focused beam(聚焦波束)——各自锁定一个说话人,更新慢、稳定,可被 AEC_FIXEDBEAMSONOFF 固定到用户指定的方位角。

一个 free-running beam(自由跟踪波束)——持续扫描环境,专门负责「新说话人出现」这种事件,响应快,代价是也容易被噪声吸走。

最后由 auto-select 逻辑决定输出用哪个波束。每个波束都有自己的方位角,这就是 DOA 的原始数据来源,也是为什么 AEC_AZIMUTH_VALUES 一次返回四个值。

图片关键词

[图 1 DOA 的完整链路:波束成形产出原始角,平滑模块合成说话人方向,三个出口各自消费]

读回来的四个角次序固定是:beam0、beam1 是两个固定波束,beam2 是自由跟踪波束,beam3 是 auto-select 波束。后续所有逻辑都围绕 beam3 展开——它代表「当前系统认为该听谁」。


二、原始数据与派生数据要分开看

XVF3800 暴露了两层数据:波束级的原始量(角度 + 语音能量)和合成后的说话人方向。混用这两层,是大部分「DOA 不准」投诉的起点。

命令个数内容
AEC_AZIMUTH_VALUES4四个波束各自的方位角(弧度)
AEC_SPENERGY_VALUES4各波束的语音能量,非零即「疑似语音」
AEC_FIXEDBEAMSAZIMUTH_VALUES2固定波束的方位角,可读可写
AUDIO_MGR_SELECTED_AZIMUTHS2[0] 合成后的说话人方向(可为 NAN) [1] beam3 原始角

一个容易忽略的官方提示:DOA 给出的角度依赖声学路径的实际测量,因此 AEC_AZIMUTH_VALUES 报告的固定波束角,可能与你在 AEC_FIXEDBEAMSAZIMUTH_VALUES 里设定的值并不完全相等。设定值是「希望它听哪」,报告值是「它实际听到哪」。


三、三层过滤:合成的说话人方向怎么来

固件里这段逻辑是独立的 doa_smoothing 模块(fwk_xvf 里源码可读),它同时解决三个问题:噪声冒充语音、角度单帧跳变、连续小抖动。

图片关键词

[图 2 两条支路:语音能量负责「有没有人说话」,角度平滑负责「他在哪个方向」]

第一层:语音能量 VAD。对 beam3 的能量做快慢双包络——快包络 α=0.9 跟住当前电平,峰值包络 α=0.999 缓慢回落(封顶 10⁶)。判定阈值是峰值的 2% 且必须高于绝对阈值 5000,一旦成立就保持 10 帧。这样既不会被持续的小噪声触发,也不会在句子之间的换气处立刻掉线。

第二层:慢波束一致性校验。只有在 beam3 的角度等于两个固定波束之一时才采信,否则沿用上一次的角度。这一条专门对付「自由跟踪波束被噪声吸走」——真实的说话人通常会同时被某个聚焦波束跟住,而噪声只会骗过灵敏的那一个。

第三层:跳变守门 + 指数平滑。角度变化超过 5° 时才开始计数,需要连续 10 帧且 VAD 有效才接受新角度;小于 5° 的变化直接接受并清零计数。最后用 EWM(α=0.825)做平滑,并处理 0°/360° 的卷绕,避免跨零点时角度从 359° 跳到 1° 被误判成大幅移动。

参数取值作用
快包络 α0.9跟随当前语音能量,决定响应速度
峰值包络 α0.999缓慢回落,形成自适应参考电平
VAD 阈值2% × 峰值
且 > 5000
相对 + 绝对双条件,避免安静环境误触发
跳变阈值5°超过才进入计数,小变化直接采信
连续帧数10 帧滤掉孤立跳变,要求新方向「站得住」
平滑系数0.825EWM:新值只占 17.5%,抹平残余抖动

三层都通过,才输出平滑后的角度;任一层判定为「没有语音」,则直接返回 NAN。所以 NAN 不是故障,它是「当前无人说话」的合法表达——你的上层逻辑必须能处理它,而不是当成异常。


四、三个出口,三种用法

① 读给主机。AUDIO_MGR_SELECTED_AZIMUTHS 返回两个值:处理后的说话人方向、以及 beam3 的原始角。想要稳定指示就用第一个,想自己写滤波就用第二个。注意版本差异:2.0.0 及更早的固件实现不同,返回的是与 AUDIO_MGR_SELECTED_CHANNELS 所选通道关联的波束角,升级固件后读数含义会变。

② 点亮灯环。主机下发 LED_DOA(0..3599,单位 0.1°),固件用三角加权把它拆成 4 组亮度:每组以 0/90/180/270° 为中心、±90° 线性衰减,相邻两组之和恒为 255,因此指向处最亮、且永远不会出现暗区。4 个字节经共享 I²C master 一次突发写入 MCU 的 LED_BURST 寄存器 0x20。也可以用 LED_LEVELS 直接指定 4 组亮度,用 LED_STATUS 回读。

③ 空间音频。把 appconfSPATIAL 置 1(需同时链接 pan_pot 模块),auto-select 波束就会按 DOA 做左右声像。这里有个细节值得学:只有角度变化超过 36°(π/5)才更新 pan pot,源码注释写得很直白——避免「warbly output」,即声像随角度连续微调产生的摇晃感。


五、角度约定与阵列几何

DOA 的单位是弧度(TYPE_RADIANS),只有 LED 接口用 0.1° 的整数。角度从哪个方向起算,取决于麦克风阵列的几何——而几何在编译期就定死了:

图片关键词

[图 3 两种阵列几何,以及 LED_DOA 到四组灯的三角加权]

线性阵列(默认):四麦位于一条直线上,位置为 ±16.65 mm 与 ±49.95 mm,相邻间距 33.3 mm、总孔径 99.9 mm。窄间距保证高频不出现空间混叠,宽孔径提供角分辨率,这是典型的「稀疏嵌套」思路。

方形阵列(-sqr 构建):四麦位于边长 66.6 mm 的正方形四角,平面内两个维度都有孔径,方位角分辨更均匀。

一个必须知道的物理限制:线性阵列关于其轴线前后对称,无法区分声源在正前方还是正后方——同一个入射角会对应两个镜像解。如果产品需要 360° 无歧义定位,就该选方形(或环形)几何,而不是靠算法补救。


六、排错清单

现象根因处理
一直返回 NAN语音能量未过绝对阈值 5000,或 VAD 未成立加大音量/距离;先读 SPENERGY 确认
角度频繁跳变噪声骗过自由跟踪波束看 beam3 是否与 beam0/1 一致;降低环境噪声
角度与设定值不符报告值是实测声学路径的结果属正常偏差,校对安装角度即可
前后方向分不清线性阵列的镜像解改用 -sqr 几何,或用唤醒/视觉辅助消歧
灯环不跟随LED_DOA 未下发,或 I²C 到 MCU 通路异常用 LED_STATUS 回读确认是否写入
升级固件后读数变了2.0.0 前后 SELECTED_AZIMUTHS 语义不同改用 AEC_AZIMUTH_VALUES 自行合成

调试时不必自己画界面:源码包里带 doa_plot.py,通过 python3 xvf_tools.py doa_plot 就能实时可视化四个波束的角度,一边说话一边看哪个波束在跟、哪个在乱跳,比打印日志直观得多。

首页
产品
新闻
联系