1034 lines
38 KiB
Markdown
1034 lines
38 KiB
Markdown
# WebSocket 与音频上下行实现说明
|
||
|
||
> 本文描述 `feature/st7789p3-new-board` 分支、提交 `07219e6` 附近的实际实现。参数取自当前 `sdkconfig`。`sdkconfig.defaults` 是新建配置时的默认值,二者不一致时,应以本次构建实际使用的 `sdkconfig` 为准。
|
||
|
||
## 1. 文档目的
|
||
|
||
本项目通过一个全双工 WebSocket 会话连接云端多模态服务。设备端主要完成以下工作:
|
||
|
||
1. 获取设备身份和临时访问 Token。
|
||
2. 建立带 TLS 的 WebSocket 连接。
|
||
3. 通过 WebSocket 文本帧发送会话控制指令。
|
||
4. 采集麦克风 PCM,并编码成 raw Opus。
|
||
5. 通过 WebSocket 二进制帧上传 Opus 音频。
|
||
6. 接收服务端 ASR、对话和 TTS 状态事件。
|
||
7. 接收 WebSocket 二进制音频,解码 Opus 并通过 ES8311 播放。
|
||
|
||
需要首先理解的一点是:
|
||
|
||
- ASR、LLM 和 TTS 都在云端完成。
|
||
- ESP32-S3 不运行本地 ASR 或大模型。
|
||
- 设备端负责会话状态管理、音频采集/编解码、数据传输和播放。
|
||
|
||
## 2. 分层和主要文件
|
||
|
||
项目依赖方向为:
|
||
|
||
```text
|
||
app_composition -> control_plane -> domain -> platform
|
||
```
|
||
|
||
WebSocket 与音频链路对应关系如下:
|
||
|
||
| 层 | 主要职责 | 关键文件 |
|
||
| --- | --- | --- |
|
||
| `control_plane` | 鉴权、会话状态机、PTT 策略、协议 JSON、错误恢复 | `components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c` |
|
||
| `control_plane` | Start/Stop/SendSpeech、服务端事件解析、音频回调桥接 | `components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` |
|
||
| `control_plane` | PTT 会话推进、心跳、超时、上下行桥接 | `components/control_plane/src/multimodal/control_plane_multimodal_runtime.c` |
|
||
| `control_plane` | PTT 状态机执行 | `components/control_plane/src/runtime/control_plane_runtime_workers.c` |
|
||
| `domain` | 对上层屏蔽 platform 类型,完成契约映射 | `components/domain/src/runtime/domain_runtime_adapters.c` |
|
||
| `platform` | ESP WebSocket Client、收发锁、消息队列、分片重组 | `components/platform/src/multimodal/transport_ws.c` 及其子目录 |
|
||
| `platform` | ES8311、I2S、Opus 编解码、音频任务与队列 | `components/platform/src/multimodal/audio_pipeline.c` 及其子目录 |
|
||
|
||
## 3. 总体链路
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Key as PTT 按键
|
||
participant CP as Control Plane
|
||
participant Auth as Token 服务
|
||
participant WS as WebSocket Transport
|
||
participant Cloud as 云端多模态服务
|
||
participant Mic as ES8311/I2S 麦克风
|
||
participant Speaker as ES8311/I2S 扬声器
|
||
|
||
Key->>CP: 单击,开始一次 click-to-talk
|
||
CP->>Auth: 获取 voice session Token
|
||
Auth-->>CP: token/workspace_id/app_id
|
||
CP->>WS: 建立 WSS,携带 Bearer 和 Workspace
|
||
WS-->>CP: Connected
|
||
CP->>Cloud: 文本帧 Start
|
||
Cloud-->>CP: DialogStateChanged(Listening)
|
||
CP->>Cloud: 文本帧 SendSpeech
|
||
CP->>Mic: 启动上行采集
|
||
loop 每个 60 ms 音频帧
|
||
Mic-->>CP: 16 kHz/16 bit/mono PCM
|
||
CP->>CP: Opus 编码并进入发送队列
|
||
CP->>Cloud: 二进制帧 raw Opus
|
||
end
|
||
Cloud-->>CP: SpeechStarted
|
||
Cloud-->>CP: SpeechContent(ASR 文本)
|
||
CP->>Cloud: 文本帧 StopSpeech
|
||
Cloud-->>CP: SpeechEnded
|
||
Cloud-->>CP: RespondingStarted
|
||
loop TTS 流式音频
|
||
Cloud-->>WS: 二进制帧 raw Opus
|
||
WS-->>CP: 下行音频回调
|
||
CP->>CP: Opus 解码为 PCM
|
||
CP->>Speaker: I2S 播放
|
||
end
|
||
Cloud-->>CP: RespondingEnded
|
||
CP->>Cloud: LocalRespondingEnded
|
||
```
|
||
|
||
## 4. 当前关键配置参数
|
||
|
||
### 4.1 服务地址和鉴权
|
||
|
||
| 参数 | 当前值 | 含义 |
|
||
| --- | --- | --- |
|
||
| `CONFIG_ENABLE_MULTIMODAL_INTERACTION` | `y` | 启用多模态交互 |
|
||
| `CONFIG_MULTIMODAL_USE_STATIC_CREDENTIALS` | 未启用 | 不直接使用静态 API Key |
|
||
| `CONFIG_MULTIMODAL_AUTH_BYPASS_TOKEN_SERVICE` | 未启用 | 不绕过设备 Token 服务 |
|
||
| `CONFIG_AUTH_V2_ENABLE` | `y` | 获取业务 Token 时使用设备 JWT 鉴权 |
|
||
| `CONFIG_MULTIMODAL_DEVICE_TOKEN_URL` | `https://gz.api.talkingq.com/token/api/device/token` | 设备业务 Token 接口 |
|
||
| `CONFIG_MULTIMODAL_WS_URI` | `wss://dashscope.aliyuncs.com/api-ws/v1/inference` | 多模态 WebSocket 地址 |
|
||
| `CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS` | `10000` | 建连网络超时,10 秒 |
|
||
| `CONFIG_MULTIMODAL_RECONNECT_DELAY_MS` | `2000` | WebSocket 自动重连间隔,2 秒 |
|
||
|
||
Token、JWT、Wi-Fi 密码等敏感信息不应写入文档、日志或提交到 Git。
|
||
|
||
### 4.2 WebSocket 参数
|
||
|
||
| 参数 | 当前值 | 含义 |
|
||
| --- | ---: | --- |
|
||
| `CONFIG_MULTIMODAL_WEBSOCKET_TASK_STACK_BYTES` | 32768 B | ESP WebSocket Client 任务栈目标值 |
|
||
| `CONFIG_MULTIMODAL_WEBSOCKET_TASK_PRIORITY` | 8 | WebSocket Client 任务优先级 |
|
||
| `CONFIG_MULTIMODAL_WS_BUFFER_SIZE` | 2048 B | WebSocket Client 收发缓冲尺寸 |
|
||
| `CONFIG_MULTIMODAL_WS_PING_INTERVAL_SEC` | 20 s | WebSocket Ping 周期 |
|
||
| `CONFIG_MULTIMODAL_WS_PINGPONG_TIMEOUT_SEC` | 120 s | Ping/Pong 超时 |
|
||
| `CONFIG_MULTIMODAL_WS_ENABLE_TCP_KEEPALIVE` | `y` | 启用 TCP Keepalive |
|
||
| `CONFIG_MULTIMODAL_WS_TCP_KEEPALIVE_IDLE_SEC` | 30 s | TCP 空闲多久后开始探测 |
|
||
| `CONFIG_MULTIMODAL_WS_TCP_KEEPALIVE_INTERVAL_SEC` | 10 s | TCP Keepalive 探测间隔 |
|
||
| `CONFIG_MULTIMODAL_WS_TCP_KEEPALIVE_COUNT` | 3 | 连续失败次数 |
|
||
| `CONFIG_MULTIMODAL_WS_REALTIME_LOCK_WAIT_MS` | 80 ms | 实时发送等待共用发送锁的上限 |
|
||
| `CONFIG_MULTIMODAL_WS_REALTIME_SEND_TIMEOUT_MS` | 300 ms | 实时发送单次超时 |
|
||
| `CONFIG_MULTIMODAL_WS_DISPATCH_TASK_STACK_BYTES` | 4096 B | 有序收包 worker 的基础栈要求 |
|
||
| `CONFIG_MULTIMODAL_WS_JSON_TASK_STACK_BYTES` | 8192 B | 文本处理所需栈配置 |
|
||
| `CONFIG_MULTIMODAL_WS_OPUS_TASK_STACK_BYTES` | 12288 B | 二进制音频处理所需栈配置 |
|
||
| `CONFIG_MULTIMODAL_WS_MSG_QUEUE_LEN` | 512 | WebSocket 入站消息队列深度 |
|
||
| `CONFIG_TQ_NETWORK_TASK_CORE` | 0 | WebSocket 有序收包 worker 固定在 CPU Core 0 |
|
||
|
||
当前实现不是分别创建 dispatch、JSON、Opus 三个任务,而是取上述三个栈配置中的最大值,创建一个保持消息顺序的 `mm_ws_ordered` worker。因此当前有序 worker的实际目标栈是 12288 B。
|
||
|
||
代码中的固定限制:
|
||
|
||
| 限制 | 数值 | 说明 |
|
||
| --- | ---: | --- |
|
||
| 单个收到的 WebSocket 数据片段 | 4096 B | 超过 `MULTIMODAL_WS_MSG_MAX_BYTES` 的片段直接丢弃 |
|
||
| 文本消息重组上限 | 32768 B | 允许一个完整文本 payload 被拆成多个片段 |
|
||
| 二进制消息重组上限 | 4096 B | 完整下行二进制 payload 超过该值会丢弃 |
|
||
| 普通文本发送超时 | 1200 ms | 非实时控制指令发送超时 |
|
||
| 普通文本发送重试 | 最多 4 次 | 仅对 `WANT_READ/WANT_WRITE/EAGAIN` 等可重试错误 |
|
||
| 停止 WS worker 等待 | 1200 ms | 有界停止,超时后进入清理兜底 |
|
||
|
||
### 4.3 上行音频参数
|
||
|
||
| 参数 | 当前值 | 含义 |
|
||
| --- | ---: | --- |
|
||
| `CONFIG_MULTIMODAL_UPSTREAM_MODE` | `push2talk` | 由客户端显式发送 `SendSpeech/StopSpeech` |
|
||
| `CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE` | 16000 Hz | 采样率 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS` | 1 | 单声道 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_CODEC_BITS_PER_SAMPLE` | 16 bit | PCM 位深 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_CHUNK_SIZE` | 1920 B | 60 ms 单声道 PCM 数据量 |
|
||
| `CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS` | 60 ms | 每个 Opus 帧的时长 |
|
||
| `CONFIG_MULTIMODAL_OPUS_BITRATE_KBPS` | 16 kbit/s | Opus 目标码率 |
|
||
| `CONFIG_MULTIMODAL_OPUS_COMPLEXITY` | 0 | Opus 编码复杂度,优先降低 MCU 负载 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_IN_GAIN_DB` | 30 dB | ES8311 输入增益 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL` | 0 | 从 codec 数据中选取的麦克风通道 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_UPLINK_TASK_STACK` | 32768 B | 采集/编码任务栈 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_TASK_STACK` | 12288 B | 上传发送任务栈 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_QUEUE_LEN` | 24 | Opus 上行队列容量 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_TIMEOUT_MS` | 5 ms | 编码任务向上行队列投递的等待时间 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_UPLINK_MAX_PACKET_BYTES` | 1024 B | 单个上行 Opus 包最大尺寸 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_PREWARM_OPUS_BEFORE_WS` | `y` | 建立 WS 前预热 Opus,降低开始对话时的内存压力和延迟 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_TASK_CORE` | 1 | 音频任务固定在 CPU Core 1 |
|
||
|
||
由这些参数可以推导:
|
||
|
||
```text
|
||
每帧采样点 = 16000 × 60 / 1000 = 960 samples
|
||
每帧 PCM = 960 × 16 / 8 × 1 = 1920 bytes
|
||
PCM 裸码率 = 16000 × 16 × 1 = 256 kbit/s = 32 kB/s
|
||
Opus 目标包大小约 = 16 kbit/s × 60 ms / 8 = 120 bytes/frame
|
||
每秒 Opus 帧数约 = 1000 / 60 = 16.67 frame/s
|
||
24 格上行队列对应约 = 24 × 60 ms = 1.44 s 音频
|
||
```
|
||
|
||
120 B 是按目标码率计算的平均值,不代表每个实际 Opus 包都固定为 120 B。
|
||
|
||
### 4.4 下行音频参数
|
||
|
||
| 参数 | 当前值 | 含义 |
|
||
| --- | ---: | --- |
|
||
| 下行协议格式 | `raw-opus` | 服务端通过 WebSocket 二进制帧发送 |
|
||
| 下行采样率 | 16000 Hz | Start 中与 codec 采样率保持一致 |
|
||
| 下行帧时长 | 60 ms | Start 的 `frame_size` |
|
||
| 下行码率 | 16 kbit/s | Start 的 `bit_rate` |
|
||
| `CONFIG_MULTIMODAL_DOWNSTREAM_RATE_LIMIT` | 2000 B/s | 请求服务端限制下发速率,约等于 16 kbit/s |
|
||
| `CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_MAX_PACKET_BYTES` | 2048 B | 单个播放队列音频包最大尺寸 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_QUEUE_LEN` | 1024 | 下行播放队列最大包数 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_TASK_STACK` | 12288 B | 解码/播放任务栈 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_OUT_VOLUME` | 60 | ES8311 输出音量配置 |
|
||
| `CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_IDLE_MUTE_MS` | 250 ms | 最后一个音频包后延时静音,减少爆音 |
|
||
| 下行积压告警阈值 | 5000 ms | 估算播放队列积压超过 5 秒时记录告警 |
|
||
|
||
理论上,1024 个 60 ms 包可对应约 61.44 秒音频。实际每个 WebSocket 二进制消息不一定严格等于一个 60 ms Opus 帧,因此这是容量估算,不是协议保证。
|
||
|
||
### 4.5 I2S、I2C 和功放引脚
|
||
|
||
| 功能 | 当前值 |
|
||
| --- | ---: |
|
||
| I2C Port | 0 |
|
||
| ES8311 I2C 地址 | `0x18` |
|
||
| I2C SCL | GPIO2 |
|
||
| I2C SDA | GPIO1 |
|
||
| I2S Port | 0 |
|
||
| I2S MCLK | GPIO38 |
|
||
| I2S BCLK | GPIO39 |
|
||
| I2S WS/LRCK | GPIO41 |
|
||
| I2S DOUT,ESP32 到 ES8311 | GPIO40 |
|
||
| I2S DIN,ES8311 到 ESP32 | GPIO42 |
|
||
| 功放 PA 控制 | GPIO11 |
|
||
| I2S DMA descriptor 数量 | 4 |
|
||
| 每个 I2S DMA descriptor 帧数 | 160 |
|
||
|
||
## 5. 鉴权和 WebSocket 建连
|
||
|
||
### 5.1 当前鉴权模式
|
||
|
||
当前不是静态 API Key 模式,而是设备身份模式:
|
||
|
||
```text
|
||
设备身份/序列号
|
||
-> AUTH_V2 challenge/response
|
||
-> 获取或刷新 device JWT
|
||
-> POST /token/api/device/token
|
||
-> 得到 token、workspace_id、app_id、voice_id
|
||
-> 使用 token 建立 DashScope WSS
|
||
```
|
||
|
||
入口函数:
|
||
|
||
```c
|
||
control_plane_multimodal_auth_fetch()
|
||
```
|
||
|
||
当前 `CONFIG_AUTH_V2_ENABLE=y`,Token 请求使用设备 JWT 作为 Bearer。若 Token 接口返回 401 或 403,代码会使缓存 JWT 失效,强制刷新后再重试一次。
|
||
|
||
Token 接口请求体包含可用的设备身份字段:
|
||
|
||
```json
|
||
{
|
||
"deviceId": "...",
|
||
"serialNumber": "..."
|
||
}
|
||
```
|
||
|
||
返回结果会解析为:
|
||
|
||
```c
|
||
control_plane_multimodal_auth_info_t {
|
||
token;
|
||
workspace_id;
|
||
app_id;
|
||
voice_id;
|
||
device_id;
|
||
serial_number;
|
||
valid;
|
||
}
|
||
```
|
||
|
||
### 5.2 启动调用链
|
||
|
||
```text
|
||
control_plane_start_multimodal_client()
|
||
-> control_plane_prepare_for_multimodal_start()
|
||
-> 注册 control_plane_multimodal_event_bridge()
|
||
-> 注册 control_plane_downstream_audio_bridge()
|
||
-> 创建 multimodal_watchdog_task
|
||
-> control_plane_multimodal_start()
|
||
-> control_plane_multimodal_auth_fetch()
|
||
-> 注册 downstream_playback_event_bridge()
|
||
-> domain_runtime_multimodal_transport_start()
|
||
-> platform_multimodal_transport_start()
|
||
-> multimodal_ws_start()
|
||
```
|
||
|
||
### 5.3 WebSocket 握手头
|
||
|
||
`multimodal_ws_start()` 构造:
|
||
|
||
```http
|
||
Authorization: Bearer <token>
|
||
X-DashScope-WorkSpace: <workspace_id>
|
||
```
|
||
|
||
`X-DashScope-WorkSpace` 很重要。代码中特别说明:只在 Start JSON 中携带 `workspace_id` 不一定能选择正确工作空间,握手阶段也要带这个 Header,否则可能返回 `Invalid workspace_id`。
|
||
|
||
TLS 使用 ESP-IDF 的证书 Bundle:
|
||
|
||
```c
|
||
.crt_bundle_attach = esp_crt_bundle_attach
|
||
```
|
||
|
||
### 5.4 WebSocket Client 配置
|
||
|
||
底层由 `esp_websocket_client` 实现,关键配置如下:
|
||
|
||
```c
|
||
esp_websocket_client_config_t cfg = {
|
||
.uri = CONFIG_MULTIMODAL_WS_URI,
|
||
.headers = s_headers,
|
||
.network_timeout_ms = 10000,
|
||
.reconnect_timeout_ms = 2000,
|
||
.disable_auto_reconnect = false,
|
||
.enable_close_reconnect = true,
|
||
.task_stack = 32768,
|
||
.task_prio = 8,
|
||
.buffer_size = 2048,
|
||
.ping_interval_sec = 20,
|
||
.pingpong_timeout_sec = 120,
|
||
.keep_alive_enable = true,
|
||
.keep_alive_idle = 30,
|
||
.keep_alive_interval = 10,
|
||
.keep_alive_count = 3,
|
||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||
};
|
||
```
|
||
|
||
如果目标 WebSocket 任务栈创建失败,代码会根据可用内部内存尝试多个栈候选值。因为 ESP WebSocket Client 的原生任务栈主要占用内部内存,所以日志中的 `largest_internal` 对排查建连失败很重要。
|
||
|
||
## 6. WebSocket 上承载的两类数据
|
||
|
||
### 6.1 文本帧:协议控制和状态事件
|
||
|
||
客户端发送的 JSON 文本帧包括:
|
||
|
||
- `Start`
|
||
- `Stop`
|
||
- `SendSpeech`
|
||
- `StopSpeech`
|
||
- `CancelSpeech`
|
||
- `RequestToSpeak`
|
||
- `RequestToRespond`
|
||
- `UpdateInfo`
|
||
- `LocalRespondingStarted`
|
||
- `LocalRespondingEnded`
|
||
- `HeartBeat`
|
||
|
||
服务端返回的文本事件包括:
|
||
|
||
- `task-started`
|
||
- `task-failed`
|
||
- `Started`
|
||
- `DialogStateChanged`
|
||
- `RequestAccepted`
|
||
- `SpeechStarted`
|
||
- `SpeechContent`
|
||
- `SpeechEnded`
|
||
- `RespondingStarted`
|
||
- `RespondingContent`
|
||
- `RespondingEnded`
|
||
- `Stopped`
|
||
- `Error`
|
||
|
||
### 6.2 二进制帧:raw Opus 音频
|
||
|
||
二进制方向由当前会话状态决定:
|
||
|
||
- 设备到服务端:麦克风 raw Opus,上行 ASR 音频。
|
||
- 服务端到设备:TTS raw Opus,下行播放音频。
|
||
|
||
二进制帧没有在设备端附加自定义包头。音频格式、采样率、帧长和码率通过最初的 `Start` 参数约定。
|
||
|
||
## 7. Start 消息
|
||
|
||
`control_plane_multimodal_send_start()` 生成 UUID 作为 `task_id`,然后发送类似:
|
||
|
||
```json
|
||
{
|
||
"header": {
|
||
"action": "run-task",
|
||
"task_id": "<generated-uuid>",
|
||
"streaming": "duplex"
|
||
},
|
||
"payload": {
|
||
"task_group": "aigc",
|
||
"task": "multimodal-generation",
|
||
"function": "generation",
|
||
"model": "multimodal-dialog",
|
||
"input": {
|
||
"directive": "Start",
|
||
"workspace_id": "<workspace-id>",
|
||
"app_id": "<app-id>"
|
||
},
|
||
"parameters": {
|
||
"upstream": {
|
||
"type": "AudioOnly",
|
||
"mode": "push2talk",
|
||
"audio_format": "raw-opus",
|
||
"sample_rate": 16000
|
||
},
|
||
"downstream": {
|
||
"audio_format": "raw-opus",
|
||
"sample_rate": 16000,
|
||
"frame_size": 60,
|
||
"bit_rate": 16,
|
||
"speech_rate": 110,
|
||
"incremental_response": false,
|
||
"intermediate_text": "transcript,dialog",
|
||
"transmit_rate_limit": 2000,
|
||
"voice": "<optional-voice-id>"
|
||
},
|
||
"client_info": {
|
||
"user_id": "<device-id>",
|
||
"device": {
|
||
"uuid": "<serial-number>"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
其中 `voice` 只有在解析到有效 voice ID 时才添加。
|
||
|
||
`Start` 发送成功只代表客户端数据成功交给 WebSocket,不代表服务端已经可以接收音频。必须继续等待 `DialogStateChanged(Listening)`。
|
||
|
||
## 8. 通用 Directive 格式
|
||
|
||
大部分会话控制指令由 `send_directive()` 统一构造:
|
||
|
||
```json
|
||
{
|
||
"header": {
|
||
"action": "continue-task",
|
||
"task_id": "<active-task-id>",
|
||
"streaming": "duplex"
|
||
},
|
||
"payload": {
|
||
"input": {
|
||
"directive": "SendSpeech",
|
||
"dialog_id": "<optional-dialog-id>"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 没有有效 `task_id` 时拒绝发送。
|
||
- `dialog_id` 只有非空时才加入 JSON。
|
||
- `Stop` 使用 `action=finish-task`。
|
||
- 普通控制指令使用普通文本发送策略,可重试。
|
||
- HeartBeat、LocalRespondingStarted/Ended 等实时指令使用较短锁等待和较短发送超时,不进行长时间重试。
|
||
|
||
## 9. PTT 和会话状态机
|
||
|
||
当前使用 click-to-talk:
|
||
|
||
```text
|
||
CONFIG_TQ_PTT_MODE_CLICK_TO_TALK=y
|
||
CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS=8000
|
||
```
|
||
|
||
短按被确认后:
|
||
|
||
```text
|
||
control_plane_system_events.c
|
||
-> control_plane_set_ptt_active(true)
|
||
-> 唤醒 runtime worker
|
||
-> control_plane_drive_ptt_state_machine()
|
||
```
|
||
|
||
状态推进如下:
|
||
|
||
| 状态 | 含义 | 下一步 |
|
||
| --- | --- | --- |
|
||
| `PTT_STATE_IDLE` | 没有本次语音请求 | 等待短按 |
|
||
| `PTT_STATE_WAIT_WIFI` | 等待 STA 网络 | 网络可用后检查 WS |
|
||
| `PTT_STATE_WAIT_WS` | 等待 WebSocket | 触发前台连接或后台连接任务 |
|
||
| `PTT_STATE_WAIT_START_SENT` | 当前没有活跃 task | 发送 `Start` |
|
||
| `PTT_STATE_WAIT_LISTENING` | Start 已发但服务端未 Listening | 等待 `DialogStateChanged(Listening)` |
|
||
| `PTT_STATE_WAIT_SPEECH_STARTED` | 准备本轮说话 | 发送 `SendSpeech` |
|
||
| `PTT_STATE_STREAMING` | 允许麦克风上传 | 启动或保持 uplink |
|
||
| `PTT_STATE_TEARDOWN_LOCAL` | 本地停止 | 停止采集并排空队列 |
|
||
| `PTT_STATE_TEARDOWN_REMOTE` | 协议停止 | 发送 `StopSpeech` |
|
||
| `PTT_STATE_DENIED` | 等待超时或条件不满足 | 本轮拒绝 |
|
||
|
||
相关固定超时:
|
||
|
||
| 超时 | 数值 | 行为 |
|
||
| --- | ---: | --- |
|
||
| 等待 Listening | 3000 ms | 标记传输不可用并尝试恢复 |
|
||
| 等待 SpeechStarted 阶段 | 1500 ms | 标记传输不可用并尝试恢复 |
|
||
| PTT teardown | 2000 ms | 防止停止流程无限卡住 |
|
||
| PTT 总等待拒绝 | 10000 ms | 进入 `PTT_STATE_DENIED` |
|
||
| click-to-talk 采集 | 8000 ms | 自动结束本次输入 |
|
||
| 下行播放保护间隔 | 220 ms | 刚收到 TTS 音频时不立即启动上行,减少回声/冲突 |
|
||
|
||
## 10. 音频上传详细流程
|
||
|
||
### 10.1 协议允许条件
|
||
|
||
服务端进入 Listening 后,`control_plane_multimodal_send_speech_start()` 发送 `SendSpeech`。成功后设置:
|
||
|
||
```c
|
||
s_client_speech_active = true;
|
||
s_ptt_release_required = false;
|
||
s_request_to_speak_pending = false;
|
||
```
|
||
|
||
真正上传每个二进制包之前,`control_plane_multimodal_send_audio_realtime()` 仍会检查:
|
||
|
||
```c
|
||
s_task_id[0] != '\0' &&
|
||
s_server_listening &&
|
||
s_client_speech_active &&
|
||
!s_ptt_release_required &&
|
||
!s_remote_responding &&
|
||
!s_drop_downstream_audio
|
||
```
|
||
|
||
任何一个条件不满足,返回 `ESP_ERR_INVALID_STATE`,音频不会发给 WebSocket。
|
||
|
||
### 10.2 启动调用链
|
||
|
||
```text
|
||
control_plane_multimodal_handle_ptt_press_step()
|
||
-> control_plane_start_uplink_if_ready()
|
||
-> control_plane_multimodal_start_uplink()
|
||
-> domain_runtime_multimodal_audio_start_uplink()
|
||
-> platform_multimodal_audio_start_uplink()
|
||
-> multimodal_audio_start_uplink()
|
||
```
|
||
|
||
`multimodal_audio_start_uplink()` 不重复创建整个音频系统,而是:
|
||
|
||
1. 注册 `control_plane_audio_uplink_send_cb()`。
|
||
2. 清空/复位上行队列。
|
||
3. 设置 `uplink_recording_active=true`。
|
||
4. 通知已经创建的采集任务开始工作。
|
||
|
||
### 10.3 采集任务
|
||
|
||
`uplink_capture_task()` 的循环:
|
||
|
||
```text
|
||
esp_codec_dev_read()
|
||
-> 从 ES8311/I2S 读取 codec PCM
|
||
-> extract_mic_channel_16()
|
||
-> 得到 16 kHz、16 bit、单声道、60 ms PCM
|
||
-> esp_opus_enc_process()
|
||
-> 得到 raw Opus
|
||
-> xQueueSend(uplink_send_queue)
|
||
```
|
||
|
||
采集和发送分成两个任务的原因:
|
||
|
||
- I2S 采集需要按音频节奏持续运行。
|
||
- TLS/WebSocket 发送可能因锁、网络抖动而短暂阻塞。
|
||
- 分离任务可以避免网络阻塞直接打断 I2S 采集。
|
||
|
||
### 10.4 上行队列溢出策略
|
||
|
||
队列容量是 24 个 Opus 包。投递等待 5 ms 仍失败时:
|
||
|
||
1. 尝试丢弃队列最旧的一个包。
|
||
2. 再放入最新包。
|
||
3. 若仍失败,则丢弃最新包并记录告警。
|
||
|
||
这是实时语音常用策略:旧音频已经失去实时价值,优先保留最新数据。但如果经常出现:
|
||
|
||
```text
|
||
Mic uplink queue overflow
|
||
Mic uplink queue full, drop packet
|
||
```
|
||
|
||
说明网络发送速度长期低于音频生成速度,ASR 会出现缺字、断句或 NoSpeechRecognized。
|
||
|
||
### 10.5 发送任务和跨层回调
|
||
|
||
`uplink_send_task()` 从队列读取一个 Opus 包:
|
||
|
||
```text
|
||
uplink_send_task()
|
||
-> control_plane_audio_uplink_send_cb()
|
||
-> control_plane_multimodal_send_audio_realtime()
|
||
-> domain_runtime_multimodal_transport_send_binary_realtime()
|
||
-> platform_multimodal_transport_send_binary_realtime()
|
||
-> multimodal_ws_send_binary_realtime()
|
||
-> esp_websocket_client_send_bin()
|
||
```
|
||
|
||
上行音频使用 WebSocket 共用发送互斥锁:
|
||
|
||
- 等锁最多 80 ms。
|
||
- 单次发送最多 300 ms。
|
||
- 实时二进制包不执行普通文本指令那种多次重试。
|
||
|
||
这样可以避免一个过期音频包长时间占住发送链路,但会在严重网络拥塞时主动丢包。
|
||
|
||
### 10.6 结束上传
|
||
|
||
本地主动结束:
|
||
|
||
```text
|
||
control_plane_set_ptt_active(false)
|
||
-> PTT_STATE_TEARDOWN_LOCAL
|
||
-> control_plane_multimodal_stop_uplink()
|
||
-> multimodal_audio_stop_uplink(6000 ms)
|
||
-> 停止继续采集
|
||
-> 等待队列和 in-flight 包排空
|
||
-> 注销 uplink callback
|
||
-> PTT_STATE_TEARDOWN_REMOTE
|
||
-> control_plane_multimodal_send_speech_stop()
|
||
-> 发送 StopSpeech
|
||
```
|
||
|
||
服务端也可能先返回 `SpeechEnded`。收到它时,客户端立即停止上行并设置 `ptt_release_required`,避免当前手势状态导致下一轮音频被错误上传。
|
||
|
||
## 11. ASR 文本事件
|
||
|
||
服务端识别阶段的事件:
|
||
|
||
### 11.1 SpeechStarted
|
||
|
||
表示服务端检测到语音起点:
|
||
|
||
```text
|
||
ASR started
|
||
```
|
||
|
||
### 11.2 SpeechContent
|
||
|
||
`text` 是从本次识别开始到当前时刻的完整累计文本,不应简单地把每次 `text` 追加到旧文本后面。
|
||
|
||
示例:
|
||
|
||
```json
|
||
{"payload":{"output":{"event":"SpeechContent","text":"你好","finished":false}}}
|
||
{"payload":{"output":{"event":"SpeechContent","text":"你好世界","finished":true}}}
|
||
```
|
||
|
||
当前代码在 `finished=true` 时打印最终 ASR 文本,但只向上层发送一个不带 payload 的 `CONTROL_PLANE_MULTIMODAL_EVENT_SPEECH_CONTENT` 事件。也就是说:
|
||
|
||
- 云端对话可以继续使用识别结果。
|
||
- 本地日志可以看到最终文字。
|
||
- 本地 UI/业务层目前拿不到 `text` 内容。
|
||
|
||
如果后续要在 LCD 显示用户说的话或做本地命令,应增加类似:
|
||
|
||
```c
|
||
asr_text_callback(const char *text, bool finished, void *user_data)
|
||
```
|
||
|
||
不能只扩展现有无参数事件枚举。
|
||
|
||
### 11.3 SpeechEnded
|
||
|
||
表示服务端检测到 ASR 语音终点。当前代码会停止麦克风上行并记录:
|
||
|
||
```text
|
||
ASR ended
|
||
```
|
||
|
||
## 12. 音频下发详细流程
|
||
|
||
### 12.1 响应开始
|
||
|
||
服务端先通过文本帧发送 `RespondingStarted`。客户端执行:
|
||
|
||
1. 设置 `s_remote_responding=true`。
|
||
2. 停止仍在运行的上行采集,防止边播边上传自身声音。
|
||
3. 清除上一轮响应的播放状态。
|
||
4. 调用 `domain_runtime_multimodal_audio_begin_downstream_response()`。
|
||
5. 复位 Opus 解码器。
|
||
6. 创建或确认播放任务、播放队列、静音定时器和 DMA bounce buffer。
|
||
7. 预热输出路径,但保持静音,等待第一个有效 PCM 帧。
|
||
|
||
### 12.2 WebSocket 收到二进制帧
|
||
|
||
底层事件入口:
|
||
|
||
```c
|
||
websocket_event_handler(..., WEBSOCKET_EVENT_DATA, ...)
|
||
```
|
||
|
||
处理步骤:
|
||
|
||
```text
|
||
ESP WebSocket Event Task
|
||
-> 校验 opcode 是 TEXT 或 BINARY
|
||
-> 将当前片段复制到 PSRAM 优先分配的内存
|
||
-> xQueueSend(s_ws_msg_queue)
|
||
-> ws_dispatch_task()
|
||
-> process_binary_fragment()
|
||
-> 必要时按 payload_offset 重组
|
||
-> emit_event(MULTIMODAL_WS_EVENT_BINARY_MESSAGE)
|
||
```
|
||
|
||
如果 worker pipeline 因内存不足创建失败,代码会回退到事件回调中直接处理,但这种路径会增加 WebSocket 事件任务的负担。
|
||
|
||
### 12.3 分片重组
|
||
|
||
ESP WebSocket Client 可能把一个完整消息拆成多个 `WEBSOCKET_EVENT_DATA`:
|
||
|
||
- `payload_len`:完整消息总长度。
|
||
- `payload_offset`:当前片段在完整消息中的偏移。
|
||
- `data_len`:当前片段长度。
|
||
|
||
代码要求片段连续且按顺序到达。出现 gap、乱序、越界或总长度超过限制时,重组缓存会被清空并丢弃该消息。
|
||
|
||
### 12.4 Control Plane 音频桥接
|
||
|
||
二进制消息被 Domain 映射后进入:
|
||
|
||
```text
|
||
transport_event_bridge(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_BINARY_MESSAGE)
|
||
-> 检查 s_drop_downstream_audio
|
||
-> 更新 last_downstream_audio_tick
|
||
-> control_plane_downstream_audio_bridge()
|
||
-> control_plane_multimodal_play_downstream_audio()
|
||
-> domain_runtime_multimodal_audio_play_downstream_opus()
|
||
-> platform_multimodal_audio_play_downstream_opus()
|
||
-> multimodal_audio_handle_downstream_opus()
|
||
```
|
||
|
||
如果用户发起打断,`s_drop_downstream_audio=true`,后续仍在网络中到达的旧 TTS 音频会被直接丢弃,不再送入播放队列。
|
||
|
||
### 12.5 下行播放队列
|
||
|
||
`multimodal_audio_handle_downstream_opus()`:
|
||
|
||
1. 检查音频系统、codec 和 Opus decoder 已初始化。
|
||
2. 检查单包不超过 2048 B。
|
||
3. 确保下行播放任务存在。
|
||
4. 在 PSRAM 优先分配一份包缓存并复制数据。
|
||
5. 用最多 20 ms 把包放入播放队列。
|
||
6. 队列满时丢弃当前包并记录告警。
|
||
|
||
上行队列满时会尝试“丢旧保新”,下行队列满时则直接丢弃当前新包,两者策略不同。
|
||
|
||
### 12.6 Opus 解码和 I2S 播放
|
||
|
||
`downstream_playback_task()` 从队列取包,调用:
|
||
|
||
```text
|
||
handle_downstream_opus_sync()
|
||
-> esp_opus_dec_decode()
|
||
-> 得到 16 bit PCM
|
||
-> 必要时扩展为 codec 的通道布局
|
||
-> prime_downstream_output_path()
|
||
-> 解除静音
|
||
-> esp_codec_dev_write()
|
||
-> I2S TX
|
||
-> ES8311 DAC
|
||
-> PA/扬声器
|
||
```
|
||
|
||
当前 codec 配置是单声道,所以解码 PCM 通常可直接写入 codec。如果未来 codec 通道数改为多声道,代码会调用 `expand_mono_to_multichannel_16()` 将单声道复制到目标通道布局。
|
||
|
||
### 12.7 播放完成握手
|
||
|
||
本项目不仅接收云端 `RespondingStarted/Ended`,还会把本地真正的播放状态反馈给服务端:
|
||
|
||
- 第一帧实际开始播放后,发送 `LocalRespondingStarted`。
|
||
- 收到 `RespondingEnded` 后,不一定立即发送本地结束。
|
||
- 需要等播放队列中的剩余音频真正播放完,再发送 `LocalRespondingEnded`。
|
||
|
||
这解决了“服务端已经下发完,但扬声器还在播放缓存”的时间差。若用户打断播放,则停止队列后也会走本地结束通知。
|
||
|
||
### 12.8 静音和爆音控制
|
||
|
||
下行开始时先保持静音,输出路径准备完成、首帧即将写入时才解除静音。最后一包播放后由 idle mute timer 延迟 250 ms 再静音。
|
||
|
||
这部分用于减少:
|
||
|
||
- PA 打开瞬间的爆音。
|
||
- Opus 包之间短间隔导致的频繁 mute/unmute。
|
||
- 响应切换时残留 PCM 被播放。
|
||
|
||
## 13. 文本响应与音频响应的关系
|
||
|
||
服务端可能同时下发:
|
||
|
||
- `RespondingContent.text/spoken`:模型或 TTS 的文字内容。
|
||
- WebSocket Binary:对应的 raw Opus 音频。
|
||
|
||
当前 `RespondingContent` 会通过 responding text callback 交给上层,用于模型命令提取;二进制帧走独立音频回调。因此文本和音频虽然属于同一响应,但设备端是两条并行链路,不能假定“一条文本事件紧跟一个音频包”。
|
||
|
||
## 14. 打断链路 RequestToSpeak
|
||
|
||
当服务端正在 TTS 响应,用户再次触发语音输入时:
|
||
|
||
```text
|
||
PTT
|
||
-> 检测 state.remote_responding
|
||
-> Send RequestToSpeak
|
||
-> s_request_to_speak_pending=true
|
||
-> s_drop_downstream_audio=true
|
||
-> 停止本地下行播放
|
||
-> 服务端返回 RequestAccepted
|
||
-> server_listening=true
|
||
-> 再发送 SendSpeech
|
||
-> 启动新一轮音频上行
|
||
```
|
||
|
||
`RequestToSpeak` 与 `SendSpeech` 不是同一件事:
|
||
|
||
- `RequestToSpeak`:请求打断当前服务端回答并切回 Listening。
|
||
- `SendSpeech`:在已经 Listening 后,声明马上开始上传本轮用户音频。
|
||
|
||
必须等待 `RequestAccepted` 或新的 Listening 状态后再走 `SendSpeech`,否则服务端可能返回协议状态错误。
|
||
|
||
## 15. 心跳、空闲和超时
|
||
|
||
项目有两层保活:
|
||
|
||
1. WebSocket Ping/Pong:20 秒 Ping,120 秒 Pong 超时。
|
||
2. 业务 `HeartBeat` Directive:默认 50 秒一次。
|
||
|
||
Watchdog 每秒检查一次运行状态:
|
||
|
||
| 参数/常量 | 当前值 | 行为 |
|
||
| --- | ---: | --- |
|
||
| `CONFIG_MULTIMODAL_HEARTBEAT_INTERVAL_MS` | 50000 ms | 有活跃 task 时发送 HeartBeat |
|
||
| `CONFIG_MULTIMODAL_NO_RESPONSE_TIMEOUT_MS` | 60000 ms | 长时间无下行时发送 Stop |
|
||
| `CONFIG_MULTIMODAL_DIALOG_IDLE_TIMEOUT_MS` | 60000 ms | 协议配置的对话空闲概念,需结合具体使用点确认 |
|
||
| `CONTROL_PLANE_IDLE_TRANSPORT_SHUTDOWN_MS` | 30000 ms | WS 已连但没有 active task,30 秒后停止 client |
|
||
|
||
注意:TCP Keepalive、WebSocket Ping/Pong 和业务 HeartBeat 作用不同,不能互相替代。
|
||
|
||
## 16. 错误处理与恢复
|
||
|
||
### 16.1 WebSocket 断开或错误
|
||
|
||
底层产生:
|
||
|
||
- `DOMAIN_MULTIMODAL_TRANSPORT_EVENT_DISCONNECTED`
|
||
- `DOMAIN_MULTIMODAL_TRANSPORT_EVENT_ERROR`
|
||
|
||
Control Plane 会:
|
||
|
||
1. 标记 transport unavailable。
|
||
2. 停止音频上行。
|
||
3. 清理或复位当前 session 状态。
|
||
4. 由运行时连接策略决定后续重连。
|
||
|
||
本地主动 stop 导致的 disconnected 不会上报成异常断开,避免停止流程被误判成网络故障。
|
||
|
||
### 16.2 task-failed
|
||
|
||
收到 `header.event=task-failed` 时,代码提取:
|
||
|
||
- `task_id`
|
||
- `error_code`
|
||
- `error_name`
|
||
- `error_message`
|
||
- `request_id`
|
||
- `dialog_id`
|
||
|
||
旧 task 的迟到失败事件会与当前 `task_id` 比较,不匹配时忽略,避免上一轮错误破坏新会话。
|
||
|
||
`InvalidParameter` 或包含 `Invalid action` 的错误被视为协议错误,会直接把 transport 标为不可用。
|
||
|
||
### 16.3 output Error
|
||
|
||
收到 `payload.output.event=Error` 时:
|
||
|
||
1. 打印 `code/name/message/request_id/dialog_id`。
|
||
2. 停止上行采集。
|
||
3. 停止本地下行播放。
|
||
4. 标记协议传输不可用。
|
||
5. 触发 `CONTROL_PLANE_MULTIMODAL_EVENT_OUTPUT_ERROR`。
|
||
6. 上层请求 `SERVER_ERROR` LCD 表情。
|
||
|
||
因此日志中 `Expression request id=1 trigger=2` 通常不是 LCD 自身故障,而是 WebSocket 服务端错误触发的错误表情。
|
||
|
||
### 16.4 音频发送失败窗口
|
||
|
||
`control_plane_multimodal_send_audio_realtime()` 会统计一段时间内的连续音频发送失败。达到阈值后会把传输标记为不可用,避免设备在已经坏掉的连接上持续编码和丢包。
|
||
|
||
## 17. 并发与内存模型
|
||
|
||
### 17.1 发送方向
|
||
|
||
文本和二进制共用 `s_ws_send_lock`,因为 ESP WebSocket Client 的同一连接不能被多个任务无保护地并发发送。
|
||
|
||
普通控制指令和实时音频采用不同策略:
|
||
|
||
| 类型 | 等锁/发送策略 | 设计目标 |
|
||
| --- | --- | --- |
|
||
| Start、Stop、SendSpeech 等普通文本 | 最长约 2 秒等锁、1200 ms 发送、可重试 | 尽量保证关键控制指令送达 |
|
||
| HeartBeat、本地播放状态等实时文本 | 80 ms 等锁、300 ms 发送、不长重试 | 避免阻塞实时链路 |
|
||
| 上行 Opus 二进制 | 80 ms 等锁、300 ms 发送、不长重试 | 过期语音包及时放弃 |
|
||
|
||
### 17.2 接收方向
|
||
|
||
WebSocket 事件回调尽量只复制数据并入队。实际文本解析、二进制重组和上层回调由 worker 处理,降低阻塞 WebSocket 内部事件任务的风险。
|
||
|
||
消息内容和重组缓冲优先从 PSRAM 分配,失败后回退到普通 8-bit heap。WebSocket Client 原生任务栈仍对内部内存大块较敏感。
|
||
|
||
### 17.3 关键队列
|
||
|
||
| 队列 | 当前深度 | 元素含义 | 满时策略 |
|
||
| --- | ---: | --- | --- |
|
||
| WebSocket 入站消息队列 | 512 | 文本/二进制片段描述符与数据指针 | 丢当前片段,完整消息可能因此重组失败 |
|
||
| 音频上行发送队列 | 24 | 已编码 Opus 包 | 尝试丢最旧包、保留最新包 |
|
||
| 音频下行播放队列 | 1024 | 待解码 Opus 包 | 丢当前新包 |
|
||
|
||
## 18. 常见日志与定位方法
|
||
|
||
### 18.1 正常建连
|
||
|
||
```text
|
||
Request device token: ...
|
||
Token fetched (workspace=..., app=...)
|
||
WebSocket starting: wss://...
|
||
WebSocket connected
|
||
Start sent (upstream_mode=push2talk ...)
|
||
```
|
||
|
||
### 18.2 正常上行和 ASR
|
||
|
||
```text
|
||
Mic uplink capture task started ...
|
||
Mic uplink send task started ...
|
||
ASR started
|
||
ASR ... finished=1
|
||
ASR ended
|
||
```
|
||
|
||
若有 `NoSpeechRecognized`,优先检查:
|
||
|
||
1. PTT 极性和状态机是否真的进入 STREAMING。
|
||
2. `ASR started` 是否出现。
|
||
3. 麦克风通道是否选对。
|
||
4. 输入增益是否过低或削顶。
|
||
5. 是否出现 uplink queue overflow/send failed。
|
||
6. Start 的 sample rate/frame/audio format 是否与实际编码一致。
|
||
|
||
### 18.3 正常下行
|
||
|
||
```text
|
||
TTS responding started
|
||
Downstream response begin: sample_rate=16000 frame=60ms ...
|
||
Downstream opus queued ...
|
||
Downstream opus played ...
|
||
TTS responding ended
|
||
```
|
||
|
||
只有文字没有声音时,按顺序检查:
|
||
|
||
1. 是否收到 WebSocket binary。
|
||
2. `s_drop_downstream_audio` 是否被误设为 true。
|
||
3. 单包是否超过 2048 B 或完整 WS binary 是否超过 4096 B。
|
||
4. 播放队列是否满。
|
||
5. Opus decoder 是否报错。
|
||
6. `esp_codec_dev_write()` 是否成功。
|
||
7. PA GPIO11、输出音量和 mute 状态是否正确。
|
||
|
||
### 18.4 关键异常日志
|
||
|
||
| 日志关键词 | 含义 |
|
||
| --- | --- |
|
||
| `Invalid workspace_id` | 握手 Workspace、Token 所属空间或 Start workspace 不一致 |
|
||
| `WebSocket send text lock timeout` | 音频或其他文本发送长时间占用共用锁 |
|
||
| `WebSocket send bin failed` | 上行音频未完整写入 WS |
|
||
| `WS ingress queue full` | 收包处理不及,当前片段被丢弃 |
|
||
| `WS text/binary fragment gap/out-of-order` | 分片缺失或乱序,完整消息被丢弃 |
|
||
| `Mic uplink queue overflow` | 网络发送慢于音频编码速度 |
|
||
| `Downstream playback queue full` | 解码/播放速度跟不上下发速度 |
|
||
| `Downstream playback backlog high` | 估算积压超过 5 秒,交互延迟明显 |
|
||
| `Downstream opus decode failed` | 格式、帧边界或数据完整性异常 |
|
||
| `WS output Error` | 服务端业务/协议错误,不是单纯网络断线 |
|
||
| `task-failed` | 云端任务级失败,需看 code/name/message |
|
||
|
||
## 19. 修改参数时必须成组检查
|
||
|
||
### 19.1 修改采样率
|
||
|
||
不能只改 `CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE`。需要同时确认:
|
||
|
||
- ES8311/I2S 实际采样率。
|
||
- Opus encoder/decoder 采样率。
|
||
- Start upstream.sample_rate。
|
||
- Start downstream.sample_rate。
|
||
- PCM frame buffer 大小。
|
||
- 服务端是否支持该采样率。
|
||
|
||
协议代码只接受 8000、16000、24000、48000 Hz。
|
||
|
||
### 19.2 修改 Opus 帧长
|
||
|
||
修改 `CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS` 时要同时检查:
|
||
|
||
- 每帧 PCM 字节数。
|
||
- `CONFIG_MULTIMODAL_AUDIO_CHUNK_SIZE` 是否匹配。
|
||
- Start downstream.frame_size。
|
||
- 上下行队列换算的缓存时长。
|
||
- 服务端是否支持该帧长。
|
||
|
||
当前 16 kHz/16 bit/mono/60 ms 对应 1920 B PCM。
|
||
|
||
### 19.3 修改码率
|
||
|
||
修改 `CONFIG_MULTIMODAL_OPUS_BITRATE_KBPS` 时要检查:
|
||
|
||
- Start downstream.bit_rate。
|
||
- `CONFIG_MULTIMODAL_DOWNSTREAM_RATE_LIMIT` 是否仍合理。
|
||
- `UPLINK_MAX_PACKET_BYTES` 是否能容纳极端帧。
|
||
- 网络带宽、Wi-Fi 稳定性和音质需求。
|
||
|
||
### 19.4 修改队列深度
|
||
|
||
队列越大不一定越好:
|
||
|
||
- 上行队列太大:ASR 收到的是越来越旧的声音,实时性变差。
|
||
- 下行队列太大:不会立即丢包,但用户听到的回答延迟越来越大。
|
||
- 每个下行包单独分配内存,超大队列可能造成 PSRAM 分配碎片和额外生命周期成本。
|
||
|
||
## 20. 当前实现需要特别注意的点
|
||
|
||
1. **ASR 文本没有携带到上层。** `SPEECH_CONTENT` 只是无参数事件;本地需要显示/处理识别文本时应补充强类型回调。
|
||
2. **文本和音频共用发送锁。** 高频控制指令或异常重试会影响实时音频,反之亦然。
|
||
3. **WebSocket 收包队列同时承载文本和音频。** 下行音频突发可能延迟关键状态事件。
|
||
4. **下行队列配置很深。** 它提高抗抖动能力,但最大理论缓存超过一分钟,需关注高延迟而非只关注是否丢包。
|
||
5. **二进制完整消息重组上限为 4096 B。** 即使播放层允许 2048 B,传输层上限仍需同步考虑;未来提高服务端单包大小时要成组修改。
|
||
6. **click-to-talk 与双击共用 PTT。** 原始 PRESSED 阶段触发 `RequestToSpeak` 可能与双击木鱼手势冲突,手势确认和协议动作需要明确仲裁。
|
||
7. **服务端 SpeechEnded 可以先于本地 8 秒超时。** 两条结束路径必须保持幂等,避免重复 StopSpeech 或错误复用上一轮状态。
|
||
8. **`sdkconfig` 是本地生成配置。** 正式调整应同步确认 `sdkconfig.defaults` 或板级配置方案,否则 fullclean/set-target 后可能恢复旧值。
|
||
|
||
## 21. 最短代码阅读顺序
|
||
|
||
建议按下面顺序阅读,最容易形成完整心智模型:
|
||
|
||
1. `components/control_plane/src/runtime/control_plane_system_events.c`
|
||
2. `components/control_plane/src/runtime/control_plane_runtime_workers.c`
|
||
3. `components/control_plane/src/multimodal/control_plane_multimodal_runtime.c`
|
||
4. `components/control_plane/src/multimodal/control_plane_multimodal_auth_service.c`
|
||
5. `components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
|
||
6. `components/domain/src/runtime/domain_runtime_adapters.c`
|
||
7. `components/platform/src/multimodal/transport_ws.c`
|
||
8. `components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c`
|
||
9. `components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c`
|
||
10. `components/platform/src/multimodal/audio_pipeline.c`
|
||
11. `components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c`
|
||
12. `components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c`
|
||
|
||
## 22. 一句话总结
|
||
|
||
该项目使用一条长期全双工 WSS 同时承载控制 JSON 和 raw Opus:Control Plane 负责鉴权与会话状态,Platform 负责 WebSocket 和 ES8311/I2S/Opus;上行是“PCM 采集 → Opus 编码 → 队列 → WS binary → 云端 ASR”,下行是“云端 TTS WS binary → 分片重组 → 播放队列 → Opus 解码 → PCM → ES8311”,文本事件贯穿两条音频链路,用来保证 Listening、说话、响应、播放完成和异常恢复的顺序正确。
|