01
核心转变
重构前 · 病灶结构
- 持锁调阻塞:
_ai_mutex被握着调player_stop,而 mic(每 50ms)、VAD、KWS 全要抢这把锁 → player 一卡全冻结 - 回调内重入:事件回调跑在 player 线程上,却直接调阻塞 stop → 自死锁 / 锁反转
- 状态打散:5 个线程 + 3 锁 + 2 队列 + 1 信号量;
set_statpost 不查返回值 → 静默丢状态
重构后 · 单事件循环
- 锁外阻塞:阻塞 player 调用只发生在 worker,且不在 player 线程 → 不连累热路径
- 回调只投递:所有回调
post(cmd)即返回 → player 线程永不自重入 - 状态收口:唯一 worker 的一个
switch独占状态机,无需锁;队列满显式告警
KWS 唤醒
→
hly_ai_worker · 唯一线程
- 读 / 改
_ai_stat(独占,无锁) - 调
player_stop / play_data(阻塞安全,有 svc/tkl 超时兜底) publish(AI_STAT_EVENT)→ UI
VAD 变化
→
AI 事件
→
shutup / 定时器
→
start / stop
→
↑ 只 post 一条 cmd 到 _ai_cmd_queue · 立即返回 · 不加锁 · 不阻塞
例外:两条不进队列的路径
mic 数据(每 ~50ms)只做无锁读判断后把音频喂给 agent,绝不进队列;idle 定时器续期(TTS keep-alive)非阻塞、不碰 player,直接调用
_hly_ai_re_idel_start,避免每个 TTS 数据块都灌队列。
02
结构对照
| 原来 | → | 重构后 |
|---|---|---|
_ai_mutex 包住阻塞 player 调用 | → | 删除;状态仅 worker 写,热路径无锁读 |
_hly_ai_sem + sem_task(shutup) | → | AI_CMD_SHUTUP |
_hly_ai_stat_queue + stat_task | → | 合并进 worker 的 _hly_ai_apply_stat() |
_hly_ai_debug_task + debug_step_* | → | 删除(抓 hang 的脚手架,已完成使命) |
回调直接调 player_stop | → | 回调只 _hly_ai_post(cmd),stop/play 在 worker |
| 5 线程 + 3 锁 + 2 队列 + 1 信号量 | → | 1 命令队列 + 1 worker + 2 窄锁 ( _timer_mutex / _alert_mutex) |
03
事件 → 命令映射
每个 wukong 事件 / 输入映射成一条语义命令,决策逻辑(进 LISTEN / IDLE / THINK 等)全部放在 worker 里,投递方不做判断。
| 来源 / 事件 | 命令 | worker 行为 |
|---|---|---|
| KWS 唤醒 | AI_CMD_WAKEUP | (IDLE 时)停全部播放 + chat break + 进 LISTEN + 唤醒提示音 |
| shutup / idle 超时 | AI_CMD_SHUTUP | 停 FG + chat break + 进 IDLE + 退出提示音 |
| chat_over | AI_CMD_CHAT_OVER | 清 wake_up_flag + 退出提示音 |
| VAD START / STOP | AI_CMD_VAD_START/STOP | 门控后开/停上行;STOP 且非 SPEAK → THINK |
| ASR 空 / 错 | AI_CMD_ASR_FAIL | THINK → LISTEN |
| TTS_PRE | AI_CMD_TTS_PRE | → SPEAK |
| PLAY_END | AI_CMD_PLAY_END | wake_up_flag ? LISTEN : IDLE |
| EXIT | AI_CMD_EXIT | → IDLE |
| PLAY_CTL_PAUSE/PLAY/REPLAY | AI_CMD_PLAY_* | 停 BG / 恢复 / 重播 |
| TTS_START/DATA/STOP… | (不进队列) | 直接续 idle 定时器(非阻塞 keep-alive) |
| mic 数据(~50ms) | (不进队列) | 无锁读门控后直接 tuya_ai_audio_input |
worker 主体(节选)
STATIC VOID _hly_ai_worker_cb(PVOID_T args) { hly_ai_msg_t m; while (1) { if (tal_queue_fetch(_ai_cmd_queue, &m, SEM_WAIT_FOREVER) != OPRT_OK) continue; switch (m.cmd) { case AI_CMD_WAKEUP: if (!ai_start || _ai_stat != AI_STAT_IDEL) break; wukong_audio_player_stop(AI_PLAYER_ALL); // 只卡 worker 自己 wukong_audio_input_reset(); tuya_ai_agent_event(AI_EVENT_CHAT_BREAK, 0); wake_up_flag = TRUE; paly_alart = TRUE; _hly_ai_apply_stat(AI_STAT_LISTEN); // 独占改状态, 无锁 wukong_audio_player_alert(AI_TOY_ALERT_TYPE_WAKEUP, FALSE); break; case AI_CMD_PLAY_END: if (paly_alart) paly_alart = FALSE; _hly_ai_apply_stat(wake_up_flag ? AI_STAT_LISTEN : AI_STAT_IDEL); break; ... } } }
04
顺手修掉的 6 个 bug
_hly_ai_post查tal_queue_post返回值并告警 —— 不再像原_hly_ai_set_stat静默丢状态。hly_ai_start / stop / chat_over补上return OPRT_OK—— 不再返回栈垃圾。_hly_ai_event改为纯void,不再return值。sprintf(&str, ...)→snprintf(path, sizeof, ...),类型与越界都修正。- 删除 mic 路径每帧的
TAL_PR_NOTICE日志刷屏。 - KWS 回调取消每帧加锁,门控移入 worker。
05
待你核对项
因无法在此编译、且未持有全部头文件,以下与工程头文件相关处请确认:
hly_fs_load_psram(path, &data, &len)第一参类型(原误传&str,已改传字符串首址)。snprintf在该 libc 可用;否则回退sprintf((char*)str, ...)。THREAD_CFG_T字段、OPRT_COM_ERROR/OPRT_INVALID_PARM、wukong_audio_player_set_resume/set_replay、tuya_ai_input_start/stop名称一致。- KWS 的
(INT_T)data在 T5(32 位)安全,保留原写法以免引入头文件假设。 - 验证场景:唤醒 → 打断(shutup)→ 隔夜待机再唤醒 全程有声、无卡顿、无 hang。
配套前提
本重构消除的是「业务层 / 播放器层」的重入与持锁阻塞;「底层
tkl_ao_put_frame 无限重试」必须同时保留前一轮的超时修复,二者配套才能根除「隔夜唤醒无声」。