From 38c7f8ff4ce119461573bb824a1c28ee96bd0d4e Mon Sep 17 00:00:00 2001 From: wolfplus2048 <30993207+wolfplus2048@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:15:15 +0100 Subject: [PATCH] add --- .../azure_speech/AzureTtsHelper.kt | 34 ++ .../bytedance_speech/BytedanceAudioPlayer.kt | 342 +++++++----------- .../bytedance_speech/BytedanceTTS.kt | 177 ++++----- .../chat_api/ChatApiService.kt | 3 +- .../com/deep_voice/speech/ITtsService.kt | 8 + 5 files changed, 240 insertions(+), 324 deletions(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 62ac83d49..9b3d8ccfe 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -169,6 +169,40 @@ class AzureTtsHelper(private val context: Context) : ITtsService { audioDataListeners.remove(listener) } + /** + * 设置是否使用内部播放器 + * + * @param useInternalPlayer true: 使用内部播放器自动播放音频 + * false: 仅通过音频数据监听器输出数据,不播放 + */ + override fun setUseInternalPlayer(useInternalPlayer: Boolean) { + // Azure TTS 通过 AudioConfig 控制播放器 + // 如果不使用内部播放器,需要重新配置 synthesizer + if (!useInternalPlayer && customAudioOutputStream == null) { + // 创建自定义音频输出流以便捕获音频数据 + customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream() + + // 如果已经初始化,需要重新创建 synthesizer + if (isInitialized && speechConfig != null) { + synthesizer?.close() + val audioConfig = AudioConfig.fromStreamOutput(customAudioOutputStream) + synthesizer = SpeechSynthesizer(speechConfig, audioConfig) + setupEventListeners() + } + } else if (useInternalPlayer && customAudioOutputStream != null) { + // 切换回使用内部播放器 + customAudioOutputStream = null + + // 如果已经初始化,需要重新创建 synthesizer + if (isInitialized && speechConfig != null) { + synthesizer?.close() + val audioConfig = AudioConfig.fromDefaultSpeakerOutput() + synthesizer = SpeechSynthesizer(speechConfig, audioConfig) + setupEventListeners() + } + } + } + /** * 触发事件通知 */ diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt index 9587f401f..84e73b1be 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt @@ -7,112 +7,134 @@ import android.media.AudioTrack import android.os.Build import android.util.Log import com.deep_voice.speech.tts.AudioDataListener -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.CancellationException + /** - * 字节跳动语音合成音频播放器 - * - * 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理 + * 简化的音频流播放器 + * 基于位置标记触发播放完成回调 */ -class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { +class BytedanceAudioPlayer : AudioDataListener { companion object { private const val TAG = "BytedanceAudioPlayer" - private const val SAMPLE_RATE = 24000 // 样本率 - private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道 - private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM - private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间 + private const val SAMPLE_RATE = 24000 + private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT } - // 协程相关 - private val job = SupervisorJob() - override val coroutineContext = Dispatchers.IO + job - - // 音频处理相关 private var audioTrack: AudioTrack? = null - private val isPlaying = AtomicBoolean(false) - private val isPaused = AtomicBoolean(false) - - // 播放状态 - private val playStarted = AtomicBoolean(false) - private val completionReported = AtomicBoolean(false) + private var isFirstData = true // 是否是第一次接收数据 + private var totalBytesWritten = 0 // 总共写入的字节数 + private var sessionActive = true // 会话是否活跃 - // 使用简单的并发队列,保证线程安全 - private val audioDataQueue = ConcurrentLinkedQueue() - - // 播放状态监听 + // 回调 private var onPlayStarted: (() -> Unit)? = null private var onPlayCompleted: (() -> Unit)? = null - private var onError: ((String) -> Unit)? = null - // 处理协程 - private var processingJob: Job? = null - private var lastDataTime = 0L + // 播放位置监听器 + private val playbackListener = object : AudioTrack.OnPlaybackPositionUpdateListener { + override fun onMarkerReached(track: AudioTrack) { + Log.d(TAG, "播放到达标记位置: ${track.playbackHeadPosition}") + onPlayCompleted?.invoke() + } + + override fun onPeriodicNotification(track: AudioTrack) { + // 不使用周期性通知 + } + } - init { - Log.d(TAG, "BytedanceAudioPlayer初始化") + + /** + * 开始新的播放会话 + */ + fun startSession() { + Log.d(TAG, "开始新会话") + + // 重置状态 + isFirstData = true + totalBytesWritten = 0 + sessionActive = true + + // 重置 AudioTrack + audioTrack?.let { track -> + if (track.state == AudioTrack.STATE_INITIALIZED) { + // 停止并清空缓冲区 + track.pause() + track.flush() + // 重新开始播放 + track.play() + Log.d(TAG, "AudioTrack 已重置") + } + } ?: initAudioTrack() // 如果没有初始化,则初始化 } /** - * 开始播放 + * 标记数据流结束 */ - fun start() { - if (isPlaying.getAndSet(true)) return + fun endSession() { + if (!sessionActive) return - isPaused.set(false) - playStarted.set(false) - completionReported.set(false) - lastDataTime = System.currentTimeMillis() + Log.d(TAG, "数据流结束,总共写入字节数: $totalBytesWritten") + sessionActive = false - processingJob = launch { - try { - initAudioTrack() + audioTrack?.let { track -> + if (totalBytesWritten > 0) { + // 计算总帧数(16-bit 单声道,每帧2字节) + val totalFrames = totalBytesWritten / 2 - // 处理队列中的音频数据 - while (isActive && isPlaying.get()) { - processQueuedAudio() - - // 检查是否播放完成 - checkPlaybackCompletion() - - // 队列为空时短暂延迟 - if (audioDataQueue.isEmpty()) { - delay(10) - } + // 设置标记位置 + try { + track.setNotificationMarkerPosition(totalFrames) + Log.d(TAG, "设置播放完成标记位置: $totalFrames") + } catch (e: Exception) { + Log.e(TAG, "设置标记失败: ${e.message}") + // 设置失败时,使用延迟触发作为后备 + val durationMs = (totalFrames * 1000L) / SAMPLE_RATE + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + onPlayCompleted?.invoke() + }, durationMs + 500) } - } catch (e: CancellationException) { - // 协程被取消,正常行为 - } catch (e: Exception) { - Log.e(TAG, "播放错误: ${e.message}") - onError?.invoke("播放错误: ${e.message}") - stopInternal(false) + } else { + // 没有数据,直接触发完成 + onPlayCompleted?.invoke() } } } /** - * 暂停播放 + * 接收音频数据 */ - fun pause() { - if (!isPlaying.get() || isPaused.getAndSet(true)) return - audioTrack?.pause() - } - - /** - * 恢复播放 - */ - fun resume() { - if (!isPlaying.get() || !isPaused.getAndSet(false)) return - audioTrack?.play() + override fun onAudioData(data: ByteArray) { + if (data.isEmpty()) return + if (!sessionActive) { + Log.w(TAG, "收到音频数据但会话未激活,忽略数据") + return + } + if (audioTrack == null || audioTrack?.state != AudioTrack.STATE_INITIALIZED) { + Log.w(TAG, "收到音频数据但 AudioTrack 未初始化或状态不正确,忽略数据") + return + } + try { + audioTrack?.let { track -> + if (track.state == AudioTrack.STATE_INITIALIZED) { + val bytesWritten = track.write(data, 0, data.size) + + if (bytesWritten > 0) { + // 第一次写入数据时自动触发开始回调 + if (isFirstData) { + isFirstData = false + onPlayStarted?.invoke() + Log.d(TAG, "播放开始") + } + + // 累计写入字节数 + if (sessionActive) { + totalBytesWritten += bytesWritten + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "写入音频数据失败: ${e.message}") + } } /** @@ -120,44 +142,32 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { */ fun stop() { Log.d(TAG, "停止播放") - stopInternal(true) - } - - /** - * 内部停止处理 - */ - private fun stopInternal(reportCompletion: Boolean) { - if (!isPlaying.getAndSet(false)) return - - isPaused.set(false) - processingJob?.cancel() - - audioTrack?.stop() - // 不在stopInternal中释放资源,只停止播放 - // 清空队列 - audioDataQueue.clear() + sessionActive = false - // 播放完成通知 - if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) { - launch(Dispatchers.Main) { - onPlayCompleted?.invoke() + audioTrack?.let { track -> + if (track.state == AudioTrack.STATE_INITIALIZED) { + track.pause() + track.flush() } } + + // 如果已经开始播放,触发完成回调 + if (!isFirstData) { + onPlayCompleted?.invoke() + } } /** * 释放资源 */ fun release() { - stopInternal(false) + Log.d(TAG, "释放资源") + + stop() - // 在release方法中释放AudioTrack资源 audioTrack?.release() audioTrack = null - - // 释放协程资源 - job.cancel() } /** @@ -175,120 +185,32 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { } /** - * 设置错误回调 + * 设置错误回调(兼容接口) */ fun setOnError(listener: (String) -> Unit) { - onError = listener - } - - /** - * 接收音频数据(实现AudioDataListener接口) - */ - override fun onAudioData(data: ByteArray) { - if (data.isEmpty()) return - - lastDataTime = System.currentTimeMillis() - - if (!isPlaying.get()) { - start() - } - - // 添加到队列 - 简单有效,保证FIFO顺序 - audioDataQueue.add(data.copyOf()) + // 不实现,仅为兼容 } /** * 检查是否正在播放 */ - fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get() - - /** - * 处理队列中的音频数据 - */ - private suspend fun processQueuedAudio() { - if (isPaused.get() || !isPlaying.get() || !isActive) return - - // 获取并处理队列中的数据 - val data = audioDataQueue.poll() ?: return - - // 写入音频数据 - val result = audioTrack?.write(data, 0, data.size) ?: 0 - - if (result > 0) { - // 标记播放开始 - if (!playStarted.getAndSet(true)) { - withContext(Dispatchers.Main) { - onPlayStarted?.invoke() - } - } - } else if (result < 0) { - // 处理错误 - handleAudioTrackError(result) - } + fun isPlaying(): Boolean { + return audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING } /** - * 检查播放是否完成(超时无数据) - */ - private suspend fun checkPlaybackCompletion() { - val currentTime = System.currentTimeMillis() - - // 超时判断 - 队列为空且超过超时时间 - if (playStarted.get() && audioDataQueue.isEmpty() && - currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) { - - // 播放完成 - if (!completionReported.getAndSet(true)) { - withContext(Dispatchers.Main) { - onPlayCompleted?.invoke() - } - stopInternal(false) // 已报告,不需要再次报告 - } - } - } - - /** - * 处理AudioTrack错误 - */ - private fun handleAudioTrackError(result: Int) { - val errorMsg = when(result) { - AudioTrack.ERROR_INVALID_OPERATION -> "无效操作" - AudioTrack.ERROR_BAD_VALUE -> "参数错误" - AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁" - else -> "未知错误" - } - - // 对象已销毁时尝试重建 - if (result == AudioTrack.ERROR_DEAD_OBJECT) { - try { - audioTrack?.release() - initAudioTrack() - } catch (e: Exception) { - Log.e(TAG, "重建AudioTrack失败: ${e.message}") - throw e - } - } else { - Log.e(TAG, "AudioTrack错误: $errorMsg") - } - } - - /** - * 初始化AudioTrack + * 初始化 AudioTrack */ private fun initAudioTrack() { - val minBufferSize = AudioTrack.getMinBufferSize( - SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT - ) - - if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) { - throw IllegalStateException("无法获取有效的音频缓冲区大小") - } + // 释放旧实例 + audioTrack?.release() - // 使用较大的缓冲区提高稳定性 - val bufferSize = minBufferSize * 4 + // 计算缓冲区大小 + val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) + val bufferSize = minBufferSize * 2 - // 创建AudioTrack - audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0) + // 创建 AudioTrack + audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { AudioTrack.Builder() .setAudioAttributes( AudioAttributes.Builder() @@ -318,12 +240,12 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { ) } - // 检查初始化状态 - if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) { - throw IllegalStateException("AudioTrack初始化失败") - } + // 设置播放位置监听器 + audioTrack?.setPlaybackPositionUpdateListener(playbackListener) // 开始播放 audioTrack?.play() + + Log.d(TAG, "AudioTrack 初始化成功") } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index d202a36e4..49d83efcc 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -100,6 +100,9 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { // 内部音频播放器 private val audioPlayer = BytedanceAudioPlayer() + // 是否使用内部播放器 + private var useInternalPlayer = true + // WebSocket连接 private var webSocket: WebSocket? = null private val client: OkHttpClient @@ -119,9 +122,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { private var language: String = "zh-CN" // 流式处理状态 - private var isStreamMode = false private var currentStatus = STATUS_STOPPED - private var isSpeaking = false // 连接相关状态 private var connectionAttempts = 0 @@ -155,12 +156,8 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { * 初始化音频播放器回调 */ private fun initAudioPlayerCallbacks() { - // 添加播放器作为音频数据监听器 - addAudioDataListener(audioPlayer) - audioPlayer.setOnPlayStarted { Log.d(TAG, "播放开始") - isSpeaking = true updateStatus(STATUS_SPEAKING) notifyEvent(TtsEventType.SYNTHESIS_STARTED) } @@ -168,17 +165,18 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { // 设置播放完成回调 audioPlayer.setOnPlayCompleted { Log.d(TAG, "播放结束") - isSpeaking = false - updateStatus(if (isStreamMode) STATUS_READY else STATUS_STOPPED) + updateStatus(STATUS_READY) notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) } // 设置错误回调 audioPlayer.setOnError { errorMsg -> - isSpeaking = false updateStatus(STATUS_ERROR) notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "PLAYER_ERROR", "errorMessage" to errorMsg)) } + + // 根据设置决定是否使用内部播放器 + updateInternalPlayerUsage() } /** @@ -208,16 +206,19 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { */ override fun stop(): Boolean { try { - audioPlayer.stop() + Log.i(TAG, ">stop()") + + if (useInternalPlayer) { + audioPlayer.stop() + } textProcessingJob?.cancel() - launch { - if (isSessionStarted) { - finishSession() - } - isSpeaking = false // 显式设置,因为这是强制停止 - updateStatus(STATUS_STOPPED) + + if (isSessionStarted) { + finishSession() } + updateStatus(STATUS_STOPPED) + return true } catch (e: Exception) { Log.e(TAG, "停止合成失败: ${e.message}") @@ -277,7 +278,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { isConnected = false isSessionStarted = false - isSpeaking = false updateStatus(STATUS_STOPPED) startTextProcessing() @@ -304,29 +304,9 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { * 单次播放(非流式) */ override fun speakOnce(text: String): Boolean { - try { - if (text.isEmpty()) { - return false - } - - isStreamMode = false - - launch { - if (!ensureSessionReady()) { - notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "SESSION_ERROR", - "errorMessage" to "无法建立会话")) - return@launch - } - - sendTextRequest(text) - } - - return true - } catch (e: Exception) { - Log.e(TAG, "语音合成失败: ${e.message}") - updateStatus(STATUS_ERROR) - return false - } + // 未实现:只支持流式模式 + Log.w(TAG, "speakOnce未实现,请使用speakStream") + return false } /** @@ -337,7 +317,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { if (text.isEmpty()) { return true } - isStreamMode = true // 确保会话已就绪 if (!isSessionStarted && !ensureSessionReady()) { @@ -365,17 +344,11 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { */ override fun flushStream(): Boolean { try { - if (!isStreamMode) return true // Log.d(TAG, "flushStream: $sessionId") - launch { - withContext(Dispatchers.IO) { - delay(300) // 确保现有文本处理完成 - } - - if (isSessionStarted) { - isSessionStarted = false - finishSession() - } + + if (isSessionStarted) { + isSessionStarted = false + finishSession() } return true @@ -440,6 +413,29 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { audioDataListeners.remove(listener) } + /** + * 设置是否使用内部播放器 + */ + override fun setUseInternalPlayer(useInternalPlayer: Boolean) { + this.useInternalPlayer = useInternalPlayer + updateInternalPlayerUsage() + } + + /** + * 更新内部播放器的使用状态 + */ + private fun updateInternalPlayerUsage() { + if (useInternalPlayer) { + // 使用内部播放器 + if (!audioDataListeners.contains(audioPlayer)) { + audioDataListeners.add(audioPlayer) + } + } else { + // 不使用内部播放器 + audioDataListeners.remove(audioPlayer) + } + } + /** * 通知事件处理 */ @@ -461,15 +457,14 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { * 通知音频数据 */ private fun notifyAudioData(data: ByteArray) { - launch(Dispatchers.Main) { - for (listener in audioDataListeners) { - try { - listener.onAudioData(data) - } catch (e: Exception) { - Log.e(TAG, "音频数据回调异常: ${e.message}") - } + for (listener in audioDataListeners) { + try { + listener.onAudioData(data) + } catch (e: Exception) { + Log.e(TAG, "音频数据回调异常: ${e.message}") } } + } /** @@ -483,7 +478,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { // 如果已连接但会话未开始,只需要开始会话 if (isConnected && !isSessionStarted) { - Log.d(TAG, "WebSocket已连接,正在启动新会话...") + // Log.d(TAG, "WebSocket已连接,正在启动新会话...") return startSessionOnly() } @@ -648,7 +643,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { "errorMessage" to errorMsg)) webSocket.close(1000, "Error") updateStatus(STATUS_ERROR) - isSpeaking = false // 错误情况下显式设置 isSessionStarted = false isConnected = false isConnecting.set(false) @@ -656,6 +650,10 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { EVENT_SESSION_STARTED -> { connectionAttempts = 0 + // 会话开始时,如果使用内部播放器则启动播放器会话 + if (useInternalPlayer) { + audioPlayer.startSession() + } } EVENT_TTS_RESPONSE -> { @@ -673,16 +671,18 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { EVENT_SESSION_FINISHED -> { isSessionStarted = false - - if (!isStreamMode) { - finishConnection(webSocket) + Log.i(TAG, "EVENT_SESSION_FINISHED, ${sessionId}") + // 如果使用内部播放器则结束播放器会话 + if (useInternalPlayer) { + audioPlayer.endSession() } + + // 流式模式下不自动关闭连接 } } } catch (e: Exception) { Log.e(TAG, "解析响应失败: ${e.message}") updateStatus(STATUS_ERROR) - isSpeaking = false // 错误情况下显式设置 isConnecting.set(false) } } @@ -690,7 +690,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { isConnected = false isSessionStarted = false - isSpeaking = false isConnecting.set(false) Log.e(TAG, "WebSocket连接失败: ${t.message}") @@ -702,7 +701,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { isConnected = false isSessionStarted = false - isSpeaking = false isConnecting.set(false) updateStatus(STATUS_STOPPED) notifyEvent(TtsEventType.SYNTHESIS_CANCELED) @@ -798,53 +796,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { sendEvent(webSocket, header, optional, payload) } - /** - * 发送文本进行合成 (非流式) - */ - private fun sendTextRequest(text: String) { - webSocket?.let { ws -> - val header = Header( - protocolVersion = PROTOCOL_VERSION, - headerSize = DEFAULT_HEADER_SIZE, - messageType = FULL_CLIENT_REQUEST, - messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT, - serializationMethod = JSON, - messageCompression = COMPRESSION_NO, - reserved = 0 - ) - - val optional = Optional( - event = EVENT_TASK_REQUEST, - sessionId = sessionId - ) - - val jsonObject = JSONObject() - val user = JSONObject() - user.put("uid", "123456") - jsonObject.put("user", user) - jsonObject.put("event", EVENT_TASK_REQUEST) - jsonObject.put("namespace", "BidirectionalTTS") - - val reqParams = JSONObject() - // 将#和*替换为空格 - val processedText = text.replace("#", " ").replace("*", " ") - reqParams.put("text", processedText) - reqParams.put("speaker", speaker) - - val audioParams = JSONObject() - audioParams.put("format", format) - audioParams.put("sample_rate", sampleRate) - audioParams.put("emotion", "happy") - - reqParams.put("audio_params", audioParams) - jsonObject.put("req_params", reqParams) - - val payload = jsonObject.toString().toByteArray() - Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}") - - sendEvent(ws, header, optional, payload) - } - } /** * 发送文本进行合成 (流式) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 3e3d056db..13f74a898 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -157,7 +157,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor currentStreamJob = null // 2. 通知旧会话被中止 - currSessionCallback?.onError(ChatApiException("Session aborted by new request")) + // currSessionCallback?.onError(ChatApiException("Session aborted by new request")) + currSessionCallback?.onComplete() // 直接完成当前会话 // 3. 清理状态 currSessionId = "" diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt index 91c8945d6..d43604582 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt @@ -89,4 +89,12 @@ interface ITtsService { * @param listener 要移除的音频数据监听器 */ fun removeAudioDataListener(listener: AudioDataListener) + + /** + * 设置是否使用内部播放器 + * + * @param useInternalPlayer true: 使用内部播放器自动播放音频 + * false: 仅通过音频数据监听器输出数据,不播放 + */ + fun setUseInternalPlayer(useInternalPlayer: Boolean) } \ No newline at end of file