Skip to content

feat(tts): 播放中切音色/语速/音调即时重读 + Edge 重入修复 (#370)#461

Open
chy5301 wants to merge 7 commits into
codedogQBY:mainfrom
chy5301:feat/tts-instant-synth-change
Open

feat(tts): 播放中切音色/语速/音调即时重读 + Edge 重入修复 (#370)#461
chy5301 wants to merge 7 commits into
codedogQBY:mainfrom
chy5301:feat/tts-instant-synth-change

Conversation

@chy5301

@chy5301 chy5301 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

概述

实现 #370:朗读过程中修改音色 / 语速 / 音调即时生效——防抖 250ms 后从当前句重读,覆盖桌面端(core)与移动端(app-expo),合成参数变化判定抽到 @readany/core 共享。

同时修复实现 #370 时在 Edge 引擎暴露的重入并发 bug:单例 EdgeTTSPlayer 在「即时重读」触发的同步 stop()+speak() 重入下,新旧异步消费循环并发、污染共享状态,表现为切音色时往后跳段、在跳到处反复读同段、先用旧音色读一段再用新音色重读。镜像 DashScopeTTSPlayer 早有的 per-run runId 隔离根除之。

Edge 即时生效在缺少该重入修复时有严重 bug,故二者绑定为一个 PR 原子合入,避免拆开后带 bug 的 Edge 覆盖先落地。

改动

#370 即时重读

  • core/src/tts/respeak.ts(新增):shouldRespeakForSynthChange(按当前引擎判定 voice/rate/pitch 变化;DashScope 仅认 voice)、250ms 防抖常量、isActivePlay
  • 桌面 / 移动端 tts-store.tsupdateConfig 接入防抖重读;非重读变更(如切引擎)取消上一次排下的待执行重读,避免陈旧定时器误触发重启。

Edge 重入修复(属 #372「抽取流式音频 TTS 播放器基类」中的「重入对齐」一块,被本次 bug 倒逼提前落地)

  • core/src/tts/tts-players.ts EdgeTTSPlayer:移除会被同步抹除的 aborted 标志,改 per-run runId;每个 await/timer/回调以 myRun !== this.runId 早退;decodeAndSchedule 捕获本地 ctx 并在解码后校验 ctx 身份;吞掉重入下 resume() 的 reject。
  • 不抽基类、不动进度机制 / 暂停恢复 / 防抖——仅做重入对齐;runId 写法刻意保持对将来抽基类友好。

测试

  • 自动化@readany/core 457 测试通过,含 respeak 判定、updateConfig 防抖 / 取消、Edge runId supersede / stop() 路径 / resume() reject。
  • 实机(桌面 Edge):播放中切音色 / 语速 / 音调 → 干净重读,不跳段、不重复、无旧音色残留;连续快切、手动跳句、暂停恢复、翻页正常;切音色后 250ms 内切引擎不误重启;DashScope 不受影响。

    async 真实时序无法在 node 有意义单测,故以实机为权威验证。

#349 / #427 的关系

#370(改合成配置 → 重读)与 #349(换引擎 → 停旧引擎)是「播放中修改 TTS 设置」同一问题的两半,互补不冲突。#349 已有维护者先于本 PR 创建的在建 PR #427 负责引擎切换那一半;本 PR 只覆盖合成配置这一半,引擎切换停播交由既有的 #427updateConfig 中保留占位注释 [占位 · #427/#349] 标明两者整合点。二者都改 tts-store.ts,合并时会有冲突,需按互补语义整合(engine 分支停播 + clearRespeakTimer(),synth 分支重读)。

本 PR 与 #427 落地后,Edge 与 DashScope 的重入隔离即告统一,#372 的基类抽取可在此基础上推进(详见 #372)。

已知 / 范围外

  • 播放中切引擎不停旧引擎:由既有并行 PR fix(tts): stop stale engine playback on switch #427(先于本 PR)负责,不在本 PR 范围——非本 PR 引入的行为。
  • DashScope 改语速 / 音调不重读:DashScope API 仅认 voice、不发 rate/pitch,by design。

Closes #370

chy5301 added 7 commits June 15, 2026 01:23
- 新增 respeak.ts,提供跨平台的合成参数变化判定
- isActivePlay() 判定播放状态是否活跃(playing/loading)
- shouldRespeakForSynthChange() 判定参数变化是否需要重读
  - DashScope:仅需 apiKey 存在时才检查 voice 变化
  - Edge:检查 voice/rate/pitch 三个参数
  - System:永远返回 false
- 新增 VOICE_RESPEAK_DEBOUNCE_MS 常量(250ms 去抖时间)
- 单测覆盖 11 个场景
播放中切音色/语速/音调会经 jumpToChunk 对单例 EdgeTTSPlayer 做同步
stop()+speak() 重入。原 speak() 入口同步将 aborted 由 true 复位为
false,悬停在 await 的旧消费循环恢复时守卫全部放行,新旧 async 循环并发、
共享 audioCtx/scheduledEnd/fetchBuffer/onChunkChange 互相污染,导致跳段、
反复读同段、旧音色残留。

镜像 DashScopeTTSPlayer:移除易被同步抹除的 aborted 标志,引入 per-run
runId——speak() 入口 ++runId 立即作废旧 run,每个 await 续体/timer/回调以
myRun !== this.runId 早退,decodeAndSchedule 捕获本地 ctx 并在解码后校验
ctx 身份,杜绝旧 run 排程进新 ctx。

仅做 codedogQBY#372 重入对齐一项,不抽流式基类、不动进度/暂停恢复/防抖。
现有用例只覆盖 supersede 路径。补一例锁定「stop() 仅靠 _playing=false
让在途解码续体 bail」这条更微妙的不变量,防 runId 重构后回归。
改音色排下 250ms respeak 定时器后,若窗口内又做一次非重读变更(切引擎、
或改当前引擎不关心的字段),原先不取消该定时器,导致它 fire 后经 jumpToChunk
强制重启播放。在 updateConfig 非重读分支补 clearRespeakTimer(),桌面与移动端一致。
重入时后继 run 会 close 掉前一个 run 正 await 的 AudioContext,使 resume()
以 InvalidStateError reject,经无 .catch 的 speak() 调用变成未处理拒绝。
给 resume() 加 .catch;后续的 myRun !== this.runId 守卫本就会丢弃该 run。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] DashScope 播放中切换音色即时生效(当前需等到下一页)

1 participant