|
|
|
@ -17,7 +17,7 @@ import java.io.InputStream |
|
|
|
|
|
|
|
/** |
|
|
|
* Azure TTS Helper |
|
|
|
* |
|
|
|
* |
|
|
|
* 基于微软Azure语音服务的TTS实现 |
|
|
|
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis |
|
|
|
*/ |
|
|
|
@ -27,33 +27,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
private const val DEFAULT_LANGUAGE = "zh-CN" |
|
|
|
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Azure语音服务配置 |
|
|
|
private var speechConfig: SpeechConfig? = null |
|
|
|
private var synthesizer: SpeechSynthesizer? = null |
|
|
|
private var isInitialized = false |
|
|
|
private var isSpeaking = false |
|
|
|
|
|
|
|
|
|
|
|
// 当前配置 |
|
|
|
private var currentVoice = DEFAULT_VOICE |
|
|
|
private var currentRate = "0%" |
|
|
|
private var currentPitch = "0%" |
|
|
|
private var currentVolume = "100%" |
|
|
|
|
|
|
|
|
|
|
|
// 事件监听器列表 |
|
|
|
private val eventListeners = mutableListOf<TtsEventListener>() |
|
|
|
private val audioDataListeners = mutableListOf<AudioDataListener>() |
|
|
|
|
|
|
|
|
|
|
|
// 自定义音频输出流 |
|
|
|
private var customAudioOutputStream: PushAudioOutputStream? = null |
|
|
|
|
|
|
|
|
|
|
|
// 流式文本处理的缓冲区 |
|
|
|
private val streamBuffer = StringBuilder() |
|
|
|
private var lastSpeakTime = 0L |
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化TTS引擎 |
|
|
|
* |
|
|
|
* |
|
|
|
* @param ttsAppId 未使用,保留为空即可 |
|
|
|
* @param ttsAppToken Azure语音服务订阅密钥 |
|
|
|
* @param ttsResource Azure语音服务区域 |
|
|
|
@ -61,33 +61,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
* @return 是否初始化成功 |
|
|
|
*/ |
|
|
|
override fun initialize( |
|
|
|
ttsAppId: String, |
|
|
|
ttsAppToken: String, |
|
|
|
ttsResource: String, |
|
|
|
ttsAppId: String, |
|
|
|
ttsAppToken: String, |
|
|
|
ttsResource: String, |
|
|
|
language: String |
|
|
|
): Boolean { |
|
|
|
try { |
|
|
|
// 释放之前的资源 |
|
|
|
dispose() |
|
|
|
|
|
|
|
|
|
|
|
// 创建语音配置 |
|
|
|
speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource) |
|
|
|
|
|
|
|
|
|
|
|
// 使用标准的音频格式(16KHz, 16bit, 单声道) |
|
|
|
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm) |
|
|
|
|
|
|
|
|
|
|
|
// 确保使用正确的语音输出 |
|
|
|
speechConfig?.setProperty("SPEECH-AudioOutputFormat", "riff-16khz-16bit-mono-pcm") |
|
|
|
|
|
|
|
|
|
|
|
// 优化:设置低延迟连接属性 |
|
|
|
speechConfig?.setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "300") |
|
|
|
speechConfig?.setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") |
|
|
|
|
|
|
|
|
|
|
|
speechConfig?.setSpeechSynthesisVoiceName(currentVoice) |
|
|
|
|
|
|
|
|
|
|
|
// 添加日志记录语音格式 |
|
|
|
FileLogger.d(TAG, "语音设置: voice=$currentVoice, format=Riff16Khz16BitMonoPcm") |
|
|
|
|
|
|
|
|
|
|
|
// 创建音频配置 |
|
|
|
val audioConfig = if (customAudioOutputStream != null) { |
|
|
|
// 使用自定义音频输出流 |
|
|
|
@ -96,29 +96,29 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
// 使用默认扬声器 |
|
|
|
AudioConfig.fromDefaultSpeakerOutput() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建合成器 |
|
|
|
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) |
|
|
|
|
|
|
|
|
|
|
|
// 设置事件监听 |
|
|
|
setupEventListeners() |
|
|
|
|
|
|
|
|
|
|
|
isInitialized = true |
|
|
|
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource") |
|
|
|
|
|
|
|
|
|
|
|
// 优化:初始化完成后立即预热 |
|
|
|
warmupSynthesizer() |
|
|
|
|
|
|
|
|
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "TTS引擎初始化失败: ${e.message}") |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置自定义音频输出流 |
|
|
|
* |
|
|
|
* |
|
|
|
* @param outputStream 自定义音频输出流,如果为null则使用默认音频输出 |
|
|
|
* @return 是否设置成功 |
|
|
|
*/ |
|
|
|
@ -126,19 +126,19 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
try { |
|
|
|
// 保存引用 |
|
|
|
customAudioOutputStream = outputStream |
|
|
|
|
|
|
|
|
|
|
|
// 如果已初始化,需要重新创建合成器以应用新的音频输出流 |
|
|
|
if (isInitialized) { |
|
|
|
recreateSynthesizer() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "设置自定义音频输出流失败: ${e.message}") |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 添加TTS事件监听器 |
|
|
|
*/ |
|
|
|
@ -147,14 +147,14 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
eventListeners.add(listener) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 移除TTS事件监听器 |
|
|
|
*/ |
|
|
|
override fun removeListener(listener: TtsEventListener) { |
|
|
|
eventListeners.remove(listener) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 添加音频数据监听器 |
|
|
|
*/ |
|
|
|
@ -163,17 +163,17 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
audioDataListeners.add(listener) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 移除音频数据监听器 |
|
|
|
*/ |
|
|
|
override fun removeAudioDataListener(listener: AudioDataListener) { |
|
|
|
audioDataListeners.remove(listener) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置是否使用内部播放器 |
|
|
|
* |
|
|
|
* |
|
|
|
* @param useInternalPlayer true: 使用内部播放器自动播放音频 |
|
|
|
* false: 仅通过音频数据监听器输出数据,不播放 |
|
|
|
*/ |
|
|
|
@ -183,7 +183,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
if (!useInternalPlayer && customAudioOutputStream == null) { |
|
|
|
// 创建自定义音频输出流以便捕获音频数据 |
|
|
|
customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream() |
|
|
|
|
|
|
|
|
|
|
|
// 如果已经初始化,需要重新创建 synthesizer |
|
|
|
if (isInitialized && speechConfig != null) { |
|
|
|
synthesizer?.close() |
|
|
|
@ -194,7 +194,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
} else if (useInternalPlayer && customAudioOutputStream != null) { |
|
|
|
// 切换回使用内部播放器 |
|
|
|
customAudioOutputStream = null |
|
|
|
|
|
|
|
|
|
|
|
// 如果已经初始化,需要重新创建 synthesizer |
|
|
|
if (isInitialized && speechConfig != null) { |
|
|
|
synthesizer?.close() |
|
|
|
@ -204,16 +204,16 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置音频输出设备 |
|
|
|
* |
|
|
|
* |
|
|
|
* @param device 音频输出设备类型 |
|
|
|
*/ |
|
|
|
override fun setAudioOutputDevice(device: AudioOutputDevice) { |
|
|
|
// Azure TTS使用系统默认的音频路由 |
|
|
|
// 音频输出设备的控制需要通过Android的AudioManager实现 |
|
|
|
FileLogger.e(TAG, "音频输出设备类型device=: ${device}") |
|
|
|
FileLogger.e(TAG, "音频输出设备类型device=: ${device}") |
|
|
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager |
|
|
|
audioManager?.let { manager -> |
|
|
|
when (device) { |
|
|
|
@ -221,7 +221,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
// 默认模式:系统自动选择 |
|
|
|
manager.mode = AudioManager.MODE_NORMAL |
|
|
|
manager.isSpeakerphoneOn = false |
|
|
|
manager.startBluetoothSco() |
|
|
|
} |
|
|
|
|
|
|
|
AudioOutputDevice.SPEAKER -> { |
|
|
|
// 强制使用扬声器 |
|
|
|
/* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖) |
|
|
|
@ -230,17 +232,20 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
*/ |
|
|
|
manager.mode = AudioManager.MODE_IN_COMMUNICATION |
|
|
|
manager.isSpeakerphoneOn = true |
|
|
|
manager.stopBluetoothSco() |
|
|
|
} |
|
|
|
|
|
|
|
AudioOutputDevice.HEADPHONES -> { |
|
|
|
// 强制使用耳机(如果已连接) |
|
|
|
manager.mode = AudioManager.MODE_NORMAL |
|
|
|
manager.isSpeakerphoneOn = false |
|
|
|
manager.startBluetoothSco() |
|
|
|
// 注意:Android不能强制路由到耳机,只能在耳机已连接时使用 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 触发事件通知 |
|
|
|
*/ |
|
|
|
@ -256,25 +261,25 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置事件监听器 |
|
|
|
*/ |
|
|
|
private fun setupEventListeners() { |
|
|
|
synthesizer?.apply { |
|
|
|
// 合成开始事件 |
|
|
|
SynthesisStarted?.addEventListener { _, eventArgs -> |
|
|
|
SynthesisStarted?.addEventListener { _, eventArgs -> |
|
|
|
FileLogger.d(TAG, "语音合成开始: resultId=${eventArgs.result.resultId}") |
|
|
|
notifyEvent(TtsEventType.SYNTHESIS_STARTED) |
|
|
|
// Azure TTS 在使用默认音频输出时会立即开始播放 |
|
|
|
notifyEvent(TtsEventType.PLAYBACK_STARTED) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 合成中事件(接收音频数据) |
|
|
|
Synthesizing?.addEventListener { _, eventArgs -> |
|
|
|
val audioData = eventArgs.result.audioData |
|
|
|
FileLogger.d(TAG, "接收到音频数据: ${audioData.size} 字节") |
|
|
|
|
|
|
|
|
|
|
|
// 通知音频数据监听器 |
|
|
|
if (audioData.isNotEmpty()) { |
|
|
|
val listeners = ArrayList(audioDataListeners) |
|
|
|
@ -287,38 +292,45 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 合成完成事件 |
|
|
|
SynthesisCompleted?.addEventListener { _, eventArgs -> |
|
|
|
FileLogger.d(TAG, "语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节") |
|
|
|
SynthesisCompleted?.addEventListener { _, eventArgs -> |
|
|
|
FileLogger.d( |
|
|
|
TAG, |
|
|
|
"语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节" |
|
|
|
) |
|
|
|
isSpeaking = false |
|
|
|
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) |
|
|
|
// Azure TTS 合成完成即播放完成 |
|
|
|
notifyEvent(TtsEventType.PLAYBACK_COMPLETED) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 合成取消事件 |
|
|
|
SynthesisCanceled?.addEventListener { _, eventArgs -> |
|
|
|
SynthesisCanceled?.addEventListener { _, eventArgs -> |
|
|
|
val reason = eventArgs.result.reason |
|
|
|
FileLogger.e(TAG, "语音合成取消: reason=$reason") |
|
|
|
|
|
|
|
|
|
|
|
if (reason == ResultReason.Canceled) { |
|
|
|
val cancellation = SpeechSynthesisCancellationDetails.fromResult(eventArgs.result) |
|
|
|
FileLogger.e(TAG, "取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}") |
|
|
|
val cancellation = |
|
|
|
SpeechSynthesisCancellationDetails.fromResult(eventArgs.result) |
|
|
|
FileLogger.e( |
|
|
|
TAG, |
|
|
|
"取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}" |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isSpeaking = false |
|
|
|
notifyEvent(TtsEventType.SYNTHESIS_CANCELED) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置语音角色 |
|
|
|
*/ |
|
|
|
override fun setVoice(voiceName: String): Boolean { |
|
|
|
if (!isInitialized) return false |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
currentVoice = voiceName |
|
|
|
speechConfig?.setSpeechSynthesisVoiceName(voiceName) |
|
|
|
@ -326,21 +338,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "设置语音失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "VOICE_SET_FAILED", |
|
|
|
"errorMessage" to "设置语音失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "VOICE_SET_FAILED", |
|
|
|
"errorMessage" to "设置语音失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 重新创建合成器 |
|
|
|
*/ |
|
|
|
private fun recreateSynthesizer() { |
|
|
|
try { |
|
|
|
synthesizer?.close() |
|
|
|
|
|
|
|
|
|
|
|
// 创建音频配置 |
|
|
|
val audioConfig = if (customAudioOutputStream != null) { |
|
|
|
// 使用自定义音频输出流 |
|
|
|
@ -349,19 +363,21 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
// 使用默认扬声器 |
|
|
|
AudioConfig.fromDefaultSpeakerOutput() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 使用新的音频配置创建合成器 |
|
|
|
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) |
|
|
|
setupEventListeners() |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "RECREATE_FAILED", |
|
|
|
"errorMessage" to "重新创建合成器失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "RECREATE_FAILED", |
|
|
|
"errorMessage" to "重新创建合成器失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 预热TTS引擎(减少首次合成延迟) |
|
|
|
*/ |
|
|
|
@ -377,7 +393,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
</voice> |
|
|
|
</speak> |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
// 静音预热(音量设为0) |
|
|
|
synthesizer?.SpeakSsmlAsync(warmupSsml) |
|
|
|
} catch (e: Exception) { |
|
|
|
@ -385,6 +401,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
FileLogger.d(TAG, "TTS预热失败: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
//清洗播报语音内容 |
|
|
|
fun cleanTextForTTS(text: String): String { |
|
|
|
return text |
|
|
|
@ -394,12 +411,13 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
.replace(Regex("\\s+"), " ") // 多个空格合并 |
|
|
|
.trim() |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 设置语音参数 |
|
|
|
*/ |
|
|
|
private fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean { |
|
|
|
if (!isInitialized) return false |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
currentRate = formatPercentage(rate) |
|
|
|
currentPitch = formatPercentage(pitch) |
|
|
|
@ -407,28 +425,30 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "设置语音参数失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "PARAMS_SET_FAILED", |
|
|
|
"errorMessage" to "设置语音参数失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "PARAMS_SET_FAILED", |
|
|
|
"errorMessage" to "设置语音参数失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化百分比值 |
|
|
|
*/ |
|
|
|
private fun formatPercentage(value: Int): String { |
|
|
|
return if (value >= 0) "+$value%" else "$value%" |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 生成SSML |
|
|
|
*/ |
|
|
|
private fun generateSsml(rawText: String): String { |
|
|
|
// 1. 定义要静音的符号和表情符号列表 |
|
|
|
val symbolsToMute = listOf( |
|
|
|
"#", "*", |
|
|
|
"#", "*", |
|
|
|
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" |
|
|
|
) |
|
|
|
|
|
|
|
@ -461,7 +481,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
</speak> |
|
|
|
""".trimIndent() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 生成优化的SSML(减少复杂度提升速度) |
|
|
|
*/ |
|
|
|
@ -487,74 +507,81 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
</speak> |
|
|
|
""".trimIndent() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 单次播放文本(非流式) |
|
|
|
*/ |
|
|
|
override fun speakOnce(text: String): Boolean { |
|
|
|
if (!isInitialized) { |
|
|
|
FileLogger.e(TAG, "TTS引擎未初始化") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
// 优化:使用简化的SSML生成 |
|
|
|
val ssml = generateOptimizedSsml(text) |
|
|
|
|
|
|
|
|
|
|
|
isSpeaking = true |
|
|
|
|
|
|
|
|
|
|
|
FileLogger.d(TAG, "开始语音合成,文本长度: ${text.length}") |
|
|
|
|
|
|
|
|
|
|
|
// 直接调用SpeakSsmlAsync,SDK内部已经是异步的 |
|
|
|
synthesizer?.SpeakSsmlAsync(ssml) |
|
|
|
|
|
|
|
|
|
|
|
// 返回true表示已开始合成请求 |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
isSpeaking = false |
|
|
|
FileLogger.e(TAG, "语音合成异常: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "SYNTHESIS_ERROR", |
|
|
|
"errorMessage" to "语音合成异常: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "SYNTHESIS_ERROR", |
|
|
|
"errorMessage" to "语音合成异常: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 流式合成文本 |
|
|
|
*/ |
|
|
|
override fun speakStream(text: String): Boolean { |
|
|
|
if (!isInitialized || text.isBlank()) { |
|
|
|
if (!isInitialized) { |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
// 添加新文本到缓冲区 |
|
|
|
streamBuffer.append(text) |
|
|
|
|
|
|
|
|
|
|
|
// 优化:大幅减少防抖时间从600ms到150ms |
|
|
|
val currentTime = System.currentTimeMillis() |
|
|
|
if (currentTime - lastSpeakTime < 150) { |
|
|
|
return true |
|
|
|
} |
|
|
|
lastSpeakTime = currentTime |
|
|
|
|
|
|
|
|
|
|
|
val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString()) |
|
|
|
|
|
|
|
|
|
|
|
// 优化:使用字符集合替代列表,提升查找效率 |
|
|
|
val punctuationSet = setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') |
|
|
|
|
|
|
|
val punctuationSet = |
|
|
|
setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') |
|
|
|
|
|
|
|
// 优化:从后往前查找最后一个标点符号 |
|
|
|
var lastPunctuationIndex = -1 |
|
|
|
for (i in currentText.length - 1 downTo 0) { |
|
|
|
@ -563,73 +590,79 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 如果找到标点符号,则播放到该标点符号 |
|
|
|
if (lastPunctuationIndex >= 0) { |
|
|
|
// 提取要播放的文本(包含标点符号) |
|
|
|
val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) |
|
|
|
|
|
|
|
|
|
|
|
// 剩余的文本保存在缓冲区中 |
|
|
|
streamBuffer.delete(0, lastPunctuationIndex + 1) |
|
|
|
|
|
|
|
|
|
|
|
// 播放提取的文本 |
|
|
|
return speakOnce(textToSpeak) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到标点符号,则等待更多文本 |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "流式语音合成失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "STREAM_FAILED", |
|
|
|
"errorMessage" to "流式语音合成失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "STREAM_FAILED", |
|
|
|
"errorMessage" to "流式语音合成失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 播放剩余的流式文本 |
|
|
|
*/ |
|
|
|
override fun flushStream(): Boolean { |
|
|
|
if (!isInitialized) { |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "NOT_INITIALIZED", |
|
|
|
"errorMessage" to "TTS引擎未初始化" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
// 获取缓冲区中剩余的文本 |
|
|
|
val remainingText = streamBuffer.toString() |
|
|
|
|
|
|
|
|
|
|
|
// 清空缓冲区 |
|
|
|
streamBuffer.clear() |
|
|
|
|
|
|
|
|
|
|
|
// 如果缓冲区为空,直接返回成功 |
|
|
|
if (remainingText.isBlank()) { |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 播放剩余文本 |
|
|
|
return speakOnce(remainingText) |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "FLUSH_FAILED", |
|
|
|
"errorMessage" to "刷新流式文本失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "FLUSH_FAILED", |
|
|
|
"errorMessage" to "刷新流式文本失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 停止当前语音合成 |
|
|
|
*/ |
|
|
|
override fun stop(): Boolean { |
|
|
|
if (!isInitialized) return false |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
synthesizer?.StopSpeakingAsync() |
|
|
|
streamBuffer.clear() |
|
|
|
@ -638,21 +671,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
FileLogger.e(TAG, "停止语音合成失败: ${e.message}") |
|
|
|
notifyEvent(TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "STOP_FAILED", |
|
|
|
"errorMessage" to "停止语音合成失败: ${e.message}" |
|
|
|
)) |
|
|
|
notifyEvent( |
|
|
|
TtsEventType.ERROR, mapOf( |
|
|
|
"errorCode" to "STOP_FAILED", |
|
|
|
"errorMessage" to "停止语音合成失败: ${e.message}" |
|
|
|
) |
|
|
|
) |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 释放资源 |
|
|
|
*/ |
|
|
|
override fun release() { |
|
|
|
dispose() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 释放资源(内部方法) |
|
|
|
*/ |
|
|
|
@ -660,15 +695,15 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
try { |
|
|
|
// 停止播放 |
|
|
|
stop() |
|
|
|
|
|
|
|
|
|
|
|
// 关闭合成器 |
|
|
|
synthesizer?.close() |
|
|
|
synthesizer = null |
|
|
|
|
|
|
|
|
|
|
|
// 关闭配置 |
|
|
|
speechConfig?.close() |
|
|
|
speechConfig = null |
|
|
|
|
|
|
|
|
|
|
|
// 关闭自定义音频输出流 |
|
|
|
try { |
|
|
|
customAudioOutputStream?.close() |
|
|
|
@ -676,10 +711,10 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
} catch (e: Exception) { |
|
|
|
// 忽略关闭异常 |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清空流缓冲区 |
|
|
|
streamBuffer.clear() |
|
|
|
|
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
isInitialized = false |
|
|
|
isSpeaking = false |
|
|
|
@ -689,7 +724,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { |
|
|
|
isSpeaking = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 是否正在播放 |
|
|
|
*/ |
|
|
|
|