音频链路与本地主动播报深挖
音频链路与本地主动播报深挖
为巽1. 两条独立音频路径
1 | Xiaozhi 下行:WebSocket binary raw OPUS -> ai_audio -> libopus -> PCM16 -> MAX98357A |
本地播报不让服务端生成文本,运动/导航事件直接组合预录数字和单位片段,延迟和离线可控。
2. Xiaozhi 会话启动
app_ai_voice_task 等待 Wi-Fi ready event。顺序固定为:稳定等待 1 s -> esp_xiaozhi_chat_get_info() -> 再等 1 s -> 创建 MCP engine、注册 chat callback、强制 WebSocket -> 等 connected 最多 8 s -> 再等 0.5 s -> 打开 OPUS channel、启动麦克风、发送 realtime start listening。
组件返回 MQTT 和 WebSocket 配置时仍强制 WebSocket,避免两种 transport 同时争用。关闭或断线时清 event bits、停音频、释放 chat/MCP,3 s 后由应用层重启。
3. 下行解码细节
音频 callback 不直接解码,只把包复制到内部 8-bit 内存,再投递 s_audio_queue。ai_audio 取包后先调用 opus_packet_get_nb_samples() 验证采样数,再 opus_decode() 到内部 PCM buffer;帧参数是 16 kHz、mono、60 ms,即每帧 960 samples,典型 PCM 长度 1920 B。
decoder 使用源码版 78/esp-opus 1.0.5,不再调用曾触发崩溃的预编译 esp_audio_codec OPUS 路径。解码成功后调用 board_max98357a_write();MAX98357A 负责 3 倍饱和增益和 mono 到 I2S stereo slot 的复制。
下行队列深度 12,满时释放最旧包,避免 TTS burst 导致延迟越来越大。播放前再次检查 s_audio_channel_opened、Wi-Fi 和 stopping 标志,关闭会话后不会把已取出的最后一包继续写入 I2S。
4. 麦克风上行细节
ICS43434 使用 I2S1:BCLK=36、DIN=37、WS=38,16 kHz、32 bit、right slot。ai_mic 以 60 ms 读一帧 960 samples,从 32 bit 原始值取有效 PCM16。
先计算平均绝对值。语音开始需要连续 2 帧超过 150;语音结束需要连续 12 帧低于 120。静音帧直接丢弃,不进入 OPUS 编码;编码器启用 DTX,opus_len <= 2 的小包也不上传。TTS 播放期间保持采样但暂停上传,降低回声回送。
OPUS encoder 为 VOIP、bitrate=16 kbps、complexity=1。连续 3 次 send_audio_data 失败后停止本轮上传并触发会话重启。encoder 状态对象使用 PSRAM;I2S raw、PCM16 和 OPUS packet 缓冲使用内部 8-bit,保证实时读取、编码和发送路径可用。ai_mic 栈 32 KiB 放 PSRAM,解决 silk_encode_frame_FIX() 栈保护异常。
5. 本地 PCM 播报
资源目录 /sdcard/navigation_audio/,格式为裸 signed 16-bit little-endian、16 kHz、mono,无 WAV 头。生成脚本预先移除头尾静音,运行时不扫描静音、不解码、不重采样。
事件检测 task 每 1000 ms 读取运动快照。P0:转向、到达;P1:每公里、每五分钟、持续速度变化。动态数字拆为 num_0..num_10000、小数点和单位文件,播放 task 依次从队列读取文件并以 2048 B PSRAM 缓冲写 MAX98357A。
优先级策略:P0 到来时调用 app_ai_voice_local_playback_begin(true),停止当前 TTS 并清 OPUS 队列;本地播放期间暂停麦克风和下行。P1 在 Xiaozhi 回复时等待,P0 到来可以打断 P1。播放结束根据会话是否仍有效恢复监听,否则关闭 MAX98357A stream。
6. 失败降级
SD 未挂载、PCM 缺失、I2S 写失败只终止当前句并记录 esp_err_t,不影响 UI 和运动记录。主动播报任务创建失败时 Xiaozhi 对话仍可用。音频内存不足时不扩大队列或挤占 DMA 区,而是让本轮音频进入错误状态。
7. 面试追问
为什么不用 WAV? WAV 头对裸片段拼接没有价值,且每段都要处理格式;固定 PCM 使播放路径只有读文件和 DMA 写入。
为什么上行不用 32 bit PCM? 麦克风 I2S 为 32 bit slot,但协议与 OPUS 处理统一为 PCM16 mono,减少网络带宽和编码输入量。
如何验证音频崩溃根因? 保留完整 backtrace,先区分任务栈、decoder 内存和 packet 格式;本项目通过切换源码 libopus、确认 samples=960 连续播放,证明不是 WebSocket 头或封装层问题。
1 |



