298 lines
9.8 KiB
Markdown
298 lines
9.8 KiB
Markdown
# Day 14: WebSocket TTS 调试与项目清理
|
||
|
||
**日期**:2025-12-12
|
||
**目标**:解决 TTS 断续问题,清理项目冗余文件
|
||
|
||
---
|
||
|
||
## 📅 工作摘要
|
||
|
||
### 1. 项目清理 ✅
|
||
|
||
**删除的文件**(共 19 个):
|
||
|
||
| 类型 | 文件 |
|
||
|------|------|
|
||
| HTTP TTS 相关 | `http_tts_stream.cpp`, `http_tts_stream.h`, `http_client.cpp`, `http_client.h` |
|
||
| 测试脚本 | `build_test.sh`, `build_test_camera.sh`, `build_test_imu.sh` |
|
||
| 调试脚本 | `debug_network_libs.sh`, `find_libs_v3.sh`, `setup_mic.sh`, `fix_speaker.sh` |
|
||
| 测试源码 | `main_test.cpp`, `test_audio.cpp`, `test_camera.cpp`, `test_gpio.cpp`, `test_imu.cpp`, `test_network.cpp`, `test_udp_only.cpp` |
|
||
| 其他 | `Makefile_test`, `build_custom.sh`, `build_phase2.sh`, `build_phase3.sh`, `test_mic.sh`, `test/` 目录 |
|
||
|
||
**清理后目录结构**:
|
||
```
|
||
avaota_app_demo/
|
||
├── build_main.sh # 主编译脚本
|
||
├── README.md
|
||
├── MUSL_COMPILE.md
|
||
└── src/
|
||
├── Makefile # 主 Makefile
|
||
├── main.cpp # 主程序
|
||
├── audio/ # 音频模块
|
||
├── camera/ # 摄像头模块
|
||
├── imu/ # IMU 模块
|
||
├── network/ # 网络模块 (ws_client, udp_sender)
|
||
└── utils/ # 日志工具
|
||
```
|
||
|
||
### 2. 恢复 WebSocket TTS ✅
|
||
|
||
**原因**:HTTP TTS (`/stream.wav`) 方案虽然存在,但之前 WebSocket TTS 已验证可以播放声音,只是存在断续问题。重新切换回 WebSocket TTS 进行优化。
|
||
|
||
**代码变更**:
|
||
- `main.cpp`: 移除 `http_tts_player_thread()`,恢复 `audio_capture_thread()` 中的 WebSocket TTS 播放逻辑
|
||
- `Makefile`: 移除 `http_tts_stream.cpp` 和 `http_client.cpp`
|
||
|
||
### 3. 增大 TTS 预缓冲区 ✅
|
||
|
||
**问题**:Omni API 响应慢,每 1-2 秒才返回一批音频数据,导致播放缓冲区频繁欠载(underrun)。
|
||
|
||
**修改** (`main.cpp`):
|
||
```cpp
|
||
// 原设置(0.5 秒预缓冲)
|
||
const size_t PRE_BUFFER_FRAMES = 8000; // 0.5s
|
||
const size_t MIN_PLAY_FRAMES = 3200; // 0.2s
|
||
|
||
// 新设置(2 秒预缓冲)
|
||
const size_t PRE_BUFFER_FRAMES = 32000; // 2s
|
||
const size_t MIN_PLAY_FRAMES = 4800; // 0.3s
|
||
```
|
||
|
||
### 4. 日志优化 ✅
|
||
|
||
更新日志中的设备名称:
|
||
- `ESP32` → `设备` (更通用)
|
||
|
||
### 5. 服务器端导航器预初始化 ✅
|
||
|
||
**问题**:客户端连接后才开始初始化导航器,导致首次连接时服务器卡顿一段时间。
|
||
|
||
**修改** (`app_main.py`):在服务器启动时预初始化所有导航组件:
|
||
|
||
```python
|
||
# 在服务器启动时预初始化(避免客户端连接后延迟)
|
||
print("[NAVIGATION] 预初始化导航器...")
|
||
blind_path_navigator = BlindPathNavigator(yolo_seg_model, obstacle_detector)
|
||
cross_street_navigator = CrossStreetNavigator(yolo_seg_model, obstacle_detector)
|
||
orchestrator = NavigationMaster(blind_path_navigator, cross_street_navigator)
|
||
print("[NAV MASTER] 统领状态机已预初始化")
|
||
```
|
||
|
||
**效果**:客户端连接时无需等待导航器加载,响应更快。
|
||
|
||
---
|
||
|
||
## 📊 测试结果
|
||
|
||
### 2 秒预缓冲测试
|
||
|
||
```
|
||
[AUD-PLAY] Pre-buffer full (33920 frames), starting playback
|
||
[WARN] [AudioPlayer] Underrun occurred, recovering...
|
||
[WARN] [AudioPlayer] Underrun occurred, recovering...
|
||
...(仍有断续)
|
||
```
|
||
|
||
**结论**:2 秒预缓冲仍不足以解决问题,Omni API 发送间隔过长(~2 秒/批),播放速度远快于接收速度。
|
||
|
||
---
|
||
|
||
## 🔴 遗留问题
|
||
|
||
### TTS 语音断断续续
|
||
|
||
**根本原因**:
|
||
- Omni API 生成音频速度 << 播放速度
|
||
- 每 1-2 秒发送一批 10KB 音频
|
||
- 即使 2 秒预缓冲,也会在长句子后半段出现 underrun
|
||
|
||
**可能的解决方案**:
|
||
|
||
| 方案 | 描述 | 工作量 | 效果 |
|
||
|------|------|--------|------|
|
||
| **继续增大预缓冲** | 3-4 秒预缓冲 | 5 分钟 | 可能有效 |
|
||
| **服务器预缓冲** | 服务端收集完整 TTS 后再发送 | 2 小时 | 最佳 |
|
||
| **静音填充** | 缓冲区空时插入静音,防止卡顿 | 1 小时 | 中等 |
|
||
| **变速播放** | 缓冲区低时降速播放(0.95x) | 3 小时 | 高级 |
|
||
|
||
**推荐下一步**:
|
||
1. 先尝试增大预缓冲到 **3-4 秒**
|
||
2. 如仍不行,考虑服务器端预缓冲方案
|
||
|
||
### 5. 服务器端 TTS 发送优化 ✅
|
||
|
||
**发现问题**:
|
||
|
||
通过代码分析发现,`audio_stream.py` 中的 `broadcast_pcm16_realtime` 函数存在设计问题:
|
||
|
||
```python
|
||
# 原来的代码:HTTP 20ms 节拍循环会阻塞整个函数
|
||
async def broadcast_pcm16_realtime(pcm16: bytes):
|
||
# ... WebSocket 发送 ...
|
||
|
||
# HTTP 节拍广播(阻塞!)
|
||
while off < len(pcm16):
|
||
# 每 20ms 发送一小块
|
||
await asyncio.sleep(next_tick - now) # 这会阻塞整个函数
|
||
```
|
||
|
||
**问题**:虽然 WebSocket 发送在前面,但 HTTP 节拍循环会阻塞函数返回。每 1KB 数据需要约 62ms 才能完成,导致下一个 Omni 音频块被延迟处理。
|
||
|
||
**修复**:将 HTTP 节拍广播改为后台任务
|
||
|
||
```python
|
||
# 修改后:WebSocket 立即发送,HTTP 在后台执行
|
||
async def broadcast_pcm16_realtime(pcm16: bytes):
|
||
# ... WebSocket 发送 ...
|
||
|
||
# Day 14 优化:HTTP 广播放到后台任务
|
||
if stream_clients:
|
||
asyncio.create_task(_http_pacing_broadcast(pcm16))
|
||
# 函数立即返回,不阻塞
|
||
|
||
async def _http_pacing_broadcast(pcm16: bytes):
|
||
"""独立后台任务处理 HTTP 节拍广播"""
|
||
# 原来的 20ms 节拍循环代码
|
||
```
|
||
|
||
**预期效果**:
|
||
- WebSocket 发送后立即返回处理下一个 Omni 音频块
|
||
- TTS 传输间隔完全由 Omni API 生成速度决定
|
||
- 无额外延迟
|
||
|
||
---
|
||
|
||
## 📝 代码变更汇总
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `main.cpp` | 移除 HTTP TTS 代码,增大预缓冲到 2 秒 |
|
||
| `Makefile` | 移除 HTTP 相关源文件 |
|
||
| `network/http_*` | **删除** |
|
||
| 测试文件 | **删除** 19 个文件 |
|
||
| **`audio_stream.py` (服务器)** | **HTTP 节拍广播改为后台任务,WebSocket 立即返回** |
|
||
|
||
---
|
||
|
||
## 🔧 技术研究:实时语音传输最佳方案
|
||
|
||
### 协议对比
|
||
|
||
| 协议 | 适用场景 | 延迟 |
|
||
|------|---------|------|
|
||
| **WebRTC** | P2P 实时通话 | <100ms |
|
||
| **WebSocket** | 服务器中转、AI 语音 | 100-500ms |
|
||
| **HTTP Chunked** | 简单流式传输 | >500ms |
|
||
|
||
### 关键技术:Jitter Buffer
|
||
|
||
专业音频系统使用**动态抖动缓冲**:
|
||
1. 预缓冲启动(等待足够数据)
|
||
2. 动态调整缓冲区大小
|
||
3. 静音填充(缓冲区空时)
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 🎉 下午进展:TTS 流畅 + 导航卡顿问题
|
||
|
||
### 6. TTS 播放成功 ✅
|
||
|
||
**服务器端修复生效**:重启服务器后测试,TTS 播放已流畅!
|
||
|
||
**修复方法总结**:
|
||
|
||
| 问题 | 原因 | 修复 |
|
||
|------|------|------|
|
||
| TTS 断续 | `broadcast_pcm16_realtime` 中 HTTP 20ms 节拍循环阻塞 WebSocket 发送 | 将 HTTP 广播改为后台任务 `asyncio.create_task(_http_pacing_broadcast())` |
|
||
|
||
**关键代码变更** (`audio_stream.py`):
|
||
|
||
```python
|
||
# 修改前:HTTP 节拍循环阻塞函数
|
||
async def broadcast_pcm16_realtime(pcm16: bytes):
|
||
await ws.send_bytes(pcm16k) # WebSocket 发送
|
||
while off < len(pcm16): # HTTP 节拍循环(阻塞!)
|
||
await asyncio.sleep(...)
|
||
|
||
# 修改后:WebSocket 发送后立即返回
|
||
async def broadcast_pcm16_realtime(pcm16: bytes):
|
||
await ws.send_bytes(pcm16k) # WebSocket 发送
|
||
if stream_clients:
|
||
asyncio.create_task(_http_pacing_broadcast(pcm16)) # 后台执行
|
||
# 函数立即返回,不阻塞
|
||
```
|
||
|
||
### 7. 导航模式卡顿问题 🔴 (新发现)
|
||
|
||
**现象**:发出"开始导航"指令后,服务器响应"盲道导航已启动",可视化界面立即卡住,FPS 从 10.0 暴跌到 0.2。
|
||
|
||
**根因分析**:
|
||
|
||
服务器端 `workflow_blindpath.py` 存在 **YOLO 检测间隔参数未使用** 的 bug:
|
||
|
||
```python
|
||
# 第253-256行:定义了间隔参数
|
||
self.BLINDPATH_DETECTION_INTERVAL = 8 # 每8帧检测一次
|
||
self.last_blindpath_detection_frame = 0 # 但这个变量从未被使用!
|
||
|
||
# 第424行:实际上每帧都执行 YOLO 推理
|
||
blind_path_mask, crosswalk_mask = self._detect_path_and_crosswalk(image) # 无间隔!
|
||
```
|
||
|
||
**对比障碍物检测**(正确实现):
|
||
```python
|
||
# 第454行:障碍物检测正确使用了间隔
|
||
if self.frame_counter % self.OBSTACLE_DETECTION_INTERVAL == 0:
|
||
detected_obstacles = self._detect_obstacles(image, blind_path_mask)
|
||
```
|
||
|
||
**推荐修复方案**:
|
||
|
||
修改 `workflow_blindpath.py` 第 424 行,添加帧间隔检查:
|
||
|
||
```python
|
||
# 修改后:每 8 帧执行一次 YOLO 推理
|
||
if self.frame_counter % self.BLINDPATH_DETECTION_INTERVAL == 0:
|
||
blind_path_mask, crosswalk_mask = self._detect_path_and_crosswalk(image)
|
||
self.last_blindpath_mask = blind_path_mask
|
||
self.last_crosswalk_mask = crosswalk_mask
|
||
else:
|
||
blind_path_mask = self.last_blindpath_mask
|
||
crosswalk_mask = self.last_crosswalk_mask
|
||
```
|
||
|
||
**预期效果**:YOLO 推理从每帧 1 次降为每 8 帧 1 次,处理负载降低 8 倍。
|
||
|
||
---
|
||
|
||
## 📝 Day 14 完整代码变更汇总
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `main.cpp` | 移除 HTTP TTS 代码,增大预缓冲到 2 秒 |
|
||
| `Makefile` | 移除 HTTP 相关源文件 |
|
||
| `network/http_*` | **删除** |
|
||
| 测试文件 | **删除** 19 个文件 |
|
||
| **`audio_stream.py` (服务器)** | **HTTP 节拍广播改为后台任务,WebSocket 立即返回** |
|
||
|
||
---
|
||
|
||
## 📂 待下次会话处理
|
||
|
||
### 必须修复
|
||
|
||
1. **导航卡顿问题**
|
||
- 修改 `workflow_blindpath.py` 第 424 行
|
||
- 添加 `BLINDPATH_DETECTION_INTERVAL` 帧间隔检查
|
||
- 测试验证导航模式 FPS 恢复正常
|
||
|
||
### 已完成验证
|
||
|
||
- ✅ 摄像头 WebSocket
|
||
- ✅ IMU UDP
|
||
- ✅ 麦克风采集
|
||
- ✅ TTS 播放(已流畅)
|
||
- ⚠️ 导航模式(待修复 YOLO 间隔问题)
|