Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/service/TTSReadAloudService.kt
T
HapeLee 8eb4881767 合并上游更改
感谢:@821938089、@zhongerxll、@youke2580c、@Leroy-X、@Dark-Tide、@lhjgege、@3harmony、@mgz0227
2025-09-01 00:57:17 +08:00

262 lines
8.3 KiB
Kotlin

package io.legato.kazusa.service
import android.app.PendingIntent
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.help.MediaHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.model.ReadAloud
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.servicePendingIntent
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
/**
* 本地朗读
*/
class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener {
private var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
private val ttsUtteranceListener = TTSUtteranceListener()
private var speakJob: Coroutine<*>? = null
private val TAG = "TTSReadAloudService"
override fun onCreate() {
super.onCreate()
initTts()
}
override fun onDestroy() {
super.onDestroy()
clearTTS()
}
@Synchronized
private fun initTts() {
ttsInitFinish = false
val engine = GSON.fromJsonObject<SelectItem<String>>(ReadAloud.ttsEngine).getOrNull()?.value
LogUtils.d(TAG, "initTts engine:$engine")
textToSpeech = if (engine.isNullOrBlank()) {
TextToSpeech(this, this)
} else {
TextToSpeech(this, this, engine)
}
upSpeechRate()
}
@Synchronized
fun clearTTS() {
textToSpeech?.runCatching {
stop()
shutdown()
}
textToSpeech = null
ttsInitFinish = false
}
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.let {
it.setOnUtteranceProgressListener(ttsUtteranceListener)
ttsInitFinish = true
play()
}
} else {
toastOnUi(R.string.tts_init_failed)
}
}
@Synchronized
override fun play() {
if (!ttsInitFinish) return
if (!requestFocus()) return
if (contentList.isEmpty()) {
AppLog.putDebug("朗读列表为空")
ReadBook.readAloud()
return
}
super.play()
MediaHelp.playSilentSound(this@TTSReadAloudService)
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) {
ensureActive()
var text = contentList[i]
if (paragraphStartPos > 0 && i == nowSpeak) {
text = text.substring(paragraphStartPos)
}
if (text.matches(AppPattern.notReadAloudRegex)) {
continue
}
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()
}
}.onError {
AppLog.put("tts朗读出错\n${it.localizedMessage}", it, true)
}
}
override fun playStop() {
textToSpeech?.runCatching {
stop()
}
}
/**
* 更新朗读速度
*/
override fun upSpeechRate(reset: Boolean) {
if (AppConfig.ttsFlowSys) {
if (reset) {
clearTTS()
initTts()
}
} else {
val speechRate = (AppConfig.ttsSpeechRate + 5) / 10f
textToSpeech?.setSpeechRate(speechRate)
}
}
/**
* 暂停朗读
*/
override fun pauseReadAloud(abandonFocus: Boolean) {
super.pauseReadAloud(abandonFocus)
speakJob?.cancel()
textToSpeech?.runCatching {
stop()
}
}
/**
* 恢复朗读
*/
override fun resumeReadAloud() {
super.resumeReadAloud()
play()
}
/**
* 朗读监听
*/
private inner class TTSUtteranceListener : UtteranceProgressListener() {
private val TAG = "TTSUtteranceListener"
override fun onStart(s: String) {
LogUtils.d(TAG, "onStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$s")
textChapter?.let {
if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) {
nextParagraph()
}
if (pageIndex + 1 < it.pageSize
&& readAloudNumber + 1 > it.getReadLength(pageIndex + 1)
) {
pageIndex++
ReadBook.moveToNextPage()
}
upTtsProgress(readAloudNumber + 1)
}
}
override fun onDone(s: String) {
LogUtils.d(TAG, "onDone utteranceId:$s")
nextParagraph()
}
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
super.onRangeStart(utteranceId, start, end, frame)
val msg =
"onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame"
LogUtils.d(TAG, msg)
textChapter?.let {
if (pageIndex + 1 < it.pageSize
&& readAloudNumber + start > it.getReadLength(pageIndex + 1)
) {
pageIndex++
ReadBook.moveToNextPage()
upTtsProgress(readAloudNumber + start)
}
}
}
override fun onError(utteranceId: String?, errorCode: Int) {
LogUtils.d(
TAG,
"onError nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId errorCode:$errorCode"
)
nextParagraph()
}
private fun nextParagraph() {
//跳过全标点段落
do {
readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
paragraphStartPos = 0
nowSpeak++
if (nowSpeak >= contentList.size) {
nextChapter()
return
}
} while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex))
}
@Deprecated("Deprecated in Java")
override fun onError(s: String) {
LogUtils.d(TAG, "onError nowSpeak:$nowSpeak pageIndex:$pageIndex s:$s")
nextParagraph()
}
}
override fun aloudServicePendingIntent(actionStr: String): PendingIntent? {
return servicePendingIntent<TTSReadAloudService>(actionStr)
}
}