feat(voice): add local VAD speech endpoint detection

This commit is contained in:
chenjintao
2026-10-23 10:59:26 +08:00
parent ee32b4d63f
commit f17ce857ff
17 changed files with 1183 additions and 141 deletions
+31 -38
View File
@@ -1,5 +1,10 @@
# 语音对话上行流程详解
> 2026-10-15 更新:默认单击/本地唤醒输入增加本地 VAD(确认 200 ms 人声后,
> 连续 900 ms VAD 静音提前收尾),保留 8 秒兜底。参数、所有权和验收见
> [本地语音端点检测](../local-speech-endpoint.zh-CN.md)。以下松键流程也适用于
> VAD/超时触发的统一 PTT 收尾。
## 1. 文档目的
本文结合当前工程代码,完整说明设备从用户按下语音键开始,到麦克风音频被编码为 Opus 并通过 WebSocket 发送给服务端为止的上行流程。
@@ -72,9 +77,11 @@ WebSocket Binary 发送
```text
用户松开 PTT
↓
停止产生新的 PCM/Opus 包
停止新增 PCM,等待在途采集迭代完成
↓
限时等待发送队列排空
采集任务为不足一帧的 PCM 补零编码
↓
限时等待发送队列和在途包排空
↓
清除上行发送回调
↓
@@ -458,6 +465,7 @@ while (!audio_get_uplink_task_exit_requested(ctx)) {
```c
if (!audio_get_uplink_recording_active(ctx)) {
audio_flush_uplink_tail(ctx);
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100));
continue;
}
@@ -780,12 +788,15 @@ delete_audio_task_with_caps(NULL);
## 8. `uplink_send_task()` 如何发送 Opus
发送任务循环等待队列:
发送任务先在锁内标记 busy,再零等待出队;空队列时清 busy,并等待任务通知
(最多 20 ms)。正常入队后立即通知发送任务,不额外等待轮询周期:
```c
xQueueReceive(ctx->uplink_send_queue, &packet, pdMS_TO_TICKS(100));
xQueueReceive(ctx->uplink_send_queue, &packet, 0U);
```
停止方先查队列再查 busy,避免包刚出队、尚未标记在途时误认为已经排空。
取到有效包后,快照保存好的回调:
```c
@@ -874,51 +885,33 @@ WebSocket Binary 的 payload 是原始 Opus 包,不使用 RTP 封装。
## 11. 停止上行流程
用户松开 PTT 后,Control Plane 执行:
VAD、8 秒兜底或松键将活动输入送入同一 PTT teardown。只有平台停止成功后
才发送 `StopSpeech`;失败则废弃该轮并关闭传输,不能把未排空当成成功。
```c
control_plane_multimodal_stop_uplink();
control_plane_multimodal_send_speech_stop();
```
### 11.1 停止采集与尾帧补齐
### 11.1 停止产生新包
生命周期锁串行化 start/stop。停止方将 `uplink_recording_active` 设为 false,
再获取 capture 锁,等待当前 I2S read/AEC/编码迭代完成。
不足 Opus 帧长的 PCM 交回原采集任务补零编码,不挪到控制线程的小栈。
采集任务与调用方通过受锁保护的尾帧请求/结果握手,不新增编码 worker。
平台层首先执行:
### 11.2 有界排空
```c
audio_set_uplink_recording_active(&s_audio, false);
```
停止预算覆盖 capture 锁等待、尾帧处理、发送队列和在途包等待。
负超时无效;零超时只尝试立即完成,无法完成返回 `ESP_ERR_TIMEOUT`。
超时保留缓冲、回调和待发送数据供重试,不安排后台资源销毁;旧轮未收尾前
新 start 返回失败,不得复用旧数据。常驻采集/发送任务只在 deinit 回收。
采集任务下次循环时进入通知等待,不再读取 Codec,也不再产生新的 Opus 包。
### 11.3 清除回调与协议结束
### 11.2 等待队列排空
停止函数会在给定超时内等待:
- `uplink_send_queue` 为空;
- 没有正在发送的包。
这样可以尽量保证用户松键之前已经采集完成的最后几帧不会立即丢失。
如果超时仍未排空,代码记录警告并清空剩余队列,避免停止流程无限阻塞。
### 11.3 清除发送回调
```c
audio_set_uplink_callback(&s_audio, NULL, NULL);
```
这可以阻止旧队列数据在下一轮会话中误用上一轮发送上下文。
### 11.4 发送 StopSpeech
本地停止采集后,Control Plane 向服务端发送:
成功确认尾帧完成、队列为空且没有正在发送的包后,清除上行回调,随后发送:
```c
send_directive("continue-task", "StopSpeech", true, false);
```
服务端据此结束本轮语音输入,并继续执行 ASR、LLM 和 TTS。
`StopSpeech sent after uplink drain` 日志可与 VAD endpoint 日志对齐。
如果平台停止失败,PTT 收尾走有界传输废弃路径,不无限重试或继续正常提交输入。
## 12. 其他会触发停止上行的情况