diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告

数字人 RTC 模式音频卡顿根因定位。通过 4 类 ESP_LOGW 埋点采集运行时
数据,对照表格判定根因,输出 Phase 9 实施分支决策。

埋点实现(main/application.cc,PHASE8_DIAG_ENABLE 宏开关,关闭后零开销):
- DIAG-1 queue 深度:3 处(出队 + WebSocket 入队 + RTC 入队),50ms 节流
- DIAG-2 codec->OutputData 写入耗时:>15ms 阈值告警
- DIAG-3 WiFi RSSI:OnClockTimer 1Hz
- DIAG-4 heap 快照 + 碎片率:OnClockTimer 1Hz

实测结论(见 DIAG_REPORT.md):用户感知卡顿 = 两个独立根因
- A. 开机播报阶段 ③' codec init 时序缺陷(ES7210 I2C 失败 +
  126 次 write_slow 集中在 2-13s)
- B. RTC 对话阶段 ⑤ Opus/WebSocket 应用层帧到达抖动
  (queue 突发堆积 19 + queue=0 出现 58 次,但 codec 写入 0 次 slow)

完全排除:① CPU 争抢、② PSRAM 带宽、④ WiFi 丢包(RSSI -24~-33dBm
极强)、⑥ 内存碎片(heap 全程稳定)

Phase 9 推荐分支 B'(双线修复,原 A/C 的 EAF 方案不适用):
- 9.1 应用层 jitter buffer(fill-threshold + drain)—— 解 B
- 9.2 开机 codec init 时序修复(ES7210 reset + ready 等待)—— 解 A
- 估时 1 天

ROADMAP 同步:Phase 7 矫正为 battery_psm(实际状态)、Phase 8 新增
诊断、Phase 9 占位待 Phase 8 决策、原"集成测试"挪到 Phase 10。
新增 .planning/STATE.md 记录 roadmap evolution。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-05-15 11:40:42 +08:00
parent 4b3206dca3
commit 3dc6cadf49
7 changed files with 2608 additions and 7 deletions

42
.planning/STATE.md Normal file
View File

@ -0,0 +1,42 @@
# STATE — 项目状态追踪
> 跨 session 的项目状态、roadmap 演化、关键决策记录。本文件用于让新 session 快速恢复项目上下文。
## Current Milestone
- **Milestone**: `digital_human_rtc` (数字人 RTC 项目)
- **Branch**: `Rtc_AIavatar`
- **ROADMAP**: `.planning/milestones/digital_human_rtc/ROADMAP.md`
- **总阶段数**: 10
- **当前进度**: Phase 1-6 完成Phase 7 (battery_psm) 进行中
## Accumulated Context
### Roadmap Evolution
记录 roadmap 在项目过程中的演化(按时间倒序,最新在前):
- **2026-05-15** — Phase 7 ROADMAP 同步矫正:原 Phase 7 "集成测试 + 推送" 因发现文件系统已有 `phase_07_battery_psm`(电量保护重构)而被重编号为 Phase 10。Phase 7 在 ROADMAP 中改写为指向 `phase_07_battery_psm/README.md`
- **2026-05-15** — Phase 8 added: 数字人 RTC 音频卡顿根因诊断。通过 4 类 ESP_LOGW 埋点audio queue 深度 / PCM write 耗时 / WiFi RSSI / heap定位卡顿真因CPU/PSRAM/DMA/WiFi/Opus/碎片 6 选 1+ 组合),产出 DIAG_REPORT.md 决定 Phase 9 实施策略。
- **2026-05-15** — Phase 9 added (占位): 音频卡顿实施优化。具体方案待 Phase 8 数据决策,分支预案 A=EAF 旁路 / B=WiFi 扩容 / C=完整切 EAF+资源再分配 / D=DMA 排查 已列入 ROADMAP。
### Key Technical Decisions
- **方案 A vs 方案 B 路线分歧已用 Phase 8 数据驱动决策化解**
- 方案 A = `eaf_dec_*` 旁路替换 lv_gif保留 LVGL
- 方案 B = 数字人模式完整切 esp_emote_gfx弃用 LVGL
- 两者选择不靠拍脑袋,靠 Phase 8 诊断报告
- **EAF 集成边界确认**
- 数字人模式 LVGL 实际范围只有 `main/dzbj/ai_chat_ui.c` (458 行) + `main/display/lcd_display.cc` 数字人分支 (~300 行)
- 不涉及任何 `ui/screens/` 下 SquareLine 界面(属吧唧模式,`CONFIG_BAJI_BADGE_MODE` 编译排除)
- `ui_ScreenUpdate` 已确认是吧唧模式 BLE 收图 UI**非 OTA**,不影响数字人模式
- **方案 B 完整切若启用,资源账本**
- 释放:~40KB DRAM + ~80KB PSRAMLVGL 框架本体)
- 投入WiFi RX 缓冲扩容 ~15KB DRAM + Opus jitter buffer ~10KB PSRAM + RTC SDK jitter ~40KB PSRAM
- 净结余:~20KB DRAM + ~10KB PSRAM 仍可备用
### Open Risks (Phase 9 实施时验证)
- `gfx_label` 是否支持中文自动换行 + 双行居中(方案 B/C 阻塞点)
- `font_puhui_20_4.c`8.5MB LVGL bitmap font能否被 EAF 直接复用
- `cst816s` 触摸路径在弃用 LVGL 后如何接驳(需确认数字人模式是否需要触摸交互)

View File

@ -1,6 +1,6 @@
# ROADMAP — 数字人 RTC 项目
7 个阶段,按依赖关系串行。每个阶段产生原子 commit可独立 revert。
10 个阶段,按依赖关系串行。每个阶段产生原子 commit可独立 revert。
## 阶段总览
@ -14,7 +14,13 @@ Phase 2 (分区表调整) ──┘ │
Phase 6 (RTC 空闲超时联动)
Phase 7 (集成测试 + 推送)
Phase 7 (电量保护 + 低功耗重构)
Phase 8 (音频卡顿根因诊断)
Phase 9 (音频卡顿实施优化 - 待定)
Phase 10 (集成测试 + 推送)
```
---
@ -341,7 +347,61 @@ static const emotion_gif_map_t emotion_gif_table[] = {
---
## Phase 7: 集成测试 + 推送
## Phase 7: 电量保护 + 低功耗管理重构
**目标**:把开机电量保护异步化 + 屏幕低电 UI + PowerSaveTimer 状态机重写 + esp_pm_configure 收口受守卫保护,重构成连贯系统而非局部打补丁。
**详细规格**:见 [phases/phase_07_battery_psm/README.md](phases/phase_07_battery_psm/README.md)
**完成标志**
- ✅ 开机不再被电池采样 6 秒阻塞
- ✅ 屏幕分级低电 UI 提示(>25% / 15-25% / <15% / <5%
- ✅ PowerSaveTimer `in_sleep_mode_` 状态机无边角
- ✅ esp_pm_configure 调用统一收口到 callback 内部
**产出 commit**`refactor(power): Phase 7 - 电量保护异步化 + 低功耗状态机重写`
---
## Phase 8: 数字人 RTC 音频卡顿根因诊断
**目标**:通过 4 类 ESP_LOGW 日志埋点采集运行时数据,定位 RTC 音频卡顿真实根因CPU 争抢 / PSRAM 带宽 / DMA / WiFi / Opus 抖动 / 内存碎片),让数据驱动 Phase 9 的实施策略决策。
**详细规格**:见 [phases/phase_08_audio_glitch_diag/README.md](phases/phase_08_audio_glitch_diag/README.md)
**完成标志**
- ✅ 4 处日志埋点编译通过并正常输出
- ✅ 实际复现一次卡顿,采集到包含卡顿瞬间的日志
- ✅ 产出 `DIAG_REPORT.md` 明确根因判定
- ✅ 给出 Phase 9 实施分支推荐A/B/C/D 之一)
**产出 commit**`diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告`
---
## Phase 9: 音频卡顿实施优化(待 Phase 8 数据决策)
**目标**:根据 Phase 8 `DIAG_REPORT.md` 的根因判定,按预案分支实施优化。**具体方案在 Phase 8 完成后细化为 PLAN.md**。
**分支预案**
| 分支 | 触发根因 | 实施动作 | 预估工时 |
|---|---|---|---|
| **A** | 仅 CPU 争抢 | `eaf_dec_*` 解码器旁路替换 `lv_gif`,保留 LVGL 框架 | 1-2 天 |
| **B** | 仅 WiFi/网络 | WiFi 缓冲扩容STATIC_RX 10→16、DYN_RX/TX 32→48、RX_BA_WIN 6→16| 0.5 天 |
| **C** | 组合 ①+④⑤ | 数字人模式完整切 EAF`CONFIG_BAJI_BADGE_MODE=n` 分支弃用 LVGL+ 释放的 ~40KB DRAM + ~80KB PSRAM 投到 WiFi/Opus/RTC jitter buffer 扩容 | 3-5 天 |
| **D** | DMA/I2S | 取消 EAF 方案,转 DMA 路径排查 | 视具体问题 |
**完成标志**
- ✅ 选定分支落地代码 + 编译通过
- ✅ 卡顿复现场景下听感主观验证:抖动消失或显著降低
- ✅ Phase 8 埋点指标改善(`queue` 抖动、`write_slow` 频率均下降)
- ✅ 内存/CPU 监控 30 分钟稳定
**产出 commit**:(按选定分支命名)`perf(rtc-only): Phase 9 - {A/B/C/D 描述}`
---
## Phase 10: 集成测试 + 推送
**目标**:端到端验证 MILESTONE.md 第 6 节全部验收项,推送到 gitea + GitHub。
@ -359,6 +419,7 @@ static const emotion_gif_map_t emotion_gif_table[] = {
**完成标志**
- ✅ MILESTONE.md 第 6 节成功标准全部 ✓
- ✅ Phase 8/9 音频卡顿问题已解决
- ✅ gitea + GitHub 远程已同步
- ✅ 文档更新完成
@ -373,9 +434,12 @@ static const emotion_gif_map_t emotion_gif_table[] = {
- Phase 4/5 依赖 Phase 1dzbj 模块清理完成)+ Phase 3GIF 资源就位)
- Phase 4 ⊥ Phase 5情绪映射和字幕显示独立可并行
- Phase 6 依赖 Phase 1清理 sleep_mgr 调用点)
- Phase 7 必须最后
- Phase 7 依赖 Phase 6PowerSaveTimer 状态机重写需 Phase 6 守卫到位)
- **Phase 8 依赖 Phase 6卡顿症状在 Phase 6 收尾发现,需要 RTC 链路稳定)**
- **Phase 9 依赖 Phase 8实施策略由诊断报告决定**
- Phase 10 必须最后(依赖 Phase 9 卡顿解决)
**建议串行执行顺序**1 → 2 → 3 → 4 → 5 → 6 → 7最稳
**建议串行执行顺序**1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10
---
@ -388,5 +452,8 @@ static const emotion_gif_map_t emotion_gif_table[] = {
| Phase 3 | ✅ 完成commit `7d1c7dc` |
| Phase 4 | ✅ 完成commit `497c1b4` |
| Phase 5 | ✅ 完成commit `f2be992` |
| Phase 6 | 🔄 进行中B+C 双源 + 软退房 + Light Sleep 防护,最新方案见 PLAN 头部"实施变更记录" |
| Phase 7 | ⏳ 待启动 |
| Phase 6 | ✅ 完成commit `b8a5fe9` + `4b7b194` 收尾) |
| Phase 7 | 🔄 进行中([phase_07_battery_psm](phases/phase_07_battery_psm/README.md) 规格已写,实施待启动) |
| **Phase 8** | **⏳ 待启动(音频卡顿诊断埋点,新增)** |
| **Phase 9** | **⏸️ 阻塞中(待 Phase 8 数据,分支预案 A/B/C/D 已列)** |
| Phase 10 | ⏳ 待启动(集成测试 + 推送,原 Phase 7 重编号) |

View File

@ -0,0 +1,138 @@
# Phase 8 — 音频卡顿诊断报告
> 状态:✅ 已完成
> 实测日志:[../../../../05-最新日志.txt](../../../../05-最新日志.txt)1716 行777 行 DIAG
---
## 1. 测试环境
| 项 | 值 |
|---|---|
| 日期 | 2026-05-15 11:22 |
| 设备 | ESP32-S3-N16R8 (Kapi) |
| 板子 | movecall-moji-esp32s3 |
| 串口 | `/dev/cu.usbmodem834401` |
| RSSI 基线 | -24 ~ -33 dBm极强信号 |
| WiFi | airhub, BW20, ch1 |
| RTC | 火山 RTCVolcEngineRTCLite v1.57.207.001 |
| 测试时长 | ~70 秒(开机 → RTC 对话 ~45 秒) |
| 主观感受 | 开机播报"卡卡在呢"声音抖 + RTC 对话过程中扬声器断续/不连贯 |
---
## 2. 用户主观感知
**用户报告**:两段都有卡顿
- **A. 开机播报阶段**"卡卡在呢"语音听感抖
- **B. RTC 对话阶段**AI 回答声音不连贯/断续
---
## 3. 关键时段数据对照
### 3.1 write_slow 时段分布
| 时段 | write_slow 次数 | 主要场景 |
|---|---|---|
| 0-2s | 0 | 系统启动 |
| **2-13s** | **126** ⚠️ | **开机播报 + standby 播报PlaySound 路径)** |
| 13-21s | 4 | 待机 |
| **21-25s** | **16** ⚠️ | **BOOT 按键 → "卡卡在呢"播报** |
| **25s+ (RTC 对话期)** | **0 ✓** | **codec 写入完全健康** |
**write_slow 耗时分布**50-58ms/帧PCM 帧时长 60ms 的 83-97%),最大 102ms 单帧
### 3.2 queue 深度时段分布
| 时段 | 平均深度 | 最大 | queue=0 次数 | queue≥5 次数 |
|---|---|---|---|---|
| 0-5s 开机 | 28.4 | 50 | 0 | 29 |
| 5-13s 播报 | 21.2 | 48 | 2 | 31 |
| 21-25s 连接 | 8.0 | 16 | 1 | 6 |
| **25-40s 对话** | **2.4** | 7 | 7 | 21 |
| **40s+ 对话** | **2.7** | **19** ⚠️ | **58** ⚠️ | 62 |
### 3.3 关键突发事件40s+ 对话期)
```
ts= 44979ms DIAG: queue=12 enq_ws ← WebSocket 入队突发
ts= 45039ms DIAG: queue=17 enq_ws ← 50ms 内入队 +5 帧
ts= 45089ms DIAG: queue=19 enq_ws ← 100ms 内堆积顶峰
ts= 45139ms DIAG: queue=17 deq ← 出队开始消化
ts= 45199ms DIAG: queue=19 enq_ws ← 又一波入队
ts= 45249ms DIAG: queue=18 deq
ts= 45309ms DIAG: queue=16 deq ← 消化中
ts= 45369ms DIAG: queue=17 deq
ts= 45429ms DIAG: queue=15 deq
ts= 45479ms DIAG: queue=15 enq_ws
...
(在此期间无任何 write_slowcodec 写入正常)
```
### 3.4 ES7210 初始化错误(影响段 A
```
I (2520) ES8311: Work in Slave mode
E (2690) I2C_If: Fail to write to dev 80 ← I2C 0x80 写入失败
E (2770) ES7210: Write register fail ← 接连失败
E (2770) ES7210: Open fail ← ES7210 输入侧 Open 失败
I (2009) ES7210: Enable ES7210_INPUT_MIC1 ← 但随后又"成功"
W (2509) DIAG: write_slow 57519us samples=960 ← 同时刻 codec 写入开始持续慢
```
**推测**ES7210 I2C 初始化报错后虽然继续,但 codec 整体 ready 状态有缺陷 → 开机播报期间每帧 PCM 写入要等 50-58ms。
### 3.5 RSSI / heap 全程
- RSSI 全程 **-24 ~ -33 dBm**(极强)→ 物理层 WiFi 完全健康
- free_int ~60KB / PSRAM 6.7MB 全程稳定frag 60% 但不上涨 → 无内存碎片问题
---
## 4. 根因判定
| 候选根因 | 成立? | 证据 |
|---|---|---|
| ① CPU 争抢 (LZW vs Opus) | ❌ 排除 | 对话期 0 次 write_slowcodec 时间预算充足 |
| ② PSRAM 带宽争抢 | ❌ 排除 | 同上 |
| ③ I2S DMA 延迟(通用)| ❌ 排除(对话期)| RTC 对话期写入完全 normal |
| **③' codec init 时序缺陷(开机阶段专属)** | **✅ 成立A** | ES7210 I2C 失败 + 2-13s 集中 126 次 write_slow |
| ④ WiFi 丢包 | ❌ 排除 | RSSI -24 ~ -33 dBm 极强 |
| **⑤ Opus/WebSocket 帧到达抖动** | **✅ 成立B** | queue 突发堆积 19 + queue=0 出现 58 次40s+ 对话期) |
| ⑥ 内存碎片 | ❌ 排除 | heap 全程稳定 |
**最终判定****两个独立根因 ③'(开机 codec 时序)+ ⑤(应用层网络抖动)**,分别对应用户主观感知的 A 和 B 段。
---
## 5. Phase 9 分支推荐
> 原 PLAN 预案 A/B/C/D 都不完全匹配。本案需要**双线并行修复**。
### 推荐:分支 B' —— 双线修复(精准化版)
| 子任务 | 解决问题 | 实施 | 估时 |
|---|---|---|---|
| **9.1 应用层 jitter buffer**(主线) | ⑤ Opus 帧到达抖动 → 用户感知 B 段卡顿 | 在 `audio_decode_queue_` 消费侧加 "fill-threshold + drain":队列首次填到 N=4 帧才开始消费;下面阈值若降到 1 帧暂停消费等再次蓄水;上限阈值 12 帧触发丢包/降级(避免无限堆积)| 0.5 天 |
| **9.2 开机 codec init 时序修复** | ③' ES7210 init 失败 → 用户感知 A 段卡顿 | 排查 `BoxAudioCodec::Initialize` 路径①ES7210 reset 时序(拉低/拉高/等待)②`Adev_Codec` open 重试 ③播报前等真正 ready | 0.5 天 |
### 不做的事(基于数据)
- ❌ 不引入 esp_emote_gfxCPU 完全够用,原 A/C 分支无效)
- ❌ 不扩 `CONFIG_ESP_WIFI_*_BUFFER_NUM`(物理层 RX 健康)
- ❌ 不优化 Opus 解码(耗时未浮现成瓶颈)
### 实施估时合计1 天
---
## 6. 附录
- 实测日志:`/Users/rdzleo/Desktop/Baji_Rtc_Toy/05-最新日志.txt`
- 关键代码位置:
- 入队点:[application.cc:351](../../../../main/application.cc#L351) / [:801](../../../../main/application.cc#L801) / [:2899](../../../../main/application.cc#L2899)
- 出队点:[application.cc:2197](../../../../main/application.cc#L2197) 附近
- codec 写入:[application.cc:2290](../../../../main/application.cc#L2290)
- codec init`main/audio_codecs/box_audio_codec.cc`(待 Phase 9 改)
- PHASE8_DIAG_ENABLE 关闭:`#define PHASE8_DIAG_ENABLE 0`[main/application.cc](../../../../main/application.cc) 顶部)

View File

@ -0,0 +1,367 @@
# Phase 8 PLAN — 数字人 RTC 音频卡顿根因诊断
> 里程碑: `digital_human_rtc`
> 阶段目标: 通过 4 类 ESP_LOGW 埋点,定位 RTC 音频卡顿真因6 候选根因 → 1+ 组合),产出 DIAG_REPORT.md 决定 Phase 9 实施分支A/B/C/D
> 性质: **诊断 phase零业务逻辑改动**,可一键 revert。
---
## 0. 调研结论
### 0.1 关键代码位置
| 位置 | 用途 | 埋点策略 |
|---|---|---|
| [main/application.cc:339](../../../../main/application.cc#L339) | 第一处 `audio_decode_queue_.emplace_back(opus)` 入队 | 入队后采样队列深度 |
| [main/application.cc:789](../../../../main/application.cc#L789) | RTC 下行数据入队 `audio_decode_queue_.emplace_back(data)` | 入队后采样队列深度 |
| [main/application.cc:2155-2156](../../../../main/application.cc#L2155) | `OnAudioOutput()` 中出队 `pop_front()` | 出队后采样队列深度 |
| [main/application.cc:2258](../../../../main/application.cc#L2258) | `codec->OutputData(pcm)` 唯一 PCM 写入点 | 写入前后包 timer>15ms 告警 |
| [main/application.cc:1957](../../../../main/application.cc#L1957) | `OnClockTimer()` 周期任务(`clock_ticks_++` 节流,已有 `% 10 == 0` 分支 ≈ 1Hz| 1Hz 分支挂 RSSI + heap 采样 |
| [main/audio_codecs/audio_codec.cc:17](../../../../main/audio_codecs/audio_codec.cc#L17) | `AudioCodec::OutputData` → 内部 `Write` | 不动timer 包在调用方 |
### 0.2 已掌握的事实
- `OnAudioOutput``AudioLoop` 中每 ~10ms 循环调用([application.cc:2019](../../../../main/application.cc#L2019)),处理一个完整 PCM 帧(~20ms
- 队列 push/pop 都在 `mutex_` 保护下size() 调用安全
- `background_task_->Schedule()` 异步执行解码 + `codec->OutputData()`Core 1 上跑
- `clock_ticks_` 在 [application.cc:1958](../../../../main/application.cc#L1958) 自增,`% 10 == 0` 分支已存在 → 复用为 1Hz 触发点
- `codec->OutputData` 内部 `Write(data, samples)` 是纯虚 → 不同板子ES7210/ES8311实现不同最好在 application.cc 端测耗时,覆盖所有板子
### 0.3 ESP_LOGW 性能影响评估
- ESP_LOGW 走 UART0/USB CDC取决于 `CONFIG_ESP_CONSOLE_*`),单次输出 ~100-500us
- 高频打印会拖慢观察对象本身 → 加节流50ms / 1Hz / 阈值告警)
- 不需要切到 USB CDC当前项目已用 UART0 输出日志且实测可承受 ESP_LOGI 高频打印(参考 phase_06 `🔍 中止后音频[N]` 等密集 INFO 日志)
---
## 1. 设计方案
### 1.1 埋点 4 处(节流策略)
| 编号 | 位置 | 触发条件 | 输出格式 | 节流 |
|---|---|---|---|---|
| **DIAG-1** | OnAudioOutput 入口 + 入队点 | 队列深度变化 | `ESP_LOGW("DIAG", "queue=%d ts=%lld", size, esp_timer_get_time())` | 50ms 节流last_log_us 全局) |
| **DIAG-2** | `codec->OutputData(pcm)` 前后 | 写入耗时 > 15ms | `ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", cost, pcm.size())` | 阈值告警(>15000us 才打印) |
| **DIAG-3** | OnClockTimer 1Hz 分支 | 1Hz 周期 | `ESP_LOGW("DIAG", "rssi=%d ch=%d", rssi, channel)` | 复用 `clock_ticks_ % 10 == 0` |
| **DIAG-4** | OnClockTimer 1Hz 分支 | 1Hz 周期 | `ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%", ...)` | 同 DIAG-3 |
### 1.2 实现位置统一
所有埋点集中在 [main/application.cc](../../../../main/application.cc)。**不修改 audio_codecs/ 或其他模块**。便于一次性 revert
```cpp
// 顶部加包裹宏(编译期开关)
#ifndef PHASE8_DIAG_ENABLE
#define PHASE8_DIAG_ENABLE 1
#endif
#if PHASE8_DIAG_ENABLE
#define DIAG_LOG(fmt, ...) ESP_LOGW("DIAG", fmt, ##__VA_ARGS__)
static int64_t g_diag_queue_last_us = 0;
#else
#define DIAG_LOG(fmt, ...) ((void)0)
#endif
```
Phase 8 完成后做 Phase 9 时,可以 `#define PHASE8_DIAG_ENABLE 0` 一键关掉所有埋点。
### 1.3 复现卡顿的测试场景
1. **基线对话**5 分钟):连续 RTC 对话,每分钟主动说话 2-3 次
2. **情绪触发**5 分钟):让 AI 说带情绪标签的回复("happy..."、"sad..."),触发 GIF 切换瞬间观察 queue + write_slow
3. **WiFi 抗扰**(可选):手机开热点 + 远离路由器,观察 rssi 下降时 queue 是否变空
4. **长时压测**10 分钟):连续无停顿对话,看是否 free_int / largest_int 持续下降
日志收集:`idf.py monitor 2>&1 | tee phase_08_diag.log`
### 1.4 数据分析方法
筛选 DIAG 日志:`grep "DIAG" phase_08_diag.log > diag_only.log`
按时间序对照表格:
| 时间窗口 | queue 序列 | write_slow 次数 | rssi 趋势 | free_int 趋势 | 推断根因 |
|---|---|---|---|---|---|
| 卡顿前 5s | ? | ? | ? | ? | — |
| 卡顿瞬间 | ? | ? | ? | ? | ?(对照表 1 |
| 卡顿后 5s | ? | ? | ? | ? | — |
参照 [README.md §5 卡顿日志特征对照表](README.md#5-卡顿日志特征对照表) 给出根因判定 + Phase 9 分支推荐。
---
## 2. 任务清单
### 任务 2.1 — 顶部包裹宏 + 全局节流变量
**文件**: `main/application.cc`
**读取参考**:
- 现有 include 顺序(不影响)
- 现有静态全局变量风格(参考 `last_subtitle_emotion` 等)
**改动**(加在 file 顶部 include 之后、其他静态变量附近):
```cpp
// ============================================================
// Phase 8: 音频卡顿诊断埋点(一键开关,关闭后零运行时开销)
// ============================================================
#ifndef PHASE8_DIAG_ENABLE
#define PHASE8_DIAG_ENABLE 1
#endif
#if PHASE8_DIAG_ENABLE
static int64_t g_diag_queue_last_us = 0; // queue 深度日志节流50ms
#endif
```
**验收**:
- `grep "PHASE8_DIAG_ENABLE" main/application.cc` 返回至少 1 处
- 编译通过:`idf.py build`
---
### 任务 2.2 — DIAG-1: queue 深度埋点3 处)
**文件**: `main/application.cc`
**读取参考**:
- 第 339 行 `audio_decode_queue_.emplace_back(std::move(opus))`(入队点 1
- 第 789 行 `audio_decode_queue_.emplace_back(std::move(data))`(入队点 2RTC 下行)
- 第 2156 行 `audio_decode_queue_.pop_front()`(出队点)
**改动**: 在 3 处之后插入节流采样块(共用宏,统一节流):
```cpp
// 在 339 行 emplace_back 之后立即插入
#if PHASE8_DIAG_ENABLE
{
int64_t now_us = esp_timer_get_time();
if (now_us - g_diag_queue_last_us > 50000) { // 50ms 节流
g_diag_queue_last_us = now_us;
ESP_LOGW("DIAG", "queue=%zu enq1", audio_decode_queue_.size());
}
}
#endif
```
入队点 2line 789 后)用 tag `enq2`出队点line 2156 后)用 tag `deq`
**验收**:
- `grep -c 'ESP_LOGW("DIAG", "queue=' main/application.cc` 返回 ≥ 3
- 编译通过
- 烧录后 `idf.py monitor` 能看到 `DIAG: queue=N enqX` / `queue=N deq` 日志
---
### 任务 2.3 — DIAG-2: codec OutputData 写入耗时
**文件**: `main/application.cc`
**读取参考**:
- 第 2258 行 `codec->OutputData(pcm)`
**改动**: 将该行替换为耗时统计块:
```cpp
#if PHASE8_DIAG_ENABLE
int64_t _diag_t = esp_timer_get_time();
codec->OutputData(pcm);
int64_t _diag_cost = esp_timer_get_time() - _diag_t;
if (_diag_cost > 15000) {
ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", _diag_cost, pcm.size());
}
#else
codec->OutputData(pcm);// 直接输出PCM数据
#endif
```
**验收**:
- `grep -n "write_slow" main/application.cc` 返回 1 处(在 codec->OutputData 上下文)
- 编译通过
- 主动制造卡顿(如同时启动多个 RTC GIF 切换)能在日志中看到至少 1 次 `write_slow` 输出
---
### 任务 2.4 — DIAG-3 + DIAG-4: OnClockTimer 1Hz 采样
**文件**: `main/application.cc`
**读取参考**:
- 第 1957-1985 行 `Application::OnClockTimer()` 完整函数
- 现有 `if (clock_ticks_ % 10 == 0)` 分支
- ESP-IDF `esp_wifi_sta_get_ap_info` API 签名(`<esp_wifi.h>`
- `heap_caps_get_free_size` / `heap_caps_get_largest_free_block` API`<esp_heap_caps.h>`
**改动**: 在 OnClockTimer 现有的 `% 10 == 0` 分支末尾追加:
```cpp
#if PHASE8_DIAG_ENABLE
// DIAG-3: WiFi RSSI每 ~1s 一次)
wifi_ap_record_t _diag_ap;
if (esp_wifi_sta_get_ap_info(&_diag_ap) == ESP_OK) {
ESP_LOGW("DIAG", "rssi=%d ch=%d", _diag_ap.rssi, _diag_ap.primary);
}
// DIAG-4: heap snapshot
int _diag_free_int = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int _diag_free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
int _diag_largest_int = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
float _diag_frag = _diag_free_int > 0 ? (1.0f - (float)_diag_largest_int / (float)_diag_free_int) * 100.0f : 0.0f;
ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%",
_diag_free_int, _diag_free_psram, _diag_largest_int, _diag_frag);
#endif
```
确保 [application.cc](../../../../main/application.cc) 顶部 include 已包含:
- `<esp_wifi.h>`(如缺则 add
- `<esp_heap_caps.h>`(如缺则 add
**验收**:
- `grep -c 'ESP_LOGW("DIAG", "rssi=' main/application.cc` = 1
- `grep -c 'ESP_LOGW("DIAG", "free_int=' main/application.cc` = 1
- `grep -nE "esp_wifi.h|esp_heap_caps.h" main/application.cc` 至少能找到 esp_wifi.h
- 编译通过
- 烧录后 1Hz 频率持续输出 `DIAG: rssi=-XX``DIAG: free_int=...` 日志
---
### 任务 2.5 — 编译 + 烧录 + 启动 RTC 对话
**前置**:
- `idf.py set-target esp32s3`(如未设置)
- `idf.py menuconfig` 确认 `CONFIG_LOG_DEFAULT_LEVEL >= 3 (WARN)` 否则 ESP_LOGW 不输出
**步骤**:
```bash
idf.py build flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/phase_08_diag.log
```
启动后:
1. 等待数字人 RTC 连接成功
2. 开始连续对话至少 5 分钟(按 §1.3 测试场景)
3. 至少触发一次明确卡顿(抖动/嗡嗡感)
4. 触发后再保持采样 30 秒
5. Ctrl+] 退出 monitor
**验收**:
- 日志文件 `phase_08_diag.log` 已生成且 > 100KB
- `grep -c "DIAG:" phase_08_diag.log` ≥ 5005 分钟 × 多类日志)
- 日志中至少有 1 处 `write_slow``queue=0` 频次密集出现
---
### 任务 2.6 — 数据分析 + DIAG_REPORT.md
**文件**: 新建 `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/DIAG_REPORT.md`
**结构**:
```markdown
# Phase 8 — 音频卡顿诊断报告
## 1. 测试环境
- 日期 / 固件 commit / 路由器型号 / 房间距离
## 2. 卡顿瞬间日志片段
(卡顿前 5s + 卡顿瞬间 + 卡顿后 5s 的 DIAG 行)
## 3. 指标统计
| 指标 | 卡顿前 60s 平均 | 卡顿瞬间 | 卡顿后 60s 平均 |
| queue | ? | ? | ? |
| write_slow 次数 | ? | ? | ? |
| rssi | ? | ? | ? |
| free_int | ? | ? | ? |
| largest_int / free_int | ? | ? | ? |
## 4. 根因判定
对照 README §5 表格__① / ② / ③ / ④ / ⑤ / ⑥ 中的一个或多个__
## 5. Phase 9 分支推荐
推荐分支__A / B / C / D__
理由:...
## 6. 附录
- 完整日志文件路径
- 分析脚本(可选)
```
**验收**:
- `DIAG_REPORT.md` 存在
- 第 4 节明确指出 ①-⑥ 中的一个或多个
- 第 5 节明确给出 A/B/C/D 之一
- 第 2 节附实际日志片段(不是占位符)
---
### 任务 2.7 — 提交 commit
**前置**: 任务 2.1-2.6 全部验收通过
**改动文件清单**:
- `main/application.cc`(埋点)
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/PLAN.md`(本文件,已存在)
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/DIAG_REPORT.md`
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/phase_08_diag.log`(实测日志)
**commit message**:
```
diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
- 加 4 处 DIAG ESP_LOGW 埋点queue 深度 / write_slow / rssi / heap
- PHASE8_DIAG_ENABLE 一键开关,关闭后零运行时开销
- 实测复现卡顿 + DIAG_REPORT.md 输出根因判定
- Phase 9 实施分支推荐:{A/B/C/D}(待 DIAG_REPORT 填)
```
**验收**:
- `git log -1 --format=%s` 包含 "Phase 8"
- `git show --stat HEAD` 列出预期 4 个文件
---
## 3. 任务顺序
```
2.1 顶部宏 → 2.2 queue 埋点 ─┐
├→ 2.5 编译烧录 → 2.6 DIAG_REPORT → 2.7 commit
2.3 write 埋点 ─┤
2.4 1Hz 采样 ─┘
```
2.2 / 2.3 / 2.4 可并行编辑,但都依赖 2.1 的宏定义。
---
## 4. 风险与回滚
| 风险 | 触发条件 | 缓解 / 回滚 |
|---|---|---|
| ESP_LOGW 自身耗时干扰测量 | 卡顿瞬间日志被自身拖累 | 50ms / 阈值 / 1Hz 已做节流;如仍不准,可改用 ringbuffer + 离线 dump |
| `esp_wifi_sta_get_ap_info` 在 STA 未连接时 ENOENT | RTC 连接前调用 | 已用 `== ESP_OK` 守卫 |
| heap fragmentation 计算有除零风险 | free_int = 0极端情况| 已加 `> 0` 守卫 |
| 卡顿无法复现 | 测试场景不足 | 至少跑完 §1.3 三种场景;若仍不复现,扩展到 30 分钟压测 |
| 编译期开关失效 | `#ifndef` 被覆盖 | 用 `grep "PHASE8_DIAG_ENABLE" main/application.cc` 确认且没有冲突 |
| 一键 revert | Phase 9 完成后需要去除埋点 | 直接 `git revert <Phase8_commit>``#define PHASE8_DIAG_ENABLE 0` |
---
## 5. Phase 8 完成验收清单
- [ ] 任务 2.1-2.7 全部验收
- [ ] `PHASE8_DIAG_ENABLE` 宏一键开关已就位
- [ ] 实测日志 `phase_08_diag.log` 已采集(> 5 分钟 RTC 对话 + 至少 1 次卡顿)
- [ ] `DIAG_REPORT.md` 明确根因判定(①-⑥ 之一或组合)
- [ ] `DIAG_REPORT.md` 明确 Phase 9 实施分支推荐A/B/C/D 之一)
- [ ] commit 推送到 `Rtc_AIavatar` 分支
---
## 6. Phase 8 不做的事
- ❌ 不修改业务逻辑pure observability
- ❌ 不引入 esp_emote_gfx 依赖
- ❌ 不调整 sdkconfigWiFi 缓冲、PSM、jitter buffer 都不动)
- ❌ 不优化 lv_gif / LZW 解码(那是 Phase 9 的事)
- ❌ 不写 audio_codecs/ 层埋点(写在 application.cc 端覆盖所有板子足够)
- ❌ 不删除已有日志DIAG 是新增 tag不冲突

View File

@ -0,0 +1,197 @@
# Phase 8数字人 RTC 音频卡顿根因诊断
**性质**:诊断 phase仅埋点 + 数据采集,零业务逻辑改动)
**预估工时**:半天
**Depends on**Phase 6卡顿症状在 Phase 6 收尾发现)
> ⚠️ Phase 7 在 ROADMAP 旧版中标注为"集成测试 + 推送",实际已被 [phase_07_battery_psm](../phase_07_battery_psm/README.md)(电量保护 + 低功耗管理重构)占用。原"集成测试 + 推送" phase 后挪到 Phase 10。
---
## 1. 背景
Phase 6 收尾时观察到 RTC 数字人对话期间扬声器音频抖动/卡顿。已采取的对策**未能消除**卡顿:
- ✅ LVGL 任务绑 Core 0、音频循环绑 Core 1
- ✅ GIF 定时器周期从 10ms 拉到 20ms
- ✅ GIF 颜色压到 8 色减少 LZW CPU 开销
- ✅ DMA flush 串行化到 `background_task_`
- ✅ HTTPS 播放参数恢复通过 `background_task_->Schedule()` 串行
继续盲投优化(如切换 esp_emote_gfx EAF 框架或 WiFi 缓冲扩容)有 3-5 天工时风险,**方向选错就浪费**。
本 phase 通过**最小日志埋点**采集运行时数据,让根因数据驱动 Phase 9 的实施策略决策。
---
## 2. 目标
通过 4 类 ESP_LOGW 日志埋点采集运行时数据,定位 RTC 音频卡顿的真实主要责任方,从下列候选根因中确认(或确认是组合):
| ID | 候选根因 | 验证手段 |
|---|---|---|
| ① | CPU 争抢LVGL/GIF LZW 解码 vs Opus 解码) | 单帧 GIF 解码耗时 + audio queue 堆积 |
| ② | PSRAM 带宽争抢 | free heap 变化 + cache miss 推断 |
| ③ | I2S DMA flush 延迟 | `codec->Write()` 单次耗时 |
| ④ | WiFi RX 丢包/重传 | RSSI + queue 空缺 |
| ⑤ | Opus 帧到达抖动(网络层) | queue 长期空 + RSSI 正常 |
| ⑥ | 内存碎片导致 malloc 卡顿 | free heap 抖动 + largest free block |
---
## 3. 任务
### 任务 1 — 埋点Opus 解码队列深度
**位置**[main/application.cc](../../../../main/application.cc) 的 `OnAudioOutput` 入队/出队点
**实现**
```cpp
// 每次入队或出队后
if (esp_log_timestamp() - last_log_ms > 50) { // 50ms 节流
last_log_ms = esp_log_timestamp();
ESP_LOGW("DIAG", "queue=%d", audio_decode_queue_.size());
}
```
**目的**:观察 audio queue 在卡顿瞬间是空(网络/抖动还是堆积CPU/带宽)。
---
### 任务 2 — 埋点PCM 写入耗时
**位置**[main/application.cc](../../../../main/application.cc) 调用 `codec->Write(pcm, samples)` 前后
**实现**
```cpp
int64_t t = esp_timer_get_time();
codec->Write(pcm, samples);
int64_t cost = esp_timer_get_time() - t;
if (cost > 15000) { // > 15ms 才告警
ESP_LOGW("DIAG", "write_slow %lldus", cost);
}
```
**目的**:识别 I2S/DMA 路径上的写入阻塞(根因 ③)。
---
### 任务 3 — 埋点WiFi RSSI
**位置**:周期性任务(建议 1Hz可挂在现有 `OnClockTimer`
**实现**
```cpp
wifi_ap_record_t ap;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
ESP_LOGW("DIAG", "rssi=%d", ap.rssi);
}
```
**目的**:判断 WiFi 信号是否参与卡顿(根因 ④⑤)。
---
### 任务 4 — 埋点Internal/PSRAM 可用堆
**位置**:周期性任务(建议 1Hz同 OnClockTimer
**实现**
```cpp
ESP_LOGW("DIAG", "free_int=%d free_psram=%d largest_int=%d",
heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
```
**目的**:判断是否有内存碎片(根因 ⑥)或 PSRAM 带宽压力。
---
### 任务 5 — 触发卡顿采集
1. `idf.py build flash monitor` 烧录埋点版本
2. 启动数字人 RTC 对话至少 **5 分钟**
3. 主动触发数字人 GIF 切换(让 AI 说带情绪标签的话,如"happy你好"、"sad抱歉"等)观察切换瞬间
4. 复现至少一次明确卡顿(抖动/嗡嗡感)
5. 用 `idf.py monitor` 保存日志到文件:`monitor 2>&1 | tee phase_08_diag.log`
---
### 任务 6 — 数据分析报告
在本目录下产出 **`DIAG_REPORT.md`**,包含:
- **卡顿瞬间日志片段**:摘取卡顿前后 5 秒的 DIAG 日志
- **根因判定**:对照「卡顿日志特征对照表」(见 §5明确指认 ①-⑥ 哪一类或组合
- **Phase 9 分支推荐**A / B / C / D 之一(见 §6
- **附录**:完整日志文件或链接
---
## 4. 验收标准
- ✅ 4 处日志埋点编译通过,正常输出 `DIAG` tag 日志
- ✅ 实际复现一次卡顿,采集到包含卡顿瞬间的日志
- ✅ 写出 `DIAG_REPORT.md`,明确判定根因属于 ①-⑥ 哪一类(或组合)
- ✅ 给出 Phase 9 实施分支推荐A/B/C/D 之一)
- ✅ 不引入业务逻辑改动,可一键 revert 埋点代码
---
## 5. 卡顿日志特征对照表
| 日志特征 | 推断根因 | Phase 9 分支 |
|---|---|---|
| `queue=0` 频繁出现 + `rssi < -70` | WiFi 丢包 | B |
| `queue=0` 频繁出现 + `rssi 正常` | Opus 帧抖动 | B |
| `write_slow` 频繁 | DMA/I2S 问题 | D |
| `queue` 堆积(>5+ `write_slow` 偶发 | CPU/带宽争抢 | A 或 C |
| `largest_int` 持续下降 | 内存碎片 | 单独修复 |
| 全都正常但听感差 | 心理学/采样率,与代码无关 | 取消 |
---
## 6. Phase 9 实施分支预案
| 分支 | 根因 | 实施动作 |
|---|---|---|
| **A** | 仅 CPU 争抢 ① | 用 esp_emote_gfx 的 `eaf_dec_*` 解码器替换 `lv_gif`LZW保留 LVGL 框架,旁路渲染到 `lv_canvas`。预估 1-2 天 |
| **B** | 仅 WiFi/网络 ④⑤ | WiFi 缓冲扩容(`CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM` 10→16、`DYNAMIC_RX/TX_BUFFER_NUM` 32→48、`RX_BA_WIN` 6→16。预估 0.5 天 |
| **C** | 组合 ①+④⑤ | 数字人模式完整切 EAF`CONFIG_BAJI_BADGE_MODE=n` 分支):弃用 LVGL重写 `ai_chat_ui.c``gfx_obj/gfx_anim/gfx_label`,释放 ~40KB DRAM + ~80KB PSRAM资源投到 WiFi RX + Opus jitter + RTC SDK jitter 扩容。预估 3-5 天 |
| **D** | DMA/I2S ③ | 取消 EAF 方案,转 DMA 路径排查(与 EAF/WiFi 无关)。预估视具体问题 |
---
## 7. 不在范围
- 不修改业务逻辑(纯埋点)
- 不引入 esp_emote_gfx 依赖
- 不调整 sdkconfigWiFi 缓冲、jitter buffer 都不动)
- 不删除已有日志(埋点是新增)
---
## 8. 风险点
| 风险 | 缓解 |
|---|---|
| ESP_LOGW 走 UART 输出本身有耗时(~100us可能影响时序观察 | 50ms 节流,避免高频打印;必要时切到 USB CDC 高速通道 |
| WiFi 信号采样频率过高拖累性能 | 限制 1Hz 即可,足以观察 RSSI 趋势 |
| 复现卡顿需要特定网络/对话场景 | 至少 5 分钟对话 + 主动触发 GIF 切换 + 让 AI 说情绪标签句 |
| 日志埋点本身改变测量结果(观察者效应) | 必要时改用 ringbuffer + 离线 dump减少在线 IO 开销 |
---
## 9. 产出 commit
```
diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
```
---
## 10. 后续 phase
- **Phase 9**:根据本 phase `DIAG_REPORT.md` 的根因结论,按 §6 分支预案选定实施方案A/B/C/D
- **Phase 10**(原 ROADMAP 中的 Phase 7集成测试 + 推送

View File

@ -49,6 +49,18 @@ extern "C" void ai_chat_resume_animation(void);
// 取消注释下行宏可恢复方案 A 作为兜底(双源刷新)。
// #define PHASE6_ENABLE_AUDIO_FALLBACK
// ============================================================
// Phase 8: 音频卡顿诊断埋点(一键开关,关闭后零运行时开销)
// 完成根因定位后改为 0 或 git revert 即可移除全部埋点。
// ============================================================
#ifndef PHASE8_DIAG_ENABLE
#define PHASE8_DIAG_ENABLE 1
#endif
#if PHASE8_DIAG_ENABLE
#include <esp_heap_caps.h>
static int64_t g_diag_queue_last_us = 0; // queue 深度日志节流50ms
#endif
// 定义设备状态字符串
static const char* const STATE_STRINGS[] = {
@ -787,6 +799,16 @@ void Application::Start() {
std::lock_guard<std::mutex> lock(mutex_);
size_t len = data.size();
audio_decode_queue_.emplace_back(std::move(data));
#if PHASE8_DIAG_ENABLE
// Phase 8 DIAG-1: WebSocket 入队后队列深度50ms 节流)
{
int64_t _diag_now = esp_timer_get_time();
if (_diag_now - g_diag_queue_last_us > 50000) {
g_diag_queue_last_us = _diag_now;
ESP_LOGW("DIAG", "queue=%zu enq_ws", audio_decode_queue_.size());
}
}
#endif
static bool first_enqueue_logged = false;
if (!first_enqueue_logged && len > 0) {
ESP_LOGI(TAG, "收到下行音频首包入队: 字节=%zu", len);
@ -1957,6 +1979,27 @@ void Application::Start() {
void Application::OnClockTimer() {
clock_ticks_++;
#if PHASE8_DIAG_ENABLE
// Phase 8 DIAG-3: WiFi RSSI1Hzclock_timer 周期 1s
{
wifi_ap_record_t _diag_ap;
if (esp_wifi_sta_get_ap_info(&_diag_ap) == ESP_OK) {
ESP_LOGW("DIAG", "rssi=%d ch=%d", _diag_ap.rssi, _diag_ap.primary);
}
}
// Phase 8 DIAG-4: heap 快照 + 碎片率1Hz
{
int _diag_free_int = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int _diag_free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
int _diag_largest_int = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
float _diag_frag = _diag_free_int > 0
? (1.0f - (float)_diag_largest_int / (float)_diag_free_int) * 100.0f
: 0.0f;
ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%",
_diag_free_int, _diag_free_psram, _diag_largest_int, _diag_frag);
}
#endif
// 每10秒打印一次调试信息
if (clock_ticks_ % 10 == 0) {
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
@ -2154,6 +2197,16 @@ void Application::OnAudioOutput() {
auto opus = std::move(audio_decode_queue_.front());
audio_decode_queue_.pop_front();
#if PHASE8_DIAG_ENABLE
// Phase 8 DIAG-1: 出队后队列深度50ms 节流)
{
int64_t _diag_now = esp_timer_get_time();
if (_diag_now - g_diag_queue_last_us > 50000) {
g_diag_queue_last_us = _diag_now;
ESP_LOGW("DIAG", "queue=%zu deq", audio_decode_queue_.size());
}
}
#endif
// 在出队时捕获opus解码标志避免background_task异步执行时标志已变化
// 导致残留的Opus帧被当作PCM播放产生杂音
bool is_opus_frame = opus_playback_active_.load();
@ -2255,7 +2308,17 @@ void Application::OnAudioOutput() {
}
} else {
ESP_LOGD(TAG, "直接输出PCM到编解码器: 样本=%zu", pcm.size());
#if PHASE8_DIAG_ENABLE
// Phase 8 DIAG-2: codec PCM 写入耗时(>15ms 阈值告警)
int64_t _diag_t = esp_timer_get_time();
codec->OutputData(pcm);// 直接输出PCM数据
int64_t _diag_cost = esp_timer_get_time() - _diag_t;
if (_diag_cost > 15000) {
ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", _diag_cost, pcm.size());
}
#else
codec->OutputData(pcm);// 直接输出PCM数据
#endif
#ifdef PHASE6_ENABLE_AUDIO_FALLBACK
if (!pcm.empty()) {
this->last_audible_output_time_ = std::chrono::steady_clock::now();
@ -2827,6 +2890,16 @@ void Application::HttpsPlaybackFromUrl(const std::string& url) {
{
std::lock_guard<std::mutex> lock(app.mutex_);
app.audio_decode_queue_.emplace_back(std::move(opus_frame));
#if PHASE8_DIAG_ENABLE
// Phase 8 DIAG-1: RTC/HTTPS 入队后队列深度50ms 节流)
{
int64_t _diag_now = esp_timer_get_time();
if (_diag_now - g_diag_queue_last_us > 50000) {
g_diag_queue_last_us = _diag_now;
ESP_LOGW("DIAG", "queue=%zu enq_rtc", app.audio_decode_queue_.size());
}
}
#endif
}
enqueued++;