9.8 KiB
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):
// 原设置(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):在服务器启动时预初始化所有导航组件:
# 在服务器启动时预初始化(避免客户端连接后延迟)
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 小时 | 高级 |
推荐下一步:
- 先尝试增大预缓冲到 3-4 秒
- 如仍不行,考虑服务器端预缓冲方案
5. 服务器端 TTS 发送优化 ✅
发现问题:
通过代码分析发现,audio_stream.py 中的 broadcast_pcm16_realtime 函数存在设计问题:
# 原来的代码: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 节拍广播改为后台任务
# 修改后: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
专业音频系统使用动态抖动缓冲:
- 预缓冲启动(等待足够数据)
- 动态调整缓冲区大小
- 静音填充(缓冲区空时)
🎉 下午进展:TTS 流畅 + 导航卡顿问题
6. TTS 播放成功 ✅
服务器端修复生效:重启服务器后测试,TTS 播放已流畅!
修复方法总结:
| 问题 | 原因 | 修复 |
|---|---|---|
| TTS 断续 | broadcast_pcm16_realtime 中 HTTP 20ms 节拍循环阻塞 WebSocket 发送 |
将 HTTP 广播改为后台任务 asyncio.create_task(_http_pacing_broadcast()) |
关键代码变更 (audio_stream.py):
# 修改前: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:
# 第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) # 无间隔!
对比障碍物检测(正确实现):
# 第454行:障碍物检测正确使用了间隔
if self.frame_counter % self.OBSTACLE_DETECTION_INTERVAL == 0:
detected_obstacles = self._detect_obstacles(image, blind_path_mask)
推荐修复方案:
修改 workflow_blindpath.py 第 424 行,添加帧间隔检查:
# 修改后:每 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 立即返回 |
📂 待下次会话处理
必须修复
- 导航卡顿问题
- 修改
workflow_blindpath.py第 424 行 - 添加
BLINDPATH_DETECTION_INTERVAL帧间隔检查 - 测试验证导航模式 FPS 恢复正常
- 修改
已完成验证
- ✅ 摄像头 WebSocket
- ✅ IMU UDP
- ✅ 麦克风采集
- ✅ TTS 播放(已流畅)
- ⚠️ 导航模式(待修复 YOLO 间隔问题)