Przeglądaj źródła

Merge pull request #1699 from ISuuuu/feat/tts-paragraph-interval

Feat/tts paragraph interval
Kudomaga 4 miesięcy temu
rodzic
commit
d4cbad33c9

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -353,6 +353,7 @@ object PreferKey {
     const val selectVibrator = "selectVibrator"
     const val audioCacheCleanTime = "audioCacheCleanTime"
     const val audioPreDownloadNum = "audioPreDownloadNum"
+    const val ttsParagraphInterval = "ttsParagraphInterval"
     const val customTagColors = "customTagColors"
     const val enableCustomTagColors = "enableCustomTagColors"
     const val navIconBookshelf = "navIconBookshelf"

+ 59 - 5
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -194,7 +194,23 @@ class HttpReadAloudService : BaseReadAloudService(),
                     val file = getSpeakFileAsMd5(fileName)
                     val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
                     launch(Main) {
-                        exoPlayer.addMediaItem(mediaItem)
+                        if (ReadConfig.ttsParagraphInterval > 0) {
+                            if (index == nowSpeak && exoPlayer.mediaItemCount == 0) {
+                                exoPlayer.setMediaItem(mediaItem)
+                                if (!pause) {
+                                    exoPlayer.prepare()
+                                }
+                            }
+                        } else {
+                            if (exoPlayer.mediaItemCount == 0) {
+                                exoPlayer.setMediaItem(mediaItem)
+                                if (!pause) {
+                                    exoPlayer.prepare()
+                                }
+                            } else {
+                                exoPlayer.addMediaItem(mediaItem)
+                            }
+                        }
                     }
                 }
                 preDownloadAudios(httpTts)
@@ -308,7 +324,23 @@ class HttpReadAloudService : BaseReadAloudService(),
                     downloaderChannel.send(downloader)
                     val mediaSource = createMediaSource(dataSourceFactory, fileName)
                     launch(Main) {
-                        exoPlayer.addMediaSource(mediaSource)
+                        if (ReadConfig.ttsParagraphInterval > 0) {
+                            if (index == nowSpeak && exoPlayer.mediaItemCount == 0) {
+                                exoPlayer.setMediaSource(mediaSource)
+                                if (!pause) {
+                                    exoPlayer.prepare()
+                                }
+                            }
+                        } else {
+                            if (exoPlayer.mediaItemCount == 0) {
+                                exoPlayer.setMediaSource(mediaSource)
+                                if (!pause) {
+                                    exoPlayer.prepare()
+                                }
+                            } else {
+                                exoPlayer.addMediaSource(mediaSource)
+                            }
+                        }
                     }
                 }
                 preDownloadAudiosStream(httpTts, downloaderChannel)
@@ -620,9 +652,31 @@ class HttpReadAloudService : BaseReadAloudService(),
             Player.STATE_ENDED -> {
                 // 结束
                 playErrorNo = 0
-                updateNextPos()
-                exoPlayer.stop()
-                exoPlayer.clearMediaItems()
+                val interval = ReadConfig.ttsParagraphInterval.toLong()
+                if (interval > 0) {
+                    val isLastParagraph = nowSpeak >= contentList.lastIndex
+                    updateNextPos()
+                    exoPlayer.stop()
+                    exoPlayer.clearMediaItems()
+                    if (!pause && !isLastParagraph) {
+                        AppLog.putDebug("HttpTTS段落开始停顿: $interval 毫秒")
+                        execute {
+                            delay(interval)
+                            if (!pause) {
+                                launch(Main) {
+                                    if (!pause) {
+                                        play()
+                                        AppLog.putDebug("HttpTTS段落停顿结束,恢复播放")
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    updateNextPos()
+                    exoPlayer.stop()
+                    exoPlayer.clearMediaItems()
+                }
             }
         }
     }

+ 96 - 38
app/src/main/java/io/legado/app/service/TTSReadAloudService.kt

@@ -1,3 +1,4 @@
+@file:Suppress("DEPRECATION")
 package io.legado.app.service
 
 import android.app.PendingIntent
@@ -9,7 +10,6 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.MediaHelp
-import io.legado.app.help.config.AppConfig
 import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.lib.dialogs.SelectItem
@@ -33,6 +33,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
     private val ttsUtteranceListener = TTSUtteranceListener()
     private var speakJob: Coroutine<*>? = null
     private var utteranceStartPos = 0
+    private var needParagraphInterval = false // 是否需要进行段落间隔延迟
     private val TAG = "TTSReadAloudService"
 
     override fun onCreate() {
@@ -91,56 +92,101 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         }
         super.play()
         MediaHelp.playSilentSound(this@TTSReadAloudService)
+        
+        // 捕获本次是否需要进行段落延迟,并将标志位复位(防多次触发)
+        val isDelay = needParagraphInterval
+        needParagraphInterval = false
+        
         speakJob?.cancel()
         speakJob = execute {
-            LogUtils.d(TAG, "朗读列表大小 ${contentList.size}")
-            LogUtils.d(TAG, "朗读页数 ${textChapter?.pageSize}")
-            val tts = textToSpeech ?: throw NoStackTraceException("tts is null")
-            val contentList = contentList
-            var isAddedText = false
-            for (i in nowSpeak until contentList.size) {
+            val interval = ReadConfig.ttsParagraphInterval.toLong()
+            AppLog.putDebug("TTS_PLAY: nowSpeak=$nowSpeak, isDelay=$isDelay, interval=$interval")
+            
+            if (interval > 0) {
+                // 段落间隔模式:单段播放
+                if (isDelay) {
+                    AppLog.putDebug("TTS开始延迟: $interval 毫秒")
+                    delay(interval)
+                    AppLog.putDebug("TTS延迟结束,准备播放")
+                }
                 ensureActive()
-                var text = contentList[i]
-                if (paragraphStartPos > 0 && i == nowSpeak) {
+                
+                LogUtils.d(TAG, "朗读列表大小 ${contentList.size}")
+                val tts = textToSpeech ?: throw NoStackTraceException("tts is null")
+                var text = contentList[nowSpeak]
+                if (paragraphStartPos > 0) {
                     text = text.substring(paragraphStartPos)
                 }
                 if (text.matches(AppPattern.notReadAloudRegex)) {
-                    continue
+                    AppLog.putDebug("TTS段落全标点跳过: nowSpeak=$nowSpeak")
+                    ttsUtteranceListener.onDone(AppConst.APP_TAG + nowSpeak)
+                    return@execute
                 }
-                if (!isAddedText) {
-                    val result = tts.runCatching {
-                        speak(text, TextToSpeech.QUEUE_FLUSH, null, AppConst.APP_TAG + i)
-                    }.getOrElse {
-                        AppLog.put("tts出错\n${it.localizedMessage}", it, true)
-                        TextToSpeech.ERROR
-                    }
-                    if (result == TextToSpeech.ERROR) {
-                        AppLog.put("tts出错 尝试重新初始化")
-                        clearTTS()
-                        initTts()
-                        return@execute
+                AppLog.putDebug("TTS开始Speak: $text")
+                val result = tts.runCatching {
+                    speak(text, TextToSpeech.QUEUE_FLUSH, null, AppConst.APP_TAG + nowSpeak)
+                }.getOrElse {
+                    AppLog.put("tts出错\n${it.localizedMessage}", it, true)
+                    TextToSpeech.ERROR
+                }
+                if (result == TextToSpeech.ERROR) {
+                    AppLog.put("tts出错 尝试重新初始化")
+                    clearTTS()
+                    initTts()
+                    return@execute
+                }
+                LogUtils.d(TAG, "朗读内容添加完成")
+            } else {
+                // 无间隔模式:保持原有的队列式连续播放,确保无缝衔接
+                LogUtils.d(TAG, "朗读列表大小 ${contentList.size}")
+                LogUtils.d(TAG, "朗读页数 ${textChapter?.pageSize}")
+                val tts = textToSpeech ?: throw NoStackTraceException("tts is null")
+                val contentList = contentList
+                var isAddedText = false
+                for (i in nowSpeak until contentList.size) {
+                    ensureActive()
+                    var text = contentList[i]
+                    if (paragraphStartPos > 0 && i == nowSpeak) {
+                        text = text.substring(paragraphStartPos)
                     }
-                } else {
-                    val result = tts.runCatching {
-                        speak(text, TextToSpeech.QUEUE_ADD, null, AppConst.APP_TAG + i)
-                    }.getOrElse {
-                        AppLog.put("tts出错\n${it.localizedMessage}", it, true)
-                        TextToSpeech.ERROR
+                    if (text.matches(AppPattern.notReadAloudRegex)) {
+                        continue
                     }
-                    if (result == TextToSpeech.ERROR) {
-                        AppLog.put("tts朗读出错:$text")
+                    if (!isAddedText) {
+                        val result = tts.runCatching {
+                            speak(text, TextToSpeech.QUEUE_FLUSH, null, AppConst.APP_TAG + i)
+                        }.getOrElse {
+                            AppLog.put("tts出错\n${it.localizedMessage}", it, true)
+                            TextToSpeech.ERROR
+                        }
+                        if (result == TextToSpeech.ERROR) {
+                            AppLog.put("tts出错 尝试重新初始化")
+                            clearTTS()
+                            initTts()
+                            return@execute
+                        }
+                    } else {
+                        val result = tts.runCatching {
+                            speak(text, TextToSpeech.QUEUE_ADD, null, AppConst.APP_TAG + i)
+                        }.getOrElse {
+                            AppLog.put("tts出错\n${it.localizedMessage}", it, true)
+                            TextToSpeech.ERROR
+                        }
+                        if (result == TextToSpeech.ERROR) {
+                            AppLog.put("tts朗读出错:$text")
+                        }
                     }
+                    isAddedText = true
+                }
+                LogUtils.d(TAG, "朗读内容添加完成")
+                if (!isAddedText) {
+                    playStop()
+                    delay(1000)
+                    nextChapter()
                 }
-                isAddedText = true
-            }
-            LogUtils.d(TAG, "朗读内容添加完成")
-            if (!isAddedText) {
-                playStop()
-                delay(1000)
-                nextChapter()
             }
         }.onError {
-            AppLog.put("tts朗读出错\n${it.localizedMessage}", it, true)
+            AppLog.putDebug("TTS协程异常: ${it.localizedMessage}")
         }
     }
 
@@ -215,6 +261,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         override fun onDone(s: String) {
             LogUtils.d(TAG, "onDone utteranceId:$s")
             nextParagraph()
+            if (!pause && ReadConfig.ttsParagraphInterval > 0) {
+                needParagraphInterval = true
+                play()
+            }
         }
 
         override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
@@ -240,6 +290,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
                 "onError nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId errorCode:$errorCode"
             )
             nextParagraph()
+            if (!pause && ReadConfig.ttsParagraphInterval > 0) {
+                needParagraphInterval = true
+                play()
+            }
         }
 
         private fun nextParagraph() {
@@ -259,6 +313,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         override fun onError(s: String) {
             LogUtils.d(TAG, "onError nowSpeak:$nowSpeak pageIndex:$pageIndex s:$s")
             nextParagraph()
+            if (!pause && ReadConfig.ttsParagraphInterval > 0) {
+                needParagraphInterval = true
+                play()
+            }
         }
 
     }

+ 4 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -234,6 +234,7 @@ data class ReadBookUiState(
     val readAloudTtsFollowSys: Boolean = false,
     val readAloudTtsSpeechRate: Int = 10,
     val readAloudTtsTimer: Int = 0,
+    val readAloudParagraphInterval: Int = 0,
     // Style config (reactive state for ReadBookConfig)
     val styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
     // Menu config (from ReadBookConfig via repository)
@@ -621,10 +622,12 @@ sealed interface ReadBookIntent {
     data object ShowReadAloudConfig : ReadBookIntent
     data object SelectSpeakEngine : ReadBookIntent
     data object OpenPreDownloadNumPicker : ReadBookIntent
+    data object OpenParagraphIntervalPicker : ReadBookIntent
     data object OpenCacheCleanTimePicker : ReadBookIntent
     data class ApplySpeakEngine(val value: String?) : ReadBookIntent
     data class ApplyPreDownloadNum(val value: Int) : ReadBookIntent
     data class ApplyAudioCacheCleanTime(val value: Int) : ReadBookIntent
+    data class ApplyParagraphInterval(val value: Int) : ReadBookIntent
     data class EditHttpTts(val engineId: Long? = null) : ReadBookIntent
     data class DeleteHttpTts(val engineId: Long) : ReadBookIntent
     data class SaveHttpTts(val httpTTS: HttpTTS) : ReadBookIntent
@@ -851,6 +854,7 @@ sealed interface ReadBookSheet {
     data class HttpTtsEdit(val engineId: Long? = null) : ReadBookSheet
     data object PreDownloadConfig : ReadBookSheet
     data object AudioCacheCleanConfig : ReadBookSheet
+    data object ParagraphIntervalConfig : ReadBookSheet
     data object ClickActionConfig : ReadBookSheet
     data object PageKeyConfig : ReadBookSheet
     data object TextSelectMenuFilterConfig : ReadBookSheet

+ 15 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -337,6 +337,21 @@ fun ReadBookScreen(
             onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
         },
     )
+    ReadAloudNumberConfigSheet(
+        show = state.activeSheet is ReadBookSheet.ParagraphIntervalConfig,
+        title = stringResource(R.string.tts_paragraph_interval),
+        description = stringResource(
+            R.string.tts_paragraph_interval_summary,
+            state.readAloudParagraphInterval
+        ),
+        value = state.readAloudParagraphInterval,
+        defaultValue = 0,
+        valueRange = 0f..5000f,
+        onValueChange = { onIntent(ReadBookIntent.ApplyParagraphInterval(it)) },
+        onDismissRequest = {
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
+        },
+    )
     AppLogSheet(
         show = state.activeSheet is ReadBookSheet.AppLog,
         onDismissRequest = dismissSheet,

+ 18 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -896,6 +896,15 @@ class ReadBookViewModel(
                 }
             }
 
+            is ReadBookIntent.OpenParagraphIntervalPicker -> {
+                _uiState.update {
+                    it.copy(
+                        readAloudParagraphInterval = ReadConfig.ttsParagraphInterval,
+                        activeSheet = ReadBookSheet.ParagraphIntervalConfig,
+                    )
+                }
+            }
+
             is ReadBookIntent.OpenCacheCleanTimePicker -> {
                 _uiState.update {
                     it.copy(
@@ -938,6 +947,15 @@ class ReadBookViewModel(
                 }
             }
 
+            is ReadBookIntent.ApplyParagraphInterval -> {
+                ReadConfig.ttsParagraphInterval = intent.value
+                _uiState.update {
+                    it.copy(
+                        readAloudParagraphInterval = intent.value
+                    )
+                }
+            }
+
             is ReadBookIntent.EditHttpTts -> {
                 if (intent.engineId == null) {
                     _uiState.update {

+ 4 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt

@@ -136,6 +136,10 @@ fun ReadAloudConfigSheet(
                 title = stringResource(R.string.read_aloud_preload),
                 onClick = { onIntent(ReadBookIntent.OpenPreDownloadNumPicker) },
             )
+            TinyClickableSettingItem(
+                title = stringResource(R.string.tts_paragraph_interval),
+                onClick = { onIntent(ReadBookIntent.OpenParagraphIntervalPicker) },
+            )
             TinyClickableSettingItem(
                 title = stringResource(R.string.audio_cache_clean_time),
                 onClick = { onIntent(ReadBookIntent.OpenCacheCleanTimePicker) },

+ 3 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt

@@ -171,6 +171,9 @@ object ReadConfig {
     var ttsTimer
         get() = ReadTtsConfig.ttsTimer
         set(value) { ReadTtsConfig.ttsTimer = value }
+    var ttsParagraphInterval
+        get() = ReadTtsConfig.ttsParagraphInterval
+        set(value) { ReadTtsConfig.ttsParagraphInterval = value }
     var ignoreAudioFocus
         get() = ReadTtsConfig.ignoreAudioFocus
         set(value) { ReadTtsConfig.ignoreAudioFocus = value }

+ 5 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadTtsConfig.kt

@@ -35,6 +35,11 @@ object ReadTtsConfig {
         0
     )
 
+    var ttsParagraphInterval by prefDelegate(
+        PreferKey.ttsParagraphInterval,
+        0
+    )
+
     var ignoreAudioFocus by prefDelegate(
         PreferKey.ignoreAudioFocus,
         false

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1728,6 +1728,8 @@
     <string name="read_aloud_preload_summary">%d</string>
     <string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
     <string name="audio_cache_clean_time_summary">当前%1$d, 输入 0 代表退出即刻清理。</string>
+    <string name="tts_paragraph_interval">段落间隔(毫秒)</string>
+    <string name="tts_paragraph_interval_summary">当前: %1$d 毫秒。朗读时段落之间的停顿时间。</string>
     <string name="show_swipe_animation">启用滑动时动画</string>
     <string name="click_image_way">点击图片行为</string>
     <string name="show_web_log">输出日志</string>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1520,6 +1520,8 @@
     <string name="ascending_order">升序</string>
     <string name="audio_cache_clean_time">音頻緩存保留時間(分鐘)</string>
     <string name="audio_cache_clean_time_summary">目前:%1$d。輸入 0 則退出時立即清除。</string>
+    <string name="tts_paragraph_interval">段落間隔(毫秒)</string>
+    <string name="tts_paragraph_interval_summary">當前: %1$d 毫秒。朗讀時段落之間的停頓時間。</string>
     <string name="audio_focus_not_acquired">未能獲取音頻焦點</string>
     <string name="author_read">%1$s • %2$d 未讀</string>
     <string name="auto_change_source_failed">自動換源失敗</string>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1856,6 +1856,8 @@
     <string name="read_aloud_preload_summary">%d</string>
     <string name="audio_cache_clean_time">音訊快取保留時間(分鐘)</string>
     <string name="audio_cache_clean_time_summary">目前:%1$d。輸入 0 則退出時立即清除。</string>
+    <string name="tts_paragraph_interval">段落間隔(毫秒)</string>
+    <string name="tts_paragraph_interval_summary">當前: %1$d 毫秒。朗讀時段落之間的停頓時間。</string>
     <string name="show_swipe_animation">滑動時啟用動畫</string>
     <string name="click_image_way">圖片點擊操作</string>
     <string name="show_web_log">輸出日誌</string>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1765,6 +1765,8 @@
     <string name="read_aloud_preload_summary">%d</string>
     <string name="audio_cache_clean_time">Audio Cache Retention Time (minutes)</string>
     <string name="audio_cache_clean_time_summary">Current: %1$d. Enter 0 to clear immediately on exit.</string>
+    <string name="tts_paragraph_interval">Paragraph Interval (ms)</string>
+    <string name="tts_paragraph_interval_summary">Current: %1$d ms. Pause between paragraphs during read-aloud.</string>
     <string name="show_swipe_animation">Enable Animation While Swiping</string>
     <string name="click_image_way">Image Tap Action</string>
     <string name="show_web_log">Output Logs</string>