用户说“打开客厅灯”,ASR 却只收到“开客厅灯”。设备日志明明显示 VAD 已经触发,开发者很容易先把检测门槛调低。但那条日志只说明检测事件发生了,不说明整句音频从哪里开始交给 ASR。

本文以 ESP32-S3 的乐鑫 ESP-SR/AFE 文档为依据,设计一个定位首字丢失的排障方法。开头的例子和后文的录音对照都是工程假设,没有在实体板卡上完成实测;也不能由此断言小智固件当前就使用下述接口。

VAD 触发与录音起点分离的作者示意

VAD 的第一帧,未必是语音的第一帧

乐鑫 ESP32-S3 VADNet 文档把延迟拆成两部分:算法本身可能晚 1–3 帧触发;为了减少误触发,连续语音还要达到 vad_min_speech_ms 才正式触发。文档明确提醒,直接从 VAD 首次触发帧开始保存音频,可能把首字截掉;AFE V2.0 因此提供了 VAD cache。

乐鑫 VADNet 触发延迟与缓存的官方要点摘录

这不是说 VAD 一定会丢字。问题出在应用把“现在确认有人声”误当作“现在才开始录这句话”。触发线之前可能已经有属于这句话的采样;如果上送 ASR 的片段从触发线才开始,识别端根本无从恢复被丢弃的开头。反过来,ASR 文本漏字也可能发生在麦克风采集、编码、传输或识别本身,不能只凭 vad_state 把责任推给 VAD。

官方配置示例里的 vad_min_speech_ms = 128 是连续语音门槛,不是“应该丢掉 128 ms 音频”,也不是任意设备的最优延迟。文档还单独给出 vad_delay_ms。若只是把门槛调低,事件可能更早触发,也可能让噪声更易误触;应用没有接回触发前音频时,片段起点的错误依然存在。

把一个“开始”拆成四个时间点

定位时可以画四条竖线:麦克风采样开始、真实发声开始、检测确认发生、业务片段开始。前三条属于采集与检测,最后一条属于应用交接。它们接近并不代表相等。尤其在按键、唤醒词或网络握手之后才建立片段的系统里,业务起点可能比检测起点还晚;这时即使检测算法提前,传输仍然可能从更靠后的声音开始。四个时间点是本文建议的观察方式,实际能否取得精确时间,应由设备采样计数与日志精度决定。

例如,假设用户在检测确认前已经说出一个短音节,AFE 保存了这部分处理后声音,但业务队列直到确认事件之后才接受音频。这段声音不是“噪声被 VAD 过滤了”,而是业务尚未把过去的声音纳入当前轮次。若开发者只比较检测状态和 ASR 返回时间,就会把一段明确的截断误解成模型速度问题。应该把片段第一个采样对应的位置标在同一条时间轴上,再判断缺口发生在检测之前还是交接之后。

还有一种相反的假设:设备一直采集,应用也保留了前缀,但用户在麦克风初始化结束之前开口。此时 AFE cache 再大也找不回尚未采集的声音。检验这一分支的方法是看原始采样留样,而不是从配置表推算缓存足够长。缓存只能保存它接收到的历史,不能补造硬件没有采到的历史。对按键说话产品,这个区别还影响提示设计:业务应清楚定义什么时候可以开口,并让提示与实际采集状态对齐。

参数、保留窗口与交互等待分别回答什么

连续语音门槛回答“观察到多长的人声才确认”,前缀保留回答“确认时把哪部分历史交过去”,业务超时回答“等多久结束或放弃这一轮”。把三者合成一个所谓低延迟参数,会让调试失去方向。降低确认门槛之后,误触发可能增多;扩大保留窗口之后,每轮可能带入更多背景声;放宽业务超时之后,用户可能等待更久。这些取舍需要分别观察,不能因为转写多了一个字就认为整套交互都变好。

文档给出的帧延迟也不能直接转换成固定丢字数量。一个字的持续时间会随说话速度、发音和音量变化,而帧长要按所用模型与接口核实。对同一套配置,慢说的首字可能在截断后仍留下足够信息,快说的短音节却可能几乎消失。因此,验收样本应覆盖短句开头与自然连续说话,不宜只反复说一个容易辨认的长词。这里没有给出通用帧长或推荐参数,因为本文没有目标固件的完整配置与实测音频。

有缓存时,先交前缀再交本次音频

乐鑫 AFE 使用示例从 fetch 结果读取 result->data 和 vad_state,并检查 vad_cache_size。示例专门注明:若存在 vad_cache,应把它接在处理后音频前面,以免丢失数据。

触发前缓存先于本次处理后音频的作者机制示意

把这处交接写成可检查的顺序,就是:非空的缓存前缀 → 本次处理后音频 → 后续音频 → ASR 输入。如果日志只显示“cache 存在”,发送路径却从 result->data 开始,首字仍会丢;如果同一前缀在两个位置各拼一次,ASR 又会收到重复声音。应记录缓存大小、处理后数据大小、片段序号或时间戳,以及实际发送的字节范围,而不是仅记录一次 VAD 状态变化。

乐鑫 AFE fetch 字段与缓存顺序的官方要点摘录

这里有一个实现边界:官方示例说明了拼接顺序,却不能替你的固件保证缓存指针可保留多久、当前版本每次 fetch 是否返回缓存、上游是否已把前缀消费。要按实际 ESP-SR 版本与业务代码核对生命周期,再决定是当场复制还是立即交付。vad_cache_size 为零时也不能凭空补出前缀;先确认配置和取样位置,再判断是否需要另一个有界预录缓冲。

一次拼接需要同时核对内容、单位和轮次

先检查缓存和当前音频是不是同一条处理路径。若缓存来自 AFE 处理后输出,当前片段也应按对应格式交接;把原始多通道采样与处理后的单路声音直接拼成一段,是另一个待验证的设计,不能因为两段都叫 PCM 就默认一致。核对时记录采样率、采样表示、通道排列和长度单位。字段名里的 size 不足以证明它表示采样数还是字节数,应查当前版本的类型定义与示例,防止复制范围与发送范围使用不同单位。

然后检查轮次归属。第一轮的前缀不能留在队列里成为第二轮的开头,取消轮次之后的旧发送任务也不能继续把音频写入新轮次。可以在业务片段上带一个轮次标记,在留样记录里把缓存前缀和本次数据标到相同标记下。标记并不替代音频顺序,只是让异步日志能够解释“这份缓存究竟属于谁”。如果应用已经有会话标识,就复用它,不必为了排障再建立一套状态机。

最后看消费次数。所谓“先缓存再当前音频”是针对一次实际交接的顺序要求,不等于每收到一个语音状态都重发前缀。应用需要辨认新片段首次交接与同一片段持续交接,按实际接口行为消费缓存。也不应自行假设缓存只在某一次 fetch 返回,或者永远不会重叠;这些必须用当前版本的结果和采样范围检查。记录连续几次输出的前缀范围,能够发现队列重试时重复写入、分支重入时二次拼接等应用错误。

编码和异步发送为什么容易把正确顺序弄丢

假设音频在发送前经过编码,缓存前缀进入编码器的时机就很关键。若先把当前音频编码并排入网络队列,之后才发现前缀需要补发,即使两个任务最终都执行了,接收端也可能先听到中间部分。发送函数调用顺序、队列入队顺序与接收端重组顺序需要一起核查。这里是一个链路假设,并不指认某个编码器或协议存在此缺陷;实际排障应保存最终重组后的音频证明顺序。

又如,代码把 fetch 返回的指针放入队列,下一次 fetch 后消费者才读取。打印时缓存内容看似完整,异步消费时却可能已经变化。本文不宣称这些指针一定在下一次调用失效,只强调不能凭示例中“得到指针”推导“队列可以长期持有指针”。应查明对应实现的有效期,并让业务的数据所有权与它一致。如果需要复制,复制大小必须与查明的长度单位相符,同时处理队列满或取消时的释放路径。

有界预录缓冲也有自己的代价。它需要容量上限、覆盖策略和片段开始时的取出规则,不能把持续采集的所有数据都堆进内存。更重要的是,预录缓冲和 AFE cache 若保留了同一区间,直接相加可能造成重复开头。选择额外缓冲前,应先画出两者各自保存的采样范围;只有确定缺失范围和重叠范围后,才能设计衔接。本篇优先要求使用官方提供且能够核对的 cache,不把第二套缓冲当默认补丁。

三处留样,找到首字第一次消失的位置

对同一句短语,在三个边界留下可对齐的音频:① 麦克风采集后的原始输入;② AFE fetch 的缓存和处理后音频;③ 真正送给 ASR 的片段。每处记录采样率、通道数、位宽、序号或时间戳,以及固件和 ESP-SR 版本。这是本文的排障设计,不是乐鑫官方验收规范,也不是已完成的设备实验。

第一处缺口该查什么暂时不能下的结论
原始输入已缺首字采集何时开启、前端输入是否截断不能怪 VAD cache
原始输入有,AFE 缓存有,ASR 入口没有缓存拼接、编码、发送队列与传输不能说 ASR 识别能力差
ASR 入口音频完整,转写仍漏字音频格式、ASR 切分与识别结果不能据此改 VAD 门槛

原始输入、AFE 输出和 ASR 入口的分段排障示意

先听和看实际送入 ASR 的波形,再比较文字结果。选择短促或弱辅音开头的短句,在相同距离和噪声下重复,对照缓存已拼接与未拼接的入口音频;同时记录误触发和额外等待。若只看转写,ASR 自身的错误会遮住前端问题。若只看一段录音,又可能把一次偶然识别结果当作修复。

只有证据表明缓存已正确接入,而 AFE 输出仍在开头缺声音时,才逐项对照 VAD 模式、vad_min_speech_ms、vad_delay_ms 和唤醒后的启停时机。每次只改一个变量,并保留原始输入与 ASR 入口样本。参数变化要同时看首字保留、误触发和等待量;官方文档没有给这块假设设备的最佳配置。

交付排障结论时,留下三处音频、缓存与发送顺序、版本和复测记录。这样才能说清首字第一次在哪一层消失,决定是修采集、补缓存、修传输,还是查识别。本文证明的是一条基于官方接口的可核对方法,现场效果仍需用目标板卡与真实语音验证。

一次可复查的假设实验怎样组织

准备实验时,先固定说话内容和输入条件,再建立一个基线轮次。假设短句是“打开客厅灯”,要保存实际说出的声音,而非只记录预期文字。原始输入、AFE 输出与 ASR 入口之间可能存在采样格式变化,因此对齐时应注明转换步骤,并用明显音节或采样序号寻找同一事件。若某处是压缩音频,至少保留解码后的听感与时间关系;不能拿文件大小相同当作声音完整的证明。

基线轮次先保持当前逻辑,观察缺口是否可重复出现。第二组只改缓存交接,并记录每轮前缀大小、实际发送长度和片段第一个有效声音的位置。第三组才考虑必要的检测参数对照。分组顺序的价值是减少混杂:若同时修改门槛、缓冲容量、编码和 ASR 模型,就算结果变好,也无法解释是哪一步补回首字。某个分支没有触发时,要记录未覆盖,不能把未观察到故障写成已经排除。

样本中应包含正常开头与容易暴露边界的开头。比如以短促音节开头、自然说完整句、唤醒后立即说命令,以及稍作停顿后说命令。它们各自回答不同问题:短音节观察截断敏感性,立即说话观察唤醒与录音交接,停顿说话帮助比较片段起点。距离和环境噪声需要在组内保持可比,否则可能只是输入信噪比改变。这里只提出覆盖思路,没有把任何样本数量或成功率写成官方要求。

结果整理可以按轮次写四项:原始输入是否包含首音节、AFE 输出是否包含、ASR 入口是否包含、转写是否保留。前三项是音频完整性,最后一项是识别表现。若音频完整而文字仍错,前端修复不能保证文字成功;若文字正确而入口仍缺声音,也不能证明前端没有问题,因为模型可能依据后文补出答案。把音频与文本分开判断,是避免用语言模型的纠错能力掩盖传输缺口。

看见“首字补回”后,还要检查哪些反例

一个常见反例是前缀确实补回,但同一句的开头出现两次。用户可能听不出来,ASR 却可能把重复声解释成停顿、重复词或另一个字。因此除了看开头是否存在,还要看波形有没有重复区间、缓存与当前块是否连续。只比对转写字符串不够:某次转写仍正确,重复音频也可能在下一种短句上出错。合理的验收结论应包含没有观察到重复拼接的证据,而不是只报漏字次数下降。

另一个反例是缓存把上一轮尾音或提示音带进新轮次。发生这种现象时,不宜直接缩短所有前缀来掩盖问题,应先看轮次切换点和缓存消费点是否一致。若应用在上轮结束后立即播放提示,当前轮是否允许录入这段声音也要作为明确设计决定。本文没有为提示音设计 AEC 策略,只要求交付结果能解释多出的声音来自哪里,以及它是否属于本轮预期输入。

还有一种结果是 cache 总为零,却观察到首字完整。它只能说明这组输入和配置下没有在所留样位置发现缺口,不能推出所有弱音节和快速开口场景都安全。反过来,cache 非零且首字仍缺,也不自动证明缓存机制无效;可能是前缀在后续编码或队列里被丢了。两个反例都提醒我们:字段值是定位线索,边界音频才是实际交付证据。

将排障结果写成下一位工程师能接手的结论

一份有用的结论应点明首次缺口的边界,而不只写“VAD 已优化”。例如,在某组假设实验中发现原始输入与 AFE 前缀包含首音节,而接收端重组音频从当前块开始,那么结论就应聚焦应用交接;把缺失范围、对应轮次与发送代码位置一起留下,下一位工程师才有条件复查。若三处录音无法对齐,结论应保留未确定,而不是用看似精确的毫秒数制造确定性。

修复后应重复原来触发故障的输入条件,并补查取消、新轮次立即开始和队列拥堵这些交接分支。它们不是要求重新验证整个系统,而是因为缓存拼接正好依赖片段开始和数据顺序。对照中若出现新增误触发、内存增长或明显等待,需要单独记录代价。首字保留改善可以成立,其他指标仍可能退化,不能把一个局部结果写成设备整体质量已经提升。

到现场时,这篇文章最适合变成一张音频留样清单:先确认采集起点,再确认 cache 的内容和单位,随后确认消费者实际收到的顺序,最后才用同一组声音评估参数与 ASR。官方文档给出了检测延迟和缓存交接的依据,实际应用需要补上采样范围、版本和生命周期证据。只有这些信息一致,才能把“VAD 触发了”进一步解释为“整句话已经被交给识别端”。