基于
codex/tts-multi-speaker分支,在原有 TTS 多角色朗读功能的基础上做了以下修改。
文件:app/src/main/java/io/legado/app/ui/book/read/ReadAloudCapsule.kt
改动:
pointerInput + detectDragGestures,用户可拖动胶囊到屏幕任意位置read_aloud_capsule),下次进入保持上次位置skipNextSave 标志避免初始加载时触发不必要的保存Modifier.offset { IntOffset(offsetX, offsetY) } 叠加在 Alignment.BottomCenter 之上注意:胶囊默认仍在底部居中,拖动后偏移量相对于默认位置。
文件:app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerViewModel.kt
根因:
isPaused 直接读取 BaseReadAloudService.pause 静态变量,但只在 combine 的 lambda 中执行——该 lambda 仅在 playbackInfo 发射时重新计算。服务未运行时 playbackInfo 不发射,导致 isPaused 永远停在初始值 true。TogglePause 调用 ReadAloud.resume(),但 resume() 在 isRun = false 时静默返回,用户无法从播放器启动朗读。修复:
aloudState flow,通过 callbackFlow 监听 EventBus.ALOUD_STATE 事件(PLAY/PAUSE/STOP),订阅时发射当前状态aloudState 加入 combine,isPaused 根据事件值动态计算TogglePause 逻辑:!isRun 时调用 ReadBook.readAloud() 启动服务,isRun && pause 时 resume,否则 pausesnapshot() 也修正为根据 isRun 正确反映初始状态功能:在朗读配置面板新增"多角色朗读"开关,关闭后完全走原来的 legacy 单引擎朗读路径。
| 文件 | 改动 |
|---|---|
PreferKey.kt |
新增 const val useMultiSpeaker = "useMultiSpeaker" |
ReadTtsConfig.kt |
新增 var useMultiSpeaker by prefDelegate(PreferKey.useMultiSpeaker, true) |
ReadConfig.kt |
新增 var useMultiSpeaker 透传属性 |
ReadAloudSettingsRepository.kt |
ReadAloudPreferences 新增字段,新增 setUseMultiSpeaker() 方法,新增 DataStore Key |
核心拦截点 — BaseReadAloudService.buildSpeechPlan()(第 323 行):
if (bookUrl.isEmpty() || !ReadConfig.useMultiSpeaker) return emptyList()
关闭时直接返回空列表 → playbackQueue 为空 → 服务走 legacy contentList 路径,行为与多角色功能引入前完全一致。
服务选择 — ReadAloud.findCoordinatorHttpSeed():
if (!ReadConfig.useMultiSpeaker) return null
关闭时不查询绑定的 HTTP/云端音色,避免被错误路由到 HTTP TTS 服务。
UseCase 参数透传:
PrepareChapterSpeechPlanUseCase 新增 useMultiSpeaker 参数BuildSpeechPlanUseCase 新增 useMultiSpeaker 参数(关闭时所有 segment 强制用默认引擎,但实际不会到达——因为上层已拦截返回空)| 文件 | 改动 |
|---|---|
ReadBookContract.kt |
ReadAloudPlayerUiState 新增 useMultiSpeaker,ReadBookIntent 新增 SetUseMultiSpeaker |
ReadBookViewModel.kt |
处理 SetUseMultiSpeaker 意图,collectReadAloudPreferences() 收集偏好 |
ReadAloudConfigSheet.kt |
在"语音分析模式"下拉框下方添加 TinySwitchSettingItem |
| Locale | Key | Value |
|---|---|---|
| EN | use_multi_speaker |
Multi-speaker reading |
| EN | use_multi_speaker_summary |
Use different voices for narration, dialogue and other roles |
| CN | use_multi_speaker |
多角色朗读 |
| CN | use_multi_speaker_summary |
为旁白、对话等不同角色使用不同音色 |
| TW/HK | use_multi_speaker |
多角色朗讀 |
| TW/HK | use_multi_speaker_summary |
為旁白、對話等不同角色使用不同音色 |
用户关闭开关
→ ReadConfig.useMultiSpeaker = false
→ BaseReadAloudService.buildSpeechPlan() 返回 emptyList()
→ playbackQueue = ReadAloudPlaybackQueue.Empty
→ 所有播放逻辑走 legacy contentList 路径
→ ReadAloud.findCoordinatorHttpSeed() 返回 null
→ getReadAloudClass() 根据 ttsEngine 配置选择 TTSReadAloudService 或 HttpReadAloudService
→ 行为与多角色功能引入前完全一致
音频文件命名方案在 legacy 路径下不变(sourceKey = httpTts?.url),旧缓存文件仍可复用。已有的 removeCacheFile() 机制(按时间过期清理)足够处理残留文件。
所有改动均通过 .\gradlew.bat :app:compileAppDebugKotlin 编译验证,仅有预存的 deprecation warning。