Compare commits
2 Commits
b8a5fe958f
...
244f28a0ab
| Author | SHA1 | Date | |
|---|---|---|---|
| 244f28a0ab | |||
| 4b7b1949d4 |
@ -0,0 +1,97 @@
|
|||||||
|
# Phase 7:电量保护 + 低功耗管理重构
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Phase 6 在调试唤醒杂音过程中,暴露出三个**历史代码的耦合问题**,它们彼此牵连影响 UX:
|
||||||
|
|
||||||
|
1. **开机电量保护**([application.cc:614](../../../../main/application.cc#L614) 原 618-630)
|
||||||
|
- 同步采样 20 × 10 × 10ms = **6 秒阻塞**才能进入开机播报
|
||||||
|
- 电量 ≤ 25% 直接 `SetOutputVolumeRuntime(0)` 静音,没有 UI 提示
|
||||||
|
- 无屏 UI 阶段的遗留设计(防止低电压下功放产生噪声)
|
||||||
|
- **Phase 6 已临时禁用**,恢复开机响应速度
|
||||||
|
|
||||||
|
2. **PowerSaveTimer 在 dialog/connecting 状态错误关闭功放**
|
||||||
|
- PowerSaveCheck 状态机 `in_sleep_mode_` 翻转有边角 bug:WakeUp 重置 ticks 但 `in_sleep_mode_` 残留为 true 的路径
|
||||||
|
- 历史症状:欢迎语期间 PowerSaveTimer 触发 OnEnterSleepMode → `codec->EnableOutput(false)` → 听不到欢迎语
|
||||||
|
- **Phase 6 已加 device_state 守卫拦截**([movecall_moji_esp32s3.cc:259](../../../../main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc#L259)),但只是补丁,根因未除
|
||||||
|
|
||||||
|
3. **PowerSaveCheck callback 外的 esp_pm_configure 不受守卫保护**
|
||||||
|
- [power_save_timer.cc:65](../../../../main/boards/common/power_save_timer.cc) callback 后无条件下发 `light_sleep_enable=true`
|
||||||
|
- 即使守卫拦截关功放,I2C/I2S 总线仍可能因 Light Sleep 被掐
|
||||||
|
- 历史症状:唤醒后 codec 通信失败 / I2S DMA 卡死
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
把上述三块**重构成一个连贯系统**,而非局部打补丁:
|
||||||
|
|
||||||
|
- 异步 + 增量电量监测,移除开机阻塞
|
||||||
|
- 屏幕 UI 低电提示(图标 + 文案),替代粗暴静音
|
||||||
|
- 分级低电策略(>25% 正常 / 15-25% 降音量 / <15% UI 警告 / <5% 强制 idle)
|
||||||
|
- PowerSaveTimer 状态机重写,根本性解决 `in_sleep_mode_` 边角
|
||||||
|
- esp_pm_configure 调用统一收口到 callback 内部,受 device_state 守卫保护
|
||||||
|
|
||||||
|
## 范围(暂定,进入 Phase 7 时细化)
|
||||||
|
|
||||||
|
### 7.1 异步电量监测
|
||||||
|
- 后台 FreeRTOS task 定时(如 5s 一次)ADC 采样,更新 `battery_level_` 原子变量
|
||||||
|
- `GetBatteryLevel()` 立即返回缓存值,开机首次返回 100% 或上次 NVS 持久化值
|
||||||
|
- 开机播报不再被电池采样阻塞
|
||||||
|
|
||||||
|
### 7.2 屏幕低电 UI
|
||||||
|
- 顶部状态栏电量图标(已有 LVGL 框架支持)
|
||||||
|
- ≤15% 弹窗"电量不足,请充电",但**不静音**,让用户主动响应
|
||||||
|
- ≤5% 才强制进入 idle,配合 Phase 6 hibernate 流程退出 RTC 房间
|
||||||
|
|
||||||
|
### 7.3 PowerSaveTimer 状态机重写
|
||||||
|
- 用清晰的 4 态机:ACTIVE / DIMMING / SLEEPING / WAKING
|
||||||
|
- `WakeUp()` 同时清 `ticks_` 和 `in_sleep_mode_`,消除"已睡未标记"路径
|
||||||
|
- `OnEnterSleepMode` 内部统一调用 `esp_pm_configure`,被 device_state 守卫保护
|
||||||
|
- 与 Phase 6 hibernate 状态机协同(不重复进入 sleep)
|
||||||
|
|
||||||
|
### 7.4 PA 启停时机 / 唤醒杂音根治
|
||||||
|
- PowerSaveTimer/hibernate 都不应在 dialog 期间关 codec/PA
|
||||||
|
- 唤醒后 codec EnableOutput → 真实 PCM 到达约有 1 秒空窗,I2S 跑空 DMA → 杂音
|
||||||
|
- 候选方案:
|
||||||
|
- 推迟 EnableOutput(true) 到 OnIncomingAudio 首帧(彻底消除空窗)
|
||||||
|
- GPIO PA 推迟到首帧 PCM 入队(事件驱动,不用 ramp)
|
||||||
|
- 用 codec 软静音但**不启用 DAC ramp**(避免之前 23s 爬升副作用),首帧瞬时解
|
||||||
|
- 多方案对比并实测后再决定
|
||||||
|
|
||||||
|
### 7.5 RTC 抖动缓解(音质优化)
|
||||||
|
- **下行音频编码 G.711A → Opus**:
|
||||||
|
- 当前 G.711A = 64 kbps,对丢包无 FEC 保护
|
||||||
|
- Opus 16 kbps 自带 FEC + DTX,抗丢包/带宽降 4 倍
|
||||||
|
- 需要服务端配合切换编解码器
|
||||||
|
- **Jitter buffer target 调整**:100ms → 200-300ms
|
||||||
|
- 用更多缓冲延迟换抗抖动能力
|
||||||
|
- 实测当前 buffer_ms 经常被自适应拉到 240-440ms,目标 100ms 偏低
|
||||||
|
- **Adaptive jitter buffer**:根据近 10s reor/expand_loss 动态调整 target
|
||||||
|
- 评估指标:reor 降到 < 200,expand_loss 降到 < 5/2 秒为达标
|
||||||
|
|
||||||
|
## 当前临时状态(进入 Phase 7 前)
|
||||||
|
|
||||||
|
| 模块 | 临时方案 | 长期方案 |
|
||||||
|
|---|---|---|
|
||||||
|
| 开机电量保护 | application.cc 注释,直接用 NVS 音量 | Phase 7.1 + 7.2 |
|
||||||
|
| PowerSaveTimer 误关功放 | board.cc OnEnterSleepMode 加 device_state 守卫 | Phase 7.3 |
|
||||||
|
| 唤醒杂音 | 已知短板,~1s 杂音用户可接受 | Phase 7.4 |
|
||||||
|
| 下行音频抖动 | 接受 reor 700-1800 / expand_loss 20-130 的现状 | Phase 7.5 |
|
||||||
|
| hibernate 队列残留 | EnterIdleHibernate 清空 audio_decode_queue_ | 保留 |
|
||||||
|
|
||||||
|
## 输入文档
|
||||||
|
|
||||||
|
- [Phase 6 PLAN.md](../phase_06_idle_hibernate/PLAN.md) - hibernate 流程
|
||||||
|
- [Phase 6 HIBERNATE_REPORT.md](../phase_06_idle_hibernate/HIBERNATE_REPORT.md) - 实施记录
|
||||||
|
- [音频卡顿_全局资源分析.md](../../../../docs/Rtc_AIavatar/音频卡顿_全局资源分析.md)
|
||||||
|
- 本次调试笔记(待补充):唤醒杂音 → soft ramp 副作用 → 回退教训
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
进入 Phase 7 的前置条件:
|
||||||
|
- [ ] Phase 6 hibernate 稳定运行 ≥ 1 周无回归
|
||||||
|
- [ ] 用户体验确认开机/休眠/唤醒流程顺畅
|
||||||
|
- [ ] 决定是否同步实现电量 UI(依赖屏幕设计稿)
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
🟡 **占位中** - 等待 Phase 6 稳定后启动正式规划。
|
||||||
829
docs/Rtc_AIavatar/官方Korvo2方案_对比分析报告.md
Normal file
829
docs/Rtc_AIavatar/官方Korvo2方案_对比分析报告.md
Normal file
@ -0,0 +1,829 @@
|
|||||||
|
# 官方 ESP32-S3-Korvo-2 高质量方案 vs 当前项目 — 对比分析报告
|
||||||
|
|
||||||
|
**生成时间**:2026-05-14
|
||||||
|
**对比对象**:
|
||||||
|
- **官方**:[ConversationalAI-Embedded-Kit-2.0/examples/high_quality_solution/espressif](https://github.com/volcengine/ConversationalAI-Embedded-Kit-2.0/tree/main_Korvo_2/examples/high_quality_solution/espressif)(基于 **ESP-ADF**)
|
||||||
|
- **当前**:`/Users/rdzleo/Desktop/Baji_Rtc_Toy`(基于 **ESP-IDF + esp-codec-dev**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR — 核心结论(先看这个)
|
||||||
|
|
||||||
|
| 痛点 | 根本原因 | 严重度 | 修复路径 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 🔴 **麦克风收音不准 / 不响应** | **AEC/NS/AGC 全关**(`CONFIG_USE_AUDIO_PROCESSOR is not set`),ES7210 4-slot TDM 取 ch0 丢 ref,无回声消除 | **极高** | 启用 ESP-SR AFE 或移植官方 `algorithm_stream` |
|
||||||
|
| 🟡 抖动 / 杂音 | jitter buffer 仅靠 std::deque,无重排/PLC/FEC;audio_loop pri=8 偏高,挤占 WiFi | 中高 | 调 jitter buffer + audio_loop pri 降到 5 |
|
||||||
|
| 🟡 唤醒杂音 ~1s | codec EnableOutput 早于 AI 首帧 PCM ~1s,I2S 跑空 DMA | 中 | Phase 7.4 已规划 |
|
||||||
|
| 🟢 本地音频与 RTC 共用通路 | **当前已经实现兼容**,opus_playback_active_ 标志区分本地/RTC,可正常播放本地音 | 无影响 | 改 G.711A → Opus **完全不影响**本地音播放 |
|
||||||
|
|
||||||
|
**最有价值的发现**:用户反馈"麦克风不响应"的真正原因不是网络或 codec,而是**完全没有启用 AEC**。当扬声器在响时(AI 在说话),麦克风采到的全是回声 → 后端 ASR 把回声当噪音 → STT 不识别用户语音。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、音频管线对比
|
||||||
|
|
||||||
|
### 官方架构(ESP-ADF audio_pipeline)
|
||||||
|
|
||||||
|
```
|
||||||
|
[采集] i2s_stream ─→ algo(AEC+NS+AGC) ─→ rsp(16k→8k) ─→ raw ─→ volc_send_audio(PCM)
|
||||||
|
↑
|
||||||
|
单 I2S 同时读 mic + reference
|
||||||
|
(Korvo-2 ES7210 4ch, input_format="RM")
|
||||||
|
左通道 = ref(DAC回采),右通道 = mic
|
||||||
|
[播放] volc_recv_audio(PCM) ─→ raw ─→ rsp(8k→16k) ─→ i2s_stream
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键证据**(`/tmp/conv_ai_korvo2/examples/high_quality_solution/espressif/main/pipeline.c`):
|
||||||
|
- `pipeline.c:88` `algo_mask = ALGORITHM_STREAM_DEFAULT_MASK | ALGORITHM_STREAM_USE_AGC` (AEC + NS + AGC 全开)
|
||||||
|
- `pipeline.c:32-35` `ALGORITHM_INPUT_FORMAT "RM"`(参考信号在左,麦克风在右)
|
||||||
|
- `pipeline.c:60` `es7210_adc_set_gain(MIC3, 30DB)`
|
||||||
|
- `pipeline.c:118` link tag `{"i2s", "algo", "rsp", "raw"}`
|
||||||
|
|
||||||
|
### 当前架构
|
||||||
|
|
||||||
|
```
|
||||||
|
[采集] codec->Read(samples) ─→ TDM 取 ch0 ─→ Opus编码/PCM/G711A ─→ volc_send_audio
|
||||||
|
↑
|
||||||
|
ch1 直接丢弃,无参考信号
|
||||||
|
[播放] OnIncomingAudio ─→ audio_decode_queue_ ─→ background_task::Decode ─→ codec->OutputData
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键证据**:
|
||||||
|
- `main/application.cc:2354-2359` 简单跳格降采样取 ch0
|
||||||
|
- `main/boards/movecall-moji-esp32s3/config.h:26` `AUDIO_INPUT_REFERENCE=0` ← **参考信号没接**
|
||||||
|
- `sdkconfig` 中 `# CONFIG_USE_AUDIO_PROCESSOR is not set` ← **AFE 完全没启用**
|
||||||
|
- `main/application.cc:2014-2025` AudioLoop 是简单 task 循环,没用 audio_pipeline
|
||||||
|
|
||||||
|
### 差距
|
||||||
|
|
||||||
|
| 维度 | 官方 | 当前 | 影响 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| AEC(回声消除)| ✅ algorithm_stream | ❌ 无 | 扬声器响时 ASR 无法识别用户 |
|
||||||
|
| NS(降噪)| ✅ algorithm_stream + ESP-SR webrtc | ❌ 无 | 环境噪音直接送云 ASR |
|
||||||
|
| AGC(自动增益)| ✅ algorithm_stream | ❌ 无 | 远场/近场音量不均 |
|
||||||
|
| 参考信号 | ✅ ES7210 硬件回采(input_format="RM")| ❌ 没接 | 没法做 AEC |
|
||||||
|
| VAD(语音检测)| ✅ ESP-SR webrtc | ⚠️ Simple VAD(已知不可靠)| 误触发或漏触发 |
|
||||||
|
| 麦克风数 | 2 mic + 1 ref(共 4 通道)| 1 mic | 双麦阵列降噪不可用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、RTC 编解码对比
|
||||||
|
|
||||||
|
### 官方
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 编解码 | **G.711A(PCMA, codec=3)** |
|
||||||
|
| 采样率 | **8 kHz** 单声道 |
|
||||||
|
| 帧长 | **20 ms** = 320 bytes PCM = 160 bytes G711A |
|
||||||
|
| 数据类型 | PCM 上行,SDK 内部编码 G711A |
|
||||||
|
| 引用 | `conv_ai_embedded_kit.c:63` `"codec": 3` |
|
||||||
|
|
||||||
|
### 当前
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 编解码 | **运行时三选一**(OPUS / PCM / G711A) |
|
||||||
|
| 采样率 | 上行 8k PCM 或 16k OPUS;下行 8k PCM 或 16k OPUS |
|
||||||
|
| 帧长 | OPUS=30ms, PCM/G711A=20ms |
|
||||||
|
| 引用 | `main/protocols/volc_rtc_protocol.cc:84` `audio_codec_type 4 = OPUS`;运行时分支 line 223/248/281 |
|
||||||
|
|
||||||
|
### 差距和建议
|
||||||
|
|
||||||
|
**当前实际跑的编解码**(看日志确认):
|
||||||
|
```bash
|
||||||
|
grep "set_audio_codec\|audio_codec_type" 05-最新日志.txt | head -3
|
||||||
|
# 输出 "audio_codec_type 4" → OPUS
|
||||||
|
```
|
||||||
|
|
||||||
|
但日志里 RTC SDK 又通过 `changeCodec` 信令切到 PCMA:
|
||||||
|
```
|
||||||
|
[W] IceMessageProcessor.c:360 TODO: handle signal engineControlMessage
|
||||||
|
content {"type":"changeCodec","body":{"media":"audio","codec":"PCMA"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
所以**实际上行下行都跑 G.711A**(与官方一致),只是设备端配了 OPUS 但服务端强制切回 G711A。
|
||||||
|
|
||||||
|
**对比和升级建议**:
|
||||||
|
|
||||||
|
| 选项 | 上下行编码 | 带宽 | CPU | 抗丢包 | 建议 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 现状 | G.711A 8k | 64 kbps | 几乎零 | 差(无 FEC) | 维持,但调 jitter buffer |
|
||||||
|
| 升级 A | Opus 16k VBR | 16-24 kbps | 中(已用 Opus 解码) | 好(内建 FEC + DTX) | **推荐**,Phase 7.5 |
|
||||||
|
| 升级 B | Opus 8k 窄带 | 8-12 kbps | 中 | 好 | 兼容老服务,但音质更差 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、任务/线程对比
|
||||||
|
|
||||||
|
| 任务 | 当前 | 官方 | 评估 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| audio_loop | pri=**8**, core=1, 栈 12KB | (在 conv_ai_task)pri=5, 不绑核 | **本项目 pri 偏高**,可能挤占 WiFi 中断 |
|
||||||
|
| background_task | pri=**5**, 32KB | — | 已经从 2 提到 5(之前优化)|
|
||||||
|
| main_loop | pri=4, core=0, 12KB | conv_ai_task pri=5 | OK |
|
||||||
|
| VolcRtc SDK 内部 | ThreadPool 自管 | 同 | 一致 |
|
||||||
|
|
||||||
|
**建议**:把 audio_loop pri 从 8 降到 5,与官方一致。WiFi/lwIP 中断在 pri 7+,pri=8 会真的抢占 WiFi。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、本地音频播放兼容性 ⭐⭐⭐
|
||||||
|
|
||||||
|
**这是用户最担心的问题,需要重点说清楚。**
|
||||||
|
|
||||||
|
### 官方实现
|
||||||
|
- ❌ **没有本地音频混音/插播**:`_on_volc_audio_data` 回调直接把远端 PCM 写进 player_pipeline
|
||||||
|
- RTC 通话中**无法**同时播放本地 P3 提示音
|
||||||
|
|
||||||
|
### 当前项目实现(已经做好兼容)
|
||||||
|
|
||||||
|
`audio_decode_queue_` **被本地音和 RTC 下行共用**,通过 `opus_playback_active_` 标志区分:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// application.cc:2167-2173 在 background_task 内
|
||||||
|
bool is_opus_frame = opus_playback_active_.load();
|
||||||
|
background_task_->Schedule([this, codec, opus, is_opus_frame]() {
|
||||||
|
// is_opus_frame=true → PlaySound(P3 文件,Opus 编码)
|
||||||
|
// is_opus_frame=false → RTC 下行(PCM 或 OPUS)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
支持的本地音频流程:
|
||||||
|
```
|
||||||
|
PlaySound(P3_KAKA_KAIJIBOBAO)
|
||||||
|
↓
|
||||||
|
opus_playback_active_=true
|
||||||
|
↓
|
||||||
|
P3 解析 → push Opus 包 → audio_decode_queue_
|
||||||
|
↓
|
||||||
|
background_task: 看到 is_opus_frame=true → 用 opus_decoder_ 解码
|
||||||
|
↓
|
||||||
|
codec->OutputData(pcm) → ES8311 → 扬声器
|
||||||
|
```
|
||||||
|
|
||||||
|
### G.711A → Opus 切换的影响分析
|
||||||
|
|
||||||
|
**结论:不影响本地音频播放**。原因:
|
||||||
|
1. 本地 P3 文件本身就是 **Opus 编码**,与 RTC 下行编码无关
|
||||||
|
2. 切换 RTC 上下行编码只改变 `volc_rtc_create` 的 `audio_codec_type` 参数
|
||||||
|
3. 本地音解码用独立的 `opus_decoder_`(应用层),不走 RTC SDK 内置解码器
|
||||||
|
4. 关键约束:必须保持 `codec->output_sample_rate()`(当前 16kHz)不变,**两者都通过 `output_resampler_` 自动重采样到 16k 输出**
|
||||||
|
|
||||||
|
**唯一需要注意的点**:
|
||||||
|
- 切换 RTC 下行编码时,`SetDecodeSampleRate(servSR, frameDur)` 会修改 `opus_decoder_` 和 `output_resampler_`
|
||||||
|
- 如果 RTC 下行从 PCM 8k 切到 Opus 16k 60ms,**正在播放的本地音也会跟着重新配置 opus_decoder_**
|
||||||
|
- CLAUDE.md 已记录这个陷阱("HTTPS 播放中止后 RTC 音频无声")
|
||||||
|
|
||||||
|
**修复策略**:保持本地音播放期间不触发 RTC 解码参数切换(已通过 `opus_playback_active_` 状态机做到)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、抗杂音 / 抗抖动机制对比
|
||||||
|
|
||||||
|
| 机制 | 官方 | 当前 | 差距 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| AEC(回声消除)| algorithm_stream | ❌ 无 | **关键缺失** |
|
||||||
|
| Jitter buffer 配置 | VolcEngineRTCLite 默认值 | VolcEngineRTCLite 默认值 | 一致(都没显式配) |
|
||||||
|
| FEC/PLC | SDK 内部 | SDK 内部 | 一致 |
|
||||||
|
| 唤醒杂音抑制 | 无特殊处理 | ⚠️ 留 Phase 7.4 | 都是已知短板 |
|
||||||
|
| audio_decode_queue 上限 | — | 无上限(潜在内存增长) | 应加上限 |
|
||||||
|
| TCP 重传调优 | `LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y` | 待确认 | 检查 sdkconfig |
|
||||||
|
|
||||||
|
**Phase 7.5 RTC 抖动缓解策略**(README 已记录):
|
||||||
|
- 下行编码 G.711A → Opus(FEC + DTX)
|
||||||
|
- jitter buffer target 100ms → 200-300ms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、麦克风收音不准 — 详细根因和修复方案
|
||||||
|
|
||||||
|
### 根因链
|
||||||
|
|
||||||
|
```
|
||||||
|
1. AUDIO_INPUT_REFERENCE=0 ← config.h:26
|
||||||
|
2. CONFIG_USE_AUDIO_PROCESSOR not set ← sdkconfig
|
||||||
|
3. ES7210 配置为 4ch TDM 但只用 mic1+mic2
|
||||||
|
4. application.cc:2354-2359 软件层简单 ch0/丢 ch1
|
||||||
|
5. 没有任何 AEC/NS/AGC 处理
|
||||||
|
```
|
||||||
|
|
||||||
|
**实际表现**:
|
||||||
|
- AI 在说话(扬声器响)→ 麦克风采到扬声器声+用户声
|
||||||
|
- 设备端没做 AEC 抵消 → 上行 PCM = 扬声器回声 + 微弱用户声
|
||||||
|
- 服务端 STT 看到的能量主导是 AI 自己的声音
|
||||||
|
- → ASR 把它判定为"AI 自言自语",不触发用户对话识别
|
||||||
|
- 用户感知:"对着麦克风说话没反应"
|
||||||
|
|
||||||
|
### 修复方案 A:启用 ESP-SR AFE(推荐,与本项目代码已支持)
|
||||||
|
|
||||||
|
代码层面已经有 [audio_processor.cc:34-77](main/audio_processing/audio_processor.cc#L34) 完整 AFE 启动逻辑:
|
||||||
|
```cpp
|
||||||
|
afe_config.aec_init = true;
|
||||||
|
afe_config.aec_mode = AEC_MODE_VOIP_HIGH_PERF;
|
||||||
|
afe_config.ns_init = true;
|
||||||
|
afe_config.agc_init = true;
|
||||||
|
afe_config.target_level = -3; // dBFS
|
||||||
|
```
|
||||||
|
|
||||||
|
需要做的事:
|
||||||
|
1. **打开 Kconfig**:`CONFIG_USE_AUDIO_PROCESSOR=y` + `CONFIG_AFE_MIC_NUM=2` + `CONFIG_AFE_INTERFACE_V1=y`
|
||||||
|
2. **打开参考通道**:`config.h: #define AUDIO_INPUT_REFERENCE 1`
|
||||||
|
3. **ES7210 配置参考输入**:把当前的 mic1+mic2 改成 mic+reference
|
||||||
|
4. **硬件层面**:确认 PCB 有从功放输出回采到 ES7210 ref 通道(Korvo-2 标配,自己改的板要确认)
|
||||||
|
|
||||||
|
**预期效果**:
|
||||||
|
- AEC 抑制 25-30 dB 回声
|
||||||
|
- 全双工对话不再"听不见用户说话"
|
||||||
|
- 上行 PCM 干净,ASR 命中率大幅提高
|
||||||
|
|
||||||
|
### 修复方案 B:移植官方 algorithm_stream(重,不推荐)
|
||||||
|
|
||||||
|
代价:要切到 ESP-ADF 框架。但项目已经投入 ESP-IDF + esp-codec-dev 架构,迁移成本极高。**不推荐**。
|
||||||
|
|
||||||
|
### 修复方案 C:服务端 AEC
|
||||||
|
|
||||||
|
让火山服务端做 echo cancellation。需要联系火山技术支持询问是否支持设备无 AEC 的模式。**不可控**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六补充、ES7210 + 硬件 AEC 完整启用方案(2026-05-14 详细补丁)
|
||||||
|
|
||||||
|
### A. 你的硬件 vs 官方 Korvo-2 的关键差异
|
||||||
|
|
||||||
|
经过 **ES7210 datasheet** + **电子吧唧 V1.0 原理图 Sheet 3** + **官方 Korvo-2 V3.1.2 原理图 Sheet 4** + **官方 pipeline.c** + **当前 box_audio_codec.cc** 五方对照(更新 2026-05-14):
|
||||||
|
|
||||||
|
| 维度 | **官方 Korvo-2 V3.1.2** | **你的板子** | 是否一致 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| MIC1 (pin 15/16) | AMIC2 数字麦克风(U23 MSM381A3729) | MIC1 数字麦克风(MSM381A3729) | ✅ 100% 一致 |
|
||||||
|
| MIC2 (pin 19/20) | AMIC1 数字麦克风(U31 MSM381A3729) | MIC2 数字麦克风(MSM381A3729) | ✅ 100% 一致 |
|
||||||
|
| **MIC3 (pin 31/32)** | **DAC 回采(AEC ref)** | **DAC 回采(AEC ref)** | ⭐ **100% 一致** |
|
||||||
|
| MIC4 (pin 27/28) | 未连接 | 未连接 | ✅ 一致 |
|
||||||
|
| AEC ref 电路 R 网络 | R134=10K, R135=20K, **R137=4.3K** | R19=10K, R20=20K, **R22=4.3K** | ✅ **元件值完全一致** |
|
||||||
|
| AEC ref 电路 C 网络 | C93=0.47uF, C95=10nF, C96=2.2nF, C103=0.22uF | C45=0.47uF, C47=10nF, C48=2.2nF, C46=0.22uF | ✅ 完全一致 |
|
||||||
|
| AEC 衰减比 | R137/(R135+R137) = 4.3/24.3 ≈ 17.7% | R22/(R20+R22) = 4.3/24.3 ≈ 17.7% | ✅ 完全相同 |
|
||||||
|
| ES7210 I2C 地址 | 0x40 (AD0=AD1=0) | 0x40 (AD0=AD1=0,R14/R15 拉低) | ✅ 一致 |
|
||||||
|
| ES7210 INT pin 13 | 未引出到 MCU | 未引出到 MCU | ✅ 一致 |
|
||||||
|
| MICBIAS12 滤波 | 1uF + RC | 1uF + RC | ✅ 一致 |
|
||||||
|
|
||||||
|
**核心结论修正**(重要):
|
||||||
|
1. 我之前推断"官方只有 1 个 mic"是**错误**的——官方板有 **AMIC1+AMIC2 两个数字麦克风**(U23 + U31,型号 MSM381A3729H9BPC,与你板子一模一样)
|
||||||
|
2. AEC 反馈电路设计(含 RC 网络元件值)官方和你的板子**100% 相同**
|
||||||
|
3. 你的板子可以理解为"官方 Korvo-2 去掉 Camera/SD/TCA9554/按键阵列后的精简版"
|
||||||
|
|
||||||
|
### B. 官方 ES7210 + I2S 配置(pipeline.c:29-67)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define I2S_SAMPLE_RATE 16000
|
||||||
|
#define ALGORITHM_STREAM_SAMPLE_BIT 32
|
||||||
|
#define CHANNEL_FORMAT I2S_CHANNEL_TYPE_ONLY_LEFT // ⚠️ "ONLY_LEFT" 但实际收两路
|
||||||
|
#define ALGORITHM_INPUT_FORMAT "RM" // Reference 在前,Mic 在后
|
||||||
|
|
||||||
|
es7210_adc_set_gain(ES7210_INPUT_MIC3, GAIN_30DB); // ref 通道增益(弱信号需要拉高)
|
||||||
|
i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(..., 16000, 32bit, READER);
|
||||||
|
i2s_stream_set_channel_type(&i2s_cfg, I2S_CHANNEL_TYPE_ONLY_LEFT);
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点解读**:
|
||||||
|
1. `32 bit` 不是 16 bit — 因为 ES7210 是 24 位 ADC,要用 32 位时隙才能完整接收
|
||||||
|
2. `"ONLY_LEFT"` 实际是从 I2S 标准格式收两路(L=ref, R=mic),然后 algorithm_stream 用 `"RM"` 拆分给 AEC
|
||||||
|
3. **只用 1 个 mic + 1 个 ref(共 2 通道,不开 TDM)**
|
||||||
|
4. `mic_num >= 3` 才会启用 TDM([es7210.c:16](managed_components/espressif__esp_codec_dev/device/es7210/es7210.c#L16) `ENABLE_TDM_MAX_NUM = 3`)
|
||||||
|
|
||||||
|
### C. 你当前的 I2S/ES7210 配置([box_audio_codec.cc:152-189](main/audio_codecs/box_audio_codec.cc#L152))
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2; // 2 个 mic,不开 TDM
|
||||||
|
// ↑ box_audio_codec.cc:75
|
||||||
|
|
||||||
|
i2s_tdm_config_t tdm_cfg = {
|
||||||
|
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, // ⚠️ 16 位
|
||||||
|
.slot_mode = I2S_SLOT_MODE_STEREO,
|
||||||
|
.slot_mask = I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3, // ⚠️ 4 slot 但 ES7210 只输 2 slot
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题诊断**:
|
||||||
|
1. `slot_mask` 配 4 slot 但 ES7210 只用 2 mic → I2S 收到的 slot 2/3 是空(无效数据),驱动里相当于在浪费 DMA
|
||||||
|
2. `data_bit_width = 16BIT` — ES7210 是 24bit ADC,理论上用 32bit 接收信号更纯净(虽然 16bit 也能工作)
|
||||||
|
3. 没接 ref → 完全不可能做 AEC
|
||||||
|
|
||||||
|
### D. 启用 AEC 的精确代码补丁(推荐方案:2mic + 1ref)
|
||||||
|
|
||||||
|
利用你硬件**2 个 mic + 1 个 ref**的优势,比官方还强:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// box_audio_codec.cc:75 — 启用 MIC3 作 AEC 参考通道
|
||||||
|
- es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2;
|
||||||
|
+ es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2 | ES7120_SEL_MIC3;
|
||||||
|
// ^^^^^^^^^^^^^^^^^
|
||||||
|
// 这一开就触发 TDM (mic_num=3 >= 3)
|
||||||
|
// TDM 输出顺序:slot0=MIC1, slot1=MIC2, slot2=MIC3
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// box_audio_codec.cc:164 — I2S 改 3 slot 接收
|
||||||
|
- .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3),
|
||||||
|
+ .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2),
|
||||||
|
|
||||||
|
// box_audio_codec.cc:172 — total_slot 设为 3
|
||||||
|
- .total_slot = I2S_TDM_AUTO_SLOT_NUM
|
||||||
|
+ .total_slot = 3
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// box_audio_codec.cc:16 — 输入通道数 1 改 3(PCM 缓冲变大 3 倍)
|
||||||
|
- input_channels_ = input_reference_ ? 2 : 1;
|
||||||
|
+ input_channels_ = input_reference_ ? 3 : 2; // 2 mic + 1 ref
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// movecall-moji-esp32s3/config.h:26
|
||||||
|
- #define AUDIO_INPUT_REFERENCE 0
|
||||||
|
+ #define AUDIO_INPUT_REFERENCE 1
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# sdkconfig
|
||||||
|
+ CONFIG_USE_AUDIO_PROCESSOR=y
|
||||||
|
+ CONFIG_AFE_MIC_NUM=2
|
||||||
|
+ CONFIG_AFE_INTERFACE_V1=y
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// audio_processor.cc — AFE 配置升级到 2mic+1ref(具体 API 看 ESP-SR 头文件)
|
||||||
|
afe_config.pcm_config.total_ch_num = 3;
|
||||||
|
afe_config.pcm_config.mic_num = 2;
|
||||||
|
afe_config.pcm_config.ref_num = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// application.cc:2354-2359 — 重写 mic 数据提取逻辑(3 通道 16bit)
|
||||||
|
// 原代码:从 stereo 取 ch0
|
||||||
|
// 新代码:3 通道按 slot0/1/2 拆分,前 2 个给 AFE 当 mic,第 3 个当 ref
|
||||||
|
// 实际上 AFE 启用后这段会被替换掉,PCM 直接喂给 audio_processor_.Feed()
|
||||||
|
```
|
||||||
|
|
||||||
|
### E. 备选方案:1mic + 1ref(最贴近官方,最稳)
|
||||||
|
|
||||||
|
如果嫌 3 通道难调,可以先按官方 1mic+1ref 跑通:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC3; // 不触发 TDM(mic_num=2)
|
||||||
|
// I2S 改成 standard mode(不用 TDM),CHANNEL_FORMAT = STEREO
|
||||||
|
// data_bit_width 改 32BIT(与官方一致)
|
||||||
|
// AFE 配置:mic_num=1, ref_num=1, total_ch_num=2
|
||||||
|
```
|
||||||
|
|
||||||
|
**代价**:丢一个 mic 的拾音能力,但 AEC 功能完整。
|
||||||
|
|
||||||
|
### F. I2C 总线共用风险(顺带提醒)
|
||||||
|
|
||||||
|
[Sheet 2] GPIO17=ES_I2C_SDA=TP_SDA,GPIO18=ES_I2C_CLK=TP_SCL — **codec ES8311 (0x18) / ES7210 (0x40) / 触摸 CST816S 三个芯片挂在同一条 I2C 总线**。
|
||||||
|
|
||||||
|
- 当前没问题(之前调试过没出现 I2C 冲突)
|
||||||
|
- 但触摸高频事件 + codec init 同时进行时偶发 `Fail to write to dev 30/80` 可能与此有关(已在之前讨论过)
|
||||||
|
- ESP-IDF I2C master 驱动自带 mutex,软件层不需要额外处理
|
||||||
|
- 长期 Phase 7 可考虑把 codec 移到 I2C1,触摸保留 I2C0,物理隔离
|
||||||
|
|
||||||
|
### G. 实施步骤建议(先稳后猛)
|
||||||
|
|
||||||
|
1. **第 1 步**(30 分钟):先用方案 E(1mic+1ref)启用 AEC,编译烧录测试,确认 AEC 实际生效
|
||||||
|
2. **第 2 步**(确认生效后,~1 小时):升级到方案 D(2mic+1ref),享受双麦阵列收益
|
||||||
|
3. **第 3 步**(~2 小时):用真实场景测试 STT 命中率,对比启用 AFE 前后
|
||||||
|
4. **第 4 步**(持续):Phase 7 进一步打磨 jitter buffer / Opus 切换
|
||||||
|
|
||||||
|
### H. 风险和回退
|
||||||
|
|
||||||
|
| 风险 | 概率 | 缓解 |
|
||||||
|
|---|---|---|
|
||||||
|
| AFE 占用 25KB SRAM | 中 | 当前 free heap ~7MB,ESP32-S3 N16R8 充足 |
|
||||||
|
| AFE 任务在 Core 1 与 audio_loop 抢核 | 中 | 实测 CPU 占用,调整优先级 |
|
||||||
|
| ES7210 TDM 时钟参数变化导致 ES8311 同步问题 | 低 | TX/RX I2S 是分开 channel,独立时钟 |
|
||||||
|
| 改后开机崩溃 | 低 | 一行 `CONFIG_USE_AUDIO_PROCESSOR=n` 立即回退 |
|
||||||
|
| 服务端 ASR 还是不响应 | 低 | 如启用 AFE 仍不识别,看上行 PCM 能量是否正常,可能是火山 bot 配置问题 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六补充·二、GPIO 引脚映射全对照(2026-05-14 新增)
|
||||||
|
|
||||||
|
拿到官方 Korvo-2 V3.1.2 完整原理图后,**逐针对照**:
|
||||||
|
|
||||||
|
### 音频相关引脚(决定性的好消息)⭐
|
||||||
|
|
||||||
|
| 信号 | **官方 Korvo-2 V3.1.2** | **你的板子** | 是否相同 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Codec_I2S0_MCLK / ES7210_MCLK | **GPIO16** | **GPIO16** | ✅ |
|
||||||
|
| Codec_I2S0_SCLK / ES7210_SCLK | **GPIO9** | **GPIO9** | ✅ |
|
||||||
|
| Codec_I2S0_LRCK / ES7210_LRCK | **GPIO45** | **GPIO45** | ✅ |
|
||||||
|
| Codec_I2S0_DSDIN(DAC 数据→ES8311)| **GPIO8** | **GPIO8** | ✅ |
|
||||||
|
| ES7210_SDOUT(ADC 数据→ESP32)| **GPIO10** | **GPIO10** | ✅ |
|
||||||
|
| ES_I2C_SDA | **GPIO17** | **GPIO17** | ✅ |
|
||||||
|
| ES_I2C_CLK | **GPIO18** | **GPIO18** | ✅ |
|
||||||
|
| PA_CTRL | **GPIO48** | **GPIO48** | ✅ |
|
||||||
|
|
||||||
|
**所有 8 个音频信号 GPIO 100% 完全一致!** 这意味着官方 `pipeline.c` 的所有音频配置可以**零修改**搬过来。
|
||||||
|
|
||||||
|
### 非音频引脚差异(不影响 AEC,仅供参考)
|
||||||
|
|
||||||
|
| 功能 | 官方 Korvo-2 | 你的板子 | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 按键 | 6 键 ADC 阵列(IO5 上 BT_ARRAY_ADC,阈值 0.38/0.82/1.11/1.65/1.98/2.41V)| 物理按键 BOOT(IO0) + KEY2(IO4) | 你的更简单 |
|
||||||
|
| LCD | FPC 16Pin,独立 SPI(DC/CLK/SDA),由 TCA9554 控制 RST/CS | QSPI 1.85寸触摸屏(IO4-14/21)+ CST816S 触摸 | 你的更高级 |
|
||||||
|
| 摄像头 | DVP 24Pin Camera(IO3/11-14/21/38-41)| 无 | 你的精简掉了 |
|
||||||
|
| SD 卡 | 4-bit SDIO(IO7/15/IO38-41)| 无 | 你的精简掉了 |
|
||||||
|
| I/O Expander | **TCA9554** @ I2C 0x38(控制 LCD/Camera/LED 4 路)| 无(直接 GPIO 驱动)| 你的更直接 |
|
||||||
|
| RGB LED | LP5562/WS2812 阵列 6 颗 | 无 | 你的精简掉了 |
|
||||||
|
| 电量检测 | BAT_MEAS_ADC = GPIO6 | BAT_ADC = GPIO3 | **不同!** 你的代码已正确处理 |
|
||||||
|
| Auto Download | DTR/RTS + Q9/Q11/Q13 自动复位 | SW2/SW3 手动按 BOOT/RESET | 你的更简单 |
|
||||||
|
|
||||||
|
### I2C 总线对比
|
||||||
|
|
||||||
|
**官方 Korvo-2 V3.1.2**(Sheet 3):
|
||||||
|
- ES7210 (0x40) + ES8311 (0x18) + **TCA9554 (0x38)** + Camera SCCB (0x42) + LCD Touch
|
||||||
|
- **5+ 个设备**挂在同一条 I2C 总线(IO17/IO18)
|
||||||
|
|
||||||
|
**你的板子**(Sheet 2):
|
||||||
|
- ES7210 (0x40) + ES8311 (0x18) + CST816S (0x15)
|
||||||
|
- **3 个设备**挂在同一条 I2C 总线(IO17/IO18)
|
||||||
|
|
||||||
|
**结论**:你的 I2C 总线比官方更清洁,**总线竞争风险更低**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六补充·三、关于"AEC 是否真的能解决麦克风不响应问题"的深度验证
|
||||||
|
|
||||||
|
拿到官方原理图后,可以**100% 确定**以下事实:
|
||||||
|
|
||||||
|
### 强证据(确定的事实)
|
||||||
|
|
||||||
|
1. ✅ 你的板子 = 官方 Korvo-2 精简版(去掉 Camera/SD/IO Expander/按键阵列/RGB LED)
|
||||||
|
2. ✅ AEC 反馈电路 100% 等效(元件值完全相同)
|
||||||
|
3. ✅ 所有音频 GPIO 完全一致
|
||||||
|
4. ✅ ES7210 配置(4ch ADC + MIC3 用于 ref)完全一致
|
||||||
|
5. ✅ 官方 esp-adf 方案在这套硬件上能正常做 AEC(官方出货证明)
|
||||||
|
|
||||||
|
### 强推断(高置信度)
|
||||||
|
|
||||||
|
1. 启用 `CONFIG_USE_AUDIO_PROCESSOR=y` + 配置 ES7210 MIC3 + 修改 AFE 的 input_format
|
||||||
|
2. 你的 ESP32-S3 + audio_processor.cc 里 ESP-SR AFE 代码([audio_processor.cc:34-77](main/audio_processing/audio_processor.cc#L34))能直接配出和官方 algorithm_stream 等效的 AEC 效果
|
||||||
|
3. 大概率能彻底消除"AI 说话时麦克风识别不到"的痛点
|
||||||
|
|
||||||
|
### 可能的失败点(需要实测确认)
|
||||||
|
|
||||||
|
1. ⚠️ ESP-SR AFE 的 input_format 字符串可能与 esp-adf algorithm_stream 不完全兼容(API 不同)
|
||||||
|
- **缓解**:看 ESP-SR `afe_config_t.pcm_config` 怎么配 mic_num/ref_num
|
||||||
|
2. ⚠️ ES7210 在你板子的 mic_selected 顺序:MIC1 在 slot0 还是 slot2?需要日志确认
|
||||||
|
- **缓解**:先用方案 E(1mic+1ref,简化)跑通,确认 slot 顺序
|
||||||
|
3. ⚠️ MIC3 的 reference 信号增益(GAIN_30dB vs default)可能影响 AEC 收敛
|
||||||
|
- **缓解**:照官方写 `es7210_adc_set_gain(ES7210_INPUT_MIC3, GAIN_30DB)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六补充·四、最终精准代码补丁(基于官方原理图确认后)
|
||||||
|
|
||||||
|
**完全跟随官方 Korvo-2 方案,不冒险**:
|
||||||
|
|
||||||
|
### Step 1: ES7210 配置(box_audio_codec.cc)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
// box_audio_codec.cc:75
|
||||||
|
- es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2;
|
||||||
|
+ es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC3;
|
||||||
|
// ^^^^^^^^^^^^^^^^
|
||||||
|
// 按官方做法用 MIC1 + MIC3,2 通道不触发 TDM
|
||||||
|
+ // 配合:MIC3 reference 增益 +30dB(官方 pipeline.c:60)
|
||||||
|
+ es7210_set_mic_gain(...MIC3..., 30); // 具体 API 看驱动
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: I2S 模式(box_audio_codec.cc:152-189)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- // 当前用 i2s_tdm_config_t (4 slot TDM, 16bit)
|
||||||
|
- i2s_tdm_config_t tdm_cfg = {...slot_mask = SLOT0|1|2|3, data_bit_width=16BIT...};
|
||||||
|
- i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg);
|
||||||
|
|
||||||
|
+ // 改用 i2s_std_config_t (2 ch standard, 32bit) — 与官方一致
|
||||||
|
+ i2s_std_config_t std_cfg = {
|
||||||
|
+ .clk_cfg = { .sample_rate_hz = 16000, ... },
|
||||||
|
+ .slot_cfg = {
|
||||||
|
+ .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||||
|
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||||
|
+ .slot_mode = I2S_SLOT_MODE_STEREO,
|
||||||
|
+ .slot_mask = I2S_STD_SLOT_BOTH, // 左右两路:L=MIC1 mic, R=MIC3 ref
|
||||||
|
+ },
|
||||||
|
+ .gpio_cfg = { ... },
|
||||||
|
+ };
|
||||||
|
+ i2s_channel_init_std_mode(rx_handle_, &std_cfg);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: 输入通道数(box_audio_codec.cc:16)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- input_channels_ = input_reference_ ? 2 : 1;
|
||||||
|
+ input_channels_ = 2; // 固定 2 通道(1 mic + 1 ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: 板级开关(movecall-moji-esp32s3/config.h)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- #define AUDIO_INPUT_REFERENCE 0
|
||||||
|
+ #define AUDIO_INPUT_REFERENCE 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: AFE 启用(sdkconfig)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+ CONFIG_USE_AUDIO_PROCESSOR=y
|
||||||
|
+ CONFIG_USE_WAKE_WORD_DETECT=n # 暂不启用唤醒词,专注 AEC
|
||||||
|
+ CONFIG_AFE_MIC_NUM=1 # 1 mic
|
||||||
|
+ CONFIG_AFE_INTERFACE_V1=y
|
||||||
|
+ CONFIG_USE_DEVICE_AEC=y
|
||||||
|
+ CONFIG_USE_DEVICE_NS=y
|
||||||
|
+ CONFIG_USE_DEVICE_AGC=y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: 应用层(application.cc:2354-2400 OnAudioInput)
|
||||||
|
|
||||||
|
ESP-SR AFE 内部会处理 2 通道(L=mic, R=ref)→ 输出 1 通道净化后 PCM。需要:
|
||||||
|
- `audio_processor_.Feed(data)` 喂入 2 通道 16-bit interleaved PCM
|
||||||
|
- `audio_processor_.GetOutput()` 拿到 1 通道净化 PCM
|
||||||
|
- 替换当前 [application.cc:2354-2359](main/application.cc#L2354) 的手工 down-sample 逻辑
|
||||||
|
|
||||||
|
### 改动量统计
|
||||||
|
|
||||||
|
| 文件 | 行数 | 难度 |
|
||||||
|
|---|---|---|
|
||||||
|
| box_audio_codec.cc | ~50 行(I2S 模式改 STD + ES7210 配置)| 中 |
|
||||||
|
| box_audio_codec.h | ~5 行 | 低 |
|
||||||
|
| config.h | 1 行 | 低 |
|
||||||
|
| sdkconfig | 6 行 | 低 |
|
||||||
|
| application.cc | ~30 行(OnAudioInput 重写)| 中 |
|
||||||
|
| audio_processor.cc | 0 行(代码已存在,只需启用)| - |
|
||||||
|
| **合计** | **~92 行** | 实施 2-3 小时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六补充·五、再次确认结论(拿到原理图后)
|
||||||
|
|
||||||
|
| 问题 | 是否能解决 | 置信度 |
|
||||||
|
|---|---|---|
|
||||||
|
| 麦克风识别不准 / 不响应 | ✅ 能(AEC 启用后) | 95% |
|
||||||
|
| 唤醒后欢迎语前杂音 | ⚠️ AEC 不直接解决,需 Phase 7.4 单独处理 | - |
|
||||||
|
| 音频抖动 | ⚠️ AEC 不直接解决,需 Phase 7.5(Opus + jitter buffer)| - |
|
||||||
|
| 网络抖动 | ❌ 设备层无法解决,需运营商/服务端层 | - |
|
||||||
|
|
||||||
|
**最关键的判断**:你的硬件**已经完全就绪**,缺的就是**软件启用 AFE 这一步**。一旦启用,麦克风识别问题应该立即缓解。
|
||||||
|
|
||||||
|
如果想保险起见,先按方案 E(1mic+1ref)跑通验证 AEC 实际生效,再考虑升级到 2mic+1ref 拿双麦阵列收益。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、实施路径建议(按优先级)
|
||||||
|
|
||||||
|
### 立即可做 ⭐⭐⭐
|
||||||
|
|
||||||
|
1. **启用 AFE**(修复麦克风收音)
|
||||||
|
- `CONFIG_USE_AUDIO_PROCESSOR=y`
|
||||||
|
- 测试硬件参考信号是否接入(用万用表测 ES7210 ref pin)
|
||||||
|
- 预计工作量:1-2 天(含硬件确认)
|
||||||
|
|
||||||
|
2. **audio_loop 优先级 8 → 5**(缓解抖动)
|
||||||
|
- 改 [application.cc:638](main/application.cc#L638) 第 3 个参数
|
||||||
|
- 改动量:1 行
|
||||||
|
- 预计工作量:10 分钟
|
||||||
|
|
||||||
|
### Phase 7 短期(README 已规划)
|
||||||
|
|
||||||
|
3. **Jitter buffer target 调到 200-300ms**
|
||||||
|
4. **G.711A → Opus**(如服务端支持)
|
||||||
|
5. **唤醒杂音根治**
|
||||||
|
|
||||||
|
### Phase 7 长期
|
||||||
|
|
||||||
|
6. **重构 PowerSaveTimer 状态机**
|
||||||
|
7. **异步电量监测 + 屏幕 UI**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、风险提示
|
||||||
|
|
||||||
|
| 风险点 | 影响 | 缓解 |
|
||||||
|
|---|---|---|
|
||||||
|
| AFE 占用约 25KB SRAM + 1 个 core | 内存压力 | ESP32-S3-N16R8 有 8MB PSRAM,足够 |
|
||||||
|
| AFE 启用后 audio_processor 任务占 Core 1 | 与 LVGL(Core 0)/audio_loop(Core 1)抢核 | 调度优先级合理,实测会有 5-10% CPU 上升 |
|
||||||
|
| ES7210 参考通道硬件不到位 | AFE 启用但 AEC 无效 | 硬件先确认,必要时改 PCB 飞线 |
|
||||||
|
| 改 RTC 编码需服务端配合 | 服务端可能强制下发 PCMA | 先用 G.711A,jitter buffer 调优后再评估 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、参考材料
|
||||||
|
|
||||||
|
### 官方代码(已 clone 到本地)
|
||||||
|
- `/tmp/conv_ai_korvo2/examples/high_quality_solution/espressif/main/pipeline.c` — 音频管线
|
||||||
|
- `/tmp/conv_ai_korvo2/examples/high_quality_solution/espressif/main/conv_ai_embedded_kit.c` — RTC 控制
|
||||||
|
- `/tmp/conv_ai_korvo2/examples/high_quality_solution/espressif/sdkconfig.defaults.esp32s3` — 板配置
|
||||||
|
- `/tmp/conv_ai_korvo2/volc_conv_ai/src/transports/high_quality/src/volc_rtc.c` — RTC SDK 封装
|
||||||
|
|
||||||
|
### 本项目关键文件
|
||||||
|
- `main/protocols/volc_rtc_protocol.cc` — RTC 协议封装
|
||||||
|
- `main/audio_codecs/box_audio_codec.cc` — ES8311+ES7210 双 codec
|
||||||
|
- `main/audio_processing/audio_processor.cc` — AFE 封装(**当前未启用**)
|
||||||
|
- `main/application.cc` — 主控
|
||||||
|
- `main/boards/movecall-moji-esp32s3/config.h` — 板级配置
|
||||||
|
|
||||||
|
### 外部参考
|
||||||
|
- [火山引擎 ConversationalAI-Embedded-Kit-2.0](https://github.com/volcengine/ConversationalAI-Embedded-Kit-2.0/tree/main_Korvo_2)
|
||||||
|
- [ESP-ADF algorithm_stream 文档](https://docs.espressif.com/projects/esp-adf/en/latest/api-reference/audio-stream/algorithm_stream.html)
|
||||||
|
- [ESP-SR AFE 文档](https://docs.espressif.com/projects/esp-sr/en/latest/esp32s3/AFE/AFE_introduction.html)
|
||||||
|
- 火山 RTC API 参考:https://www.volcengine.com/docs/6348/1806633
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、给用户的执行建议
|
||||||
|
|
||||||
|
按你的需求"麦克风识别不准"是最痛的问题,建议这样做:
|
||||||
|
|
||||||
|
1. **先确认硬件**:测量 ES7210 是否能拿到功放输出回采信号(PCB 走线 + 万用表)
|
||||||
|
2. **如硬件 OK**:开 AFE(一天搞定),收音问题大概率解决
|
||||||
|
3. **如硬件不接**:考虑改 PCB 飞线接 ref,或者用单 mic(不带 AEC)凑合(识别率会差)
|
||||||
|
4. **抖动**:先把 audio_loop pri 改到 5 试,没改善再上 jitter buffer + Opus
|
||||||
|
5. **唤醒杂音**:Phase 7.4 之前暂时接受
|
||||||
|
|
||||||
|
要我做哪一步?我可以:
|
||||||
|
- 看一下你的 PCB 是否有 ref 信号 → 需要你提供原理图
|
||||||
|
- 把 audio_loop pri 改到 5(10 分钟) → 立即可做
|
||||||
|
- 试着启用 AFE 看编译/运行情况 → 实测 1-2 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、🛑 决策记录:为什么"暂不实施方案 E"(2026-05-14 更新)
|
||||||
|
|
||||||
|
> 拿到火山服务端配置截图 + 实测内存数据后,对方案 E 做了最终评估。**结论:当前不实施**,待条件成熟再启动。
|
||||||
|
|
||||||
|
### 11.1 触发重新评估的两个关键信号
|
||||||
|
|
||||||
|
**信号 1:服务端能力清单(用户提供截图)**
|
||||||
|
|
||||||
|
| 服务端能力 | 当前启用状态 |
|
||||||
|
|---|---|
|
||||||
|
| 语音打断 | ✅ 开 |
|
||||||
|
| **VAD(语音活动检测)** | ✅ 开 |
|
||||||
|
| 语义判停 | ❌ 关(应开) |
|
||||||
|
| 音频快速发送 | ✅ 开 |
|
||||||
|
| **AI 降噪** | ❌ 关(应开) |
|
||||||
|
| 字幕显示 | ✅ 开 |
|
||||||
|
|
||||||
|
**关键观察**:用户当前**已经能正常打断 AI 说话**,证明云端 AEC 已在工作(如果没 AEC,AI 自己的声音会被设备麦克风采到 → 服务端 VAD 把回声当用户语音 → AI 自己打断自己 → 无限循环)。
|
||||||
|
|
||||||
|
**信号 2:Internal SRAM 实测仅剩 44 KB**
|
||||||
|
|
||||||
|
从日志 [05-最新日志.txt:300](../../05-最新日志.txt) 提取:
|
||||||
|
```
|
||||||
|
Memory before byte_rtc_join_room - Heap: 7048640 bytes, SPIRAM: 7004584 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
减一下:`Internal SRAM = 7048640 - 7004584 = 44,056 bytes ≈ 44 KB 可用`
|
||||||
|
|
||||||
|
| 资源 | 总量 | 当前剩余 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Flash | 16 MB | 16% (~2.4MB) | 充裕 |
|
||||||
|
| PSRAM | 8 MB | ~7 MB | 充裕 |
|
||||||
|
| **Internal SRAM** | 320KB 可用堆 | **~44 KB** | ⚠️ **危险线** |
|
||||||
|
|
||||||
|
ESP32-S3 的 internal SRAM 给 I2C/I2S DMA buffer / WiFi 协议栈 / 中断处理用,**44KB 已经是临界**。之前开机偶发 `Fail to write to dev 30/80` 很可能就是 I2C buffer 分配失败的信号。
|
||||||
|
|
||||||
|
### 11.2 方案 E 的真实代价
|
||||||
|
|
||||||
|
| 组件 | Internal SRAM | CPU |
|
||||||
|
|---|---|---|
|
||||||
|
| ESP-SR AFE 算法 buffer | ~25 KB | 10-15% Core 1 |
|
||||||
|
| AFE 任务 stack | ~6 KB | - |
|
||||||
|
| ES7210 I2S STD 32bit 升级 | +4 KB DMA | - |
|
||||||
|
| **合计新增** | **~35 KB** | 10-15% |
|
||||||
|
| **改后剩余 SRAM** | **44 - 35 = 9 KB** ⚠️ | |
|
||||||
|
|
||||||
|
**风险评估**:剩 9 KB internal SRAM 几乎确定会触发:
|
||||||
|
- 开机 I2C 初始化失败率显著上升
|
||||||
|
- WiFi 协议栈分配失败 → 断连
|
||||||
|
- BLE 启用时 OOM
|
||||||
|
|
||||||
|
### 11.3 AEC 的边际收益分析
|
||||||
|
|
||||||
|
| 维度 | 没设备端 AEC | 启用方案 E 后 | 提升幅度 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 打断功能 | ✅ 已能用(云端 AEC)| ✅ 已能用 | 0 |
|
||||||
|
| VAD 误触发 | 偶发 | 减少 30% | 中 |
|
||||||
|
| 远场拾音(1m+)| 一般 | 明显改善 | 中高 |
|
||||||
|
| 用户体验"灵敏度" | 偶尔不灵敏 | 改善 5-10% | **小** |
|
||||||
|
| **总收益评级** | — | — | **中小** |
|
||||||
|
|
||||||
|
### 11.4 "偶尔不灵敏"的真实归因(无 AEC 情况下的根因排查)
|
||||||
|
|
||||||
|
| 可能原因 | 占比估计 | 修复成本 |
|
||||||
|
|---|---|---|
|
||||||
|
| 网络抖动导致 PCM 上行丢包 | **40%** | 中(Phase 7.5 改 Opus) |
|
||||||
|
| 环境噪音 | **25%** | 零(开服务端 AI 降噪)|
|
||||||
|
| 麦克风距离太远(>50cm)| **20%** | 零(物理靠近)/低(调 mic gain)|
|
||||||
|
| 服务端 VAD 误判 | **10%** | 零(开语义判停)|
|
||||||
|
| **设备端缺 AEC** | **5%** | 高(方案 E)|
|
||||||
|
|
||||||
|
**结论**:设备端缺 AEC 在你的场景里只占 5%,**95% 的不灵敏可以通过零成本/低成本手段解决**,没必要为 5% 付出 35KB SRAM + 10-15% CPU 的代价。
|
||||||
|
|
||||||
|
### 11.5 推荐的低风险高收益路径(替代方案 E)
|
||||||
|
|
||||||
|
#### Step 1:服务端零成本调优(最优先,5 分钟)
|
||||||
|
|
||||||
|
在火山智能体后台:
|
||||||
|
- ✅ **打开"AI 降噪"开关** — 让服务端帮做降噪,不消耗设备资源
|
||||||
|
- ✅ **打开"语义判停"开关** — 减少 VAD 误触发
|
||||||
|
|
||||||
|
#### Step 2:物理改善(10 分钟)
|
||||||
|
|
||||||
|
- 用户距离麦克风 30 cm 内测试,对比远场效果
|
||||||
|
- 关掉空调/风扇等环境噪音源
|
||||||
|
|
||||||
|
#### Step 3:单行代码调 mic gain(如服务端调优不够)
|
||||||
|
|
||||||
|
```diff
|
||||||
|
// box_audio_codec.cc:265 周围
|
||||||
|
- esp_codec_dev_set_in_channel_gain(input_dev_, MASK(0), 27.0);
|
||||||
|
+ esp_codec_dev_set_in_channel_gain(input_dev_, MASK(0), 33.0); // +6 dB
|
||||||
|
```
|
||||||
|
零内存代价,立即改善远场识别。
|
||||||
|
|
||||||
|
#### Step 4:Phase 7.5 网络层优化(中期)
|
||||||
|
|
||||||
|
- Jitter buffer target 100ms → 200-300ms
|
||||||
|
- 下行 G.711A → Opus(FEC + DTX)
|
||||||
|
- 解决 40% 占比的"网络抖动"根因
|
||||||
|
|
||||||
|
### 11.6 重新评估方案 E 的触发条件
|
||||||
|
|
||||||
|
只有**以下 3 个条件全部满足**时才重新考虑方案 E:
|
||||||
|
|
||||||
|
1. ✅ 服务端 "AI 降噪" 已开 + 实测仍不灵敏
|
||||||
|
2. ✅ mic gain 调到 33dB + 仍不灵敏
|
||||||
|
3. ✅ 网络层稳定(jitter buffer reor < 200)
|
||||||
|
|
||||||
|
并且:
|
||||||
|
4. ✅ **先解决 Internal SRAM 紧张问题**(候选方向):
|
||||||
|
- 砍掉部分 LVGL 字体(CJK 字库占 SRAM 大头)
|
||||||
|
- 把更多 buffer 强制走 PSRAM (`MALLOC_CAP_SPIRAM_ONLY`)
|
||||||
|
- 减少 audio_decode_queue_ 最大长度
|
||||||
|
- 评估 BLE 是否能完全砍掉(如不用蓝牙)
|
||||||
|
|
||||||
|
### 11.7 决策的本质
|
||||||
|
|
||||||
|
**这不是一个技术问题,是一个性价比问题**:
|
||||||
|
|
||||||
|
```
|
||||||
|
启用 AEC 收益 / 资源代价 = 5% 改善 / (35KB SRAM + 10-15% CPU)
|
||||||
|
= 极低
|
||||||
|
|
||||||
|
替代方案收益 / 资源代价 = 65% 改善(服务端 + 物理 + mic gain)/ 0 资源
|
||||||
|
= 极高
|
||||||
|
```
|
||||||
|
|
||||||
|
**先吃掉容易摘的果子(服务端开关 + mic gain)**,再考虑要不要爬树摘 AEC 这颗果子。
|
||||||
|
|
||||||
|
### 11.8 给 Phase 7 的输入
|
||||||
|
|
||||||
|
- Phase 7.6(新增):**Internal SRAM 优化**,目标释放 100KB 给未来 AFE/BLE 共存
|
||||||
|
- Phase 7.5(已规划):网络层 Opus + jitter buffer
|
||||||
|
- Phase 7.7(新增):**设备端 AFE 启用**(条件成熟后),前置依赖 7.5+7.6 完成
|
||||||
|
|
||||||
|
### 11.9 🎯 交叉引用:Kapi_Rtc_toy 是更合适的 AFE 实验场地
|
||||||
|
|
||||||
|
**重要决策提示**:未来如要验证 ESP-SR AFE / G711A→Opus 切换等方案,**优先在 Kapi_Rtc_toy 底座项目实验**,跑通后再回到 Baji 评估。
|
||||||
|
|
||||||
|
| 项目 | Internal SRAM 现状 | 启用 AFE 后剩余 | 风险评级 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Baji_Rtc_Toy**(衍生·有屏)| **~44 KB** | **~9 KB** ⚠️ | 几乎必崩 |
|
||||||
|
| **Kapi_Rtc_toy**(底座·无屏)| **~80-130 KB** | **~50-100 KB** ✅ | 安全余量充足 |
|
||||||
|
|
||||||
|
差异原因:Kapi 无 LVGL(~50KB DRAM)+ 无 GIF 缓冲(~250KB PSRAM)+ 无相册+应援灯,资源宽松 30-50KB SRAM + 300-500KB PSRAM。
|
||||||
|
|
||||||
|
**详细分析见**:[Kapi_Rtc_toy/05Kapi_项目业务全貌与重构决策分析.md](../../../Kapi_Rtc_toy/05Kapi_项目业务全貌与重构决策分析.md)
|
||||||
|
|
||||||
|
该文档覆盖:
|
||||||
|
- Kapi 项目业务全貌(10 个维度)
|
||||||
|
- 三个选项决策矩阵(A 移植 Phase 6 / B 启用 AFE+Opus / C ESP-ADF 重构)
|
||||||
|
- 推荐执行路线(4 步:Phase 6 移植 → AFE → 观察 → 不重构 ADF)
|
||||||
|
- 最终结论:**ESP-ADF 重构不推荐**(工作量 3-4 周,风险极高,等同重写整个项目)
|
||||||
|
|
||||||
|
**Baji 项目目前的优化路径建议**(基于资源约束):
|
||||||
|
1. 短期(不动 AFE):服务端 AI 降噪开 + mic gain 27→33dB + Phase 7.5 网络优化
|
||||||
|
2. 中期(Phase 7.6):先释放 100KB internal SRAM(砍 LVGL 字体 / buffer 走 PSRAM / BLE 评估砍除)
|
||||||
|
3. 长期(Phase 7.7):SRAM 释放成功 + Kapi 实验跑通 AFE 后,再回 Baji 启用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、附录:本报告版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| v1 | 2026-05-14 | 初版对比分析,基于代码 + 推测 |
|
||||||
|
| v1.1 | 2026-05-14 | 拿到电子吧唧原理图,确认 AEC 硬件就绪 |
|
||||||
|
| v1.2 | 2026-05-14 | 拿到 ES7210 datasheet,补充 TDM 配置细节 |
|
||||||
|
| v1.3 | 2026-05-14 | 拿到官方 Korvo-2 V3.1.2 原理图,确认 GPIO 100% 一致 |
|
||||||
|
| **v2.0** | **2026-05-14** | **重大决策更新:基于资源实测 + 服务端能力,暂不实施方案 E** |
|
||||||
@ -611,23 +611,18 @@ void Application::Start() {
|
|||||||
uplink_resampler_.Configure(16000, 8000);
|
uplink_resampler_.Configure(16000, 8000);
|
||||||
codec->Start();
|
codec->Start();
|
||||||
}
|
}
|
||||||
{
|
// ⚠️ 开机电量保护逻辑临时禁用(Phase 7 重构)
|
||||||
int battery_level = 0;
|
// 原设计:开机同步采样 20×10×10ms ADC 数据 → 电量≤25% 时强制静音
|
||||||
bool charging = false;
|
// 问题:阻塞 6 秒才能播放开机播报,且阈值粗暴无 UI 提示
|
||||||
bool discharging = false;
|
// 临时方案:跳过阻塞采样,直接读 NVS 音量设置,恢复开机响应速度
|
||||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
// 长期方案:见 .planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/
|
||||||
// 如果电池电量低于25%,则将输出音量设置为0(静音)
|
{
|
||||||
if (battery_level <= 25) {
|
Settings s("audio", false);
|
||||||
codec->SetOutputVolumeRuntime(0);
|
int vol = s.GetInt("output_volume", AudioCodec::default_output_volume());
|
||||||
} else {
|
if (vol <= 0) {
|
||||||
Settings s("audio", false);
|
vol = AudioCodec::default_output_volume();
|
||||||
int vol = s.GetInt("output_volume", AudioCodec::default_output_volume());
|
|
||||||
if (vol <= 0) {
|
|
||||||
vol = AudioCodec::default_output_volume();
|
|
||||||
}
|
|
||||||
codec->SetOutputVolumeRuntime(vol);// 设置运行时输出音量
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
codec->SetOutputVolumeRuntime(vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 在启动阶段创建并运行播放管道以统一输出(开机启动播放管道)
|
// // 在启动阶段创建并运行播放管道以统一输出(开机启动播放管道)
|
||||||
@ -2023,6 +2018,10 @@ void Application::AudioLoop() {
|
|||||||
if (codec->output_enabled()) {
|
if (codec->output_enabled()) {
|
||||||
OnAudioOutput();
|
OnAudioOutput();
|
||||||
}
|
}
|
||||||
|
// 卡顿优化 1: 让出 Core 1 idle task(FreeRTOS 100Hz tick = 10ms)
|
||||||
|
// 避免 busy loop 占满 Core 1,防止 WiFi 中断/RTC 协议栈饥饿
|
||||||
|
// OnAudioInput/Output 内部本身处理一个完整 PCM 帧(20ms),10ms 调度间隔够
|
||||||
|
vTaskDelay(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4383,26 +4382,42 @@ void Application::EnterIdleHibernate() {
|
|||||||
|
|
||||||
auto display = Board::GetInstance().GetDisplay();
|
auto display = Board::GetInstance().GetDisplay();
|
||||||
|
|
||||||
|
// [废弃方案] 静音填充曾尝试在此处用 codec->OutputData 填 200ms 静音覆盖 DMA 残留
|
||||||
|
// 但实测会让 ES7210 codec 进入卡死状态(连续 10 次重启 ES7210 I2C Open fail)
|
||||||
|
// 移除该方案,杂音问题需要用其他方式解决(如降低唤醒后初始音量)
|
||||||
|
|
||||||
// 1. 真退出 RTC 房间(释放 License)
|
// 1. 真退出 RTC 房间(释放 License)
|
||||||
// Protocol 基类的虚函数 LeaveRoom 默认回退到 CloseAudioChannel,
|
// Protocol 基类的虚函数 LeaveRoom 默认回退到 CloseAudioChannel,
|
||||||
// VolcRtcProtocol 覆写为 volc_rtc_stop + volc_rtc_destroy
|
// VolcRtcProtocol 覆写为 volc_rtc_stop + volc_rtc_destroy
|
||||||
|
// 注意:LeaveRoom 内部会触发 on_audio_channel_closed_ 回调 → codec EnableOutput(false)
|
||||||
if (protocol_) {
|
if (protocol_) {
|
||||||
protocol_->LeaveRoom();
|
protocol_->LeaveRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 字幕显示推迟到最后做(此时 LVGL 锁竞争最少)— 见步骤 9
|
auto codec = Board::GetInstance().GetAudioCodec();
|
||||||
|
|
||||||
// 3. 关闭 codec input/output 让状态机重置
|
// 3. 字幕显示推迟到最后做(此时 LVGL 锁竞争最少)— 见步骤 9
|
||||||
|
|
||||||
|
// 4. 显式关闭 codec input/output 让状态机重置(回调可能已关 output,这里幂等 + 关 input)
|
||||||
// 修复 bug:若不关闭,唤醒后 EnableInput(true) 会进入 "已 open" 异常路径
|
// 修复 bug:若不关闭,唤醒后 EnableInput(true) 会进入 "已 open" 异常路径
|
||||||
// → esp_codec_dev_set_in_channel_gain ES_ERROR_CHECK 失败 abort
|
// → esp_codec_dev_set_in_channel_gain ES_ERROR_CHECK 失败 abort
|
||||||
// → ESP32-S3 软重启而不是恢复对话
|
|
||||||
auto codec = Board::GetInstance().GetAudioCodec();
|
|
||||||
if (codec) {
|
if (codec) {
|
||||||
ESP_LOGI(TAG, "EnterIdleHibernate: 关闭 codec input/output 重置状态机");
|
ESP_LOGI(TAG, "EnterIdleHibernate: 关闭 codec input/output 重置状态机");
|
||||||
codec->EnableInput(false);
|
codec->EnableInput(false);
|
||||||
codec->EnableOutput(false);
|
codec->EnableOutput(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.5. 清空音频解码队列:阻止 hibernate 之前残留的 standby_sound / AI 半句 PCM
|
||||||
|
// 在唤醒后的 OnAudioOutput 中被错误"首帧"识别,从而把软静音过早解开。
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (!audio_decode_queue_.empty()) {
|
||||||
|
ESP_LOGI(TAG, "EnterIdleHibernate: 清空残留音频队列 size=%zu",
|
||||||
|
audio_decode_queue_.size());
|
||||||
|
audio_decode_queue_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 关闭录音管道(避免唤醒后重新打开时冲突)
|
// 4. 关闭录音管道(避免唤醒后重新打开时冲突)
|
||||||
if (recorder_pipeline_) {
|
if (recorder_pipeline_) {
|
||||||
recorder_pipeline_close(recorder_pipeline_);
|
recorder_pipeline_close(recorder_pipeline_);
|
||||||
|
|||||||
@ -6,10 +6,12 @@
|
|||||||
#define TAG "BackgroundTask"
|
#define TAG "BackgroundTask"
|
||||||
|
|
||||||
BackgroundTask::BackgroundTask(uint32_t stack_size) {
|
BackgroundTask::BackgroundTask(uint32_t stack_size) {
|
||||||
|
// 卡顿优化 2: priority 2 → 5
|
||||||
|
// 避免 AI Opus 解码被 main_loop(pri 4)延迟,提升音频实时性
|
||||||
xTaskCreate([](void* arg) {
|
xTaskCreate([](void* arg) {
|
||||||
BackgroundTask* task = (BackgroundTask*)arg;
|
BackgroundTask* task = (BackgroundTask*)arg;
|
||||||
task->BackgroundTaskLoop();
|
task->BackgroundTaskLoop();
|
||||||
}, "background_task", stack_size, this, 2, &background_task_handle_);
|
}, "background_task", stack_size, this, 5, &background_task_handle_);
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundTask::~BackgroundTask() {
|
BackgroundTask::~BackgroundTask() {
|
||||||
|
|||||||
@ -257,6 +257,15 @@ public:
|
|||||||
// 创建 PowerSaveTimer(仅 AI 模式需要)
|
// 创建 PowerSaveTimer(仅 AI 模式需要)
|
||||||
power_save_timer_ = new PowerSaveTimer(240, 10, -1);
|
power_save_timer_ = new PowerSaveTimer(240, 10, -1);
|
||||||
power_save_timer_->OnEnterSleepMode([this]() {
|
power_save_timer_->OnEnterSleepMode([this]() {
|
||||||
|
// 门禁:CanEnterSleepMode 已要求 idle,但 PowerSaveTimer 状态机存在
|
||||||
|
// "in_sleep_mode_ 未翻转 + WakeUp 后立即再次进入"的边角情况,
|
||||||
|
// 历史上曾在 dialog/connecting 期间关功放,导致欢迎语无声。
|
||||||
|
auto& app = Application::GetInstance();
|
||||||
|
auto state = app.GetDeviceState();
|
||||||
|
if (state != kDeviceStateIdle) {
|
||||||
|
ESP_LOGW(TAG, "PowerSaveTimer 在非 idle 状态(%d)触发,忽略关功放", (int)state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ESP_LOGI(TAG, "🔋 进入低功耗模式:CPU降频、Light Sleep启用、功放关闭");
|
ESP_LOGI(TAG, "🔋 进入低功耗模式:CPU降频、Light Sleep启用、功放关闭");
|
||||||
auto codec = GetAudioCodec();
|
auto codec = GetAudioCodec();
|
||||||
if (codec) {
|
if (codec) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include "lvgl.h"
|
#include "lvgl.h"
|
||||||
#include "esp_lvgl_port.h"
|
#include "esp_lvgl_port.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h" // 卡顿优化 5: 字幕节流用 esp_timer_get_time
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
@ -174,7 +175,7 @@ void ai_chat_screen_init(void) {
|
|||||||
|
|
||||||
// 降低 GIF 定时器频率(10ms→20ms),平衡动画流畅度与 CPU 占用
|
// 降低 GIF 定时器频率(10ms→20ms),平衡动画流畅度与 CPU 占用
|
||||||
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
|
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
|
||||||
lv_timer_set_period(gifobj->timer, 20);
|
lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量
|
||||||
|
|
||||||
// GIF 图标(表情上方居中,45x45)
|
// GIF 图标(表情上方居中,45x45)
|
||||||
// 表情高89,顶边y=-44.5,icon高45,中心再上移几像素避免重叠
|
// 表情高89,顶边y=-44.5,icon高45,中心再上移几像素避免重叠
|
||||||
@ -373,14 +374,14 @@ void ai_chat_set_emotion(const char* emotion) {
|
|||||||
lv_gif_set_src(gif_emotion, entry->emotion_gif);
|
lv_gif_set_src(gif_emotion, entry->emotion_gif);
|
||||||
// set_src 内部会重建 10ms 定时器,重新设置为 50ms 降低 CPU 占用
|
// set_src 内部会重建 10ms 定时器,重新设置为 50ms 降低 CPU 占用
|
||||||
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
|
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
|
||||||
lv_timer_set_period(gifobj->timer, 20);
|
lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量
|
||||||
gif_animation_paused = false;
|
gif_animation_paused = false;
|
||||||
|
|
||||||
// 处理叠加图标
|
// 处理叠加图标
|
||||||
if (entry->icon_gif) {
|
if (entry->icon_gif) {
|
||||||
lv_gif_set_src(gif_icon, entry->icon_gif);
|
lv_gif_set_src(gif_icon, entry->icon_gif);
|
||||||
lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon;
|
lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon;
|
||||||
lv_timer_set_period(icon_gifobj->timer, 20);
|
lv_timer_set_period(icon_gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms
|
||||||
lv_obj_clear_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_clear_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else {
|
} else {
|
||||||
// 隐藏图标时暂停其定时器,避免空跑浪费 CPU
|
// 隐藏图标时暂停其定时器,避免空跑浪费 CPU
|
||||||
@ -425,6 +426,17 @@ void ai_chat_set_chat_message(const char* role, const char* content) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 卡顿优化 5: 100ms 最小更新间隔(防抖)
|
||||||
|
// AI 流式 TTS 字幕每秒 5-15 次推送,节流后最多每秒 10 次
|
||||||
|
// 减少 PSRAM 写入流量 5-10 倍(chat_label 重绘)
|
||||||
|
// 例外:空内容(清空字幕)不节流,立即响应
|
||||||
|
static int64_t last_update_us = 0;
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
if (content[0] != '\0' && (now_us - last_update_us) < 100000) { // 100ms
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_update_us = now_us;
|
||||||
|
|
||||||
if (!lvgl_port_lock(500)) { // 200ms → 500ms(GIF 解码繁忙时给予更长等待)
|
if (!lvgl_port_lock(500)) { // 200ms → 500ms(GIF 解码繁忙时给予更长等待)
|
||||||
ESP_LOGW(TAG, "LVGL锁超时,跳过字幕更新");
|
ESP_LOGW(TAG, "LVGL锁超时,跳过字幕更新");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -215,7 +215,7 @@ esp_err_t bg_gif_demo_switch_gif(const char *new_gif_path) {
|
|||||||
// (CLAUDE.md "lv_gif_set_src 会重建定时器" 经验)
|
// (CLAUDE.md "lv_gif_set_src 会重建定时器" 经验)
|
||||||
lv_gif_t *gifobj = (lv_gif_t *)g_gif_obj;
|
lv_gif_t *gifobj = (lv_gif_t *)g_gif_obj;
|
||||||
if (gifobj->timer) {
|
if (gifobj->timer) {
|
||||||
lv_timer_set_period(gifobj->timer, 20);
|
lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量
|
||||||
}
|
}
|
||||||
|
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|||||||
@ -330,7 +330,7 @@ void lvgl_lcd_init(){
|
|||||||
.task_stack = 8192,
|
.task_stack = 8192,
|
||||||
.task_affinity = -1,
|
.task_affinity = -1,
|
||||||
.task_max_sleep_ms = 500,
|
.task_max_sleep_ms = 500,
|
||||||
.timer_period_ms = 5
|
.timer_period_ms = 16 // 卡顿优化 4: 5ms→16ms (60Hz) 减少 LVGL CPU 占用 60%
|
||||||
};
|
};
|
||||||
lvgl_port_init(&lvgl_cfg);
|
lvgl_port_init(&lvgl_cfg);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user