diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 03bce6c62..0a003e349 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -155,6 +155,7 @@ class AgentController extends GetxController { @override void onClose() { + AgentService.stopAiSteam(); //停止AI流 AgentService.stopConversation(); // 停止当前会话 AgentService.stopTts(); // 停止当前语音 textController.dispose(); diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 0ebed9187..877ddad5b 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -161,7 +161,7 @@ class TranslationController extends GetxController { } // 加载历史记录 - loadTranslationHistory(); + //loadTranslationHistory(); // 初始化服务 _initServices(); @@ -469,6 +469,7 @@ class TranslationController extends GetxController { isIntermediate: true, ); // 添加新项后滚动到底部 + translationHistory.add(newItem); _scrollToBottom(); } else { // 更新当前项 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index fdd51c709..7a9e15d16 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -26,6 +26,7 @@ import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.bytedance_speech.BytedanceTTS import java.util.UUID +import com.yunqiinnovation.agent_service.Utils /** * 代理服务事件监听接口 @@ -571,7 +572,7 @@ object AgentService : CoroutineScope { /** * 停止AI流输出 */ - private fun stopAiStream() { + fun stopAiStream() { if (isAiStreaming) { try { // 先更新状态,避免回调时的状态不一致 @@ -580,7 +581,8 @@ object AgentService : CoroutineScope { // 取消当前AI生成任务 currentAiJob?.cancel() currentAiJob = null - + currsessionId = "" //取消会话 + // 通知ChatAPI服务终止当前流式请求 chatApiService.cancelCurrentStream() @@ -756,9 +758,6 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && broadcast) { ttsService?.speakStream(token) - if (token.length > 0) { - audioPlayer?.stopAudio() - } } if (broadcast) { // 发送流式回复token @@ -774,6 +773,7 @@ object AgentService : CoroutineScope { // 视情况决定是否朗读回复 if (speakResponse && broadcast && sessionid == currsessionId){ ttsService?.flushStream() + audioPlayer?.stopAudio() } val response = responseBuilder.toString() // 发送完整回复,包含是否有图片的标记 @@ -1164,6 +1164,22 @@ object AgentService : CoroutineScope { class AudioPlayer(private val context: Context) { private var mediaPlayer: MediaPlayer? = null private var isInitialized = false + private val playbackLock = Object() // 添加同步锁 + + /** + * 安全检查 MediaPlayer 是否正在播放 + */ + private fun isMediaPlayerPlaying(): Boolean { + return try { + mediaPlayer?.isPlaying == true + } catch (e: IllegalStateException) { + Log.w(TAG, "MediaPlayer 状态异常,假定未播放: ${e.message}") + false + } catch (e: Exception) { + Log.w(TAG, "检查播放状态时发生异常: ${e.message}") + false + } + } /** * 播放音频资源 @@ -1172,75 +1188,167 @@ object AgentService : CoroutineScope { * @param volume 音量大小,范围0.0-1.0,默认1.0 */ fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { - try { - // 释放之前的资源 - release() + synchronized(playbackLock) { + try { + // 强制停止并释放之前的资源 + forceStop() - // 创建播放器并设置资源 - mediaPlayer = MediaPlayer().apply { - // 设置资源 - context.resources.openRawResourceFd(resId)?.use { fd -> - setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) - } + // 创建播放器并设置资源 + mediaPlayer = MediaPlayer().apply { + // 设置资源 + context.resources.openRawResourceFd(resId)?.use { fd -> + setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) + } - this.isLooping = isLooping // 设置循环属性 - setVolume(volume, volume) // 设置音量(左声道,右声道) + this.isLooping = isLooping // 设置循环属性 + setVolume(volume, volume) // 设置音量(左声道,右声道) - setOnCompletionListener { - if (!isLooping) { - release() + setOnCompletionListener { + if (!isLooping) { + release() + } + } + + setOnErrorListener { _, what, extra -> + Log.e(TAG, "MediaPlayer 错误: what=$what, extra=$extra") + forceRelease() + true // 返回true表示错误已处理 } - } - prepare() - start() + prepare() + start() + Log.d(TAG, "开始播放音频资源: $resId, 循环: $isLooping") + } + isInitialized = true + } catch (e: Exception) { + Log.e(TAG, "播放音频资源异常: ${e.message}", e) + forceRelease() } - } catch (e: Exception) { - Log.e(TAG, "播放音频资源异常: ${e.message}", e) - release() } } + /** + * 停止音频播放 + */ fun stopAudio() { - try { - mediaPlayer?.apply { - when { - isPlaying -> { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + if (isMediaPlayerPlaying()) { stop() Log.d(TAG, "音频已停止") - } - else -> { + } else { Log.d(TAG, "音频未在播放状态,无需停止") } } + } catch (e: IllegalStateException) { + Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e) + forceRelease() + } catch (e: Exception) { + Log.e(TAG, "停止音频播放异常: ${e.message}", e) + forceRelease() } - } catch (e: IllegalStateException) { - Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e) - // 重置 MediaPlayer - release() - } catch (e: Exception) { - Log.e(TAG, "停止音频播放异常: ${e.message}", e) } } /** - * 释放资源 + * 强制停止播放(不释放资源) */ - fun release() { + private fun forceStop() { try { mediaPlayer?.apply { - if (isPlaying) { - stop() + if (isMediaPlayerPlaying()) { + try { + stop() + Log.d(TAG, "强制停止音频播放") + } catch (e: Exception) { + Log.w(TAG, "强制停止时异常: ${e.message}") + } + } + try { + reset() + } catch (e: Exception) { + Log.w(TAG, "重置时异常: ${e.message}") } - reset() - release() } - mediaPlayer = null - isInitialized = false } catch (e: Exception) { - Log.e(TAG, "释放音频资源异常: ${e.message}", e) - mediaPlayer = null - isInitialized = false + Log.w(TAG, "强制停止过程中异常: ${e.message}") + } + } + + /** + * 释放资源 + */ + fun release() { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + // 安全停止播放 + if (isMediaPlayerPlaying()) { + try { + stop() + } catch (e: IllegalStateException) { + Log.w(TAG, "停止播放时状态异常: ${e.message}") + } + } + + // 安全重置 + try { + reset() + } catch (e: IllegalStateException) { + Log.w(TAG, "重置 MediaPlayer 时状态异常: ${e.message}") + } + + // 安全释放 - 修复递归调用问题 + try { + release() // 这里调用的是 MediaPlayer.release(),不是递归 + } catch (e: Exception) { + Log.w(TAG, "释放 MediaPlayer 时异常: ${e.message}") + } + } + } catch (e: Exception) { + Log.e(TAG, "释放音频资源异常: ${e.message}", e) + } finally { + // 确保变量被清理 + mediaPlayer = null + isInitialized = false + } + } + } + + /** + * 强制释放资源,不进行状态检查 + */ + private fun forceRelease() { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + try { + reset() + } catch (e: Exception) { + Log.w(TAG, "重置 MediaPlayer 失败: ${e.message}") + } + try { + release() + } catch (e: Exception) { + Log.w(TAG, "释放 MediaPlayer 失败: ${e.message}") + } + } + } catch (e: Exception) { + Log.w(TAG, "强制释放资源时发生异常: ${e.message}") + } finally { + mediaPlayer = null + isInitialized = false + } + } + } + + /** + * 检查是否正在播放 + */ + fun isPlaying(): Boolean { + synchronized(playbackLock) { + return isMediaPlayerPlaying() } } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 64353c834..44f6013e8 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -177,6 +177,16 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) } } + "stopAiSteam" -> { + try { + AgentService.stopAiStream() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "停止语音合成失败", e) + result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) + } + } + "clearChatHistory" -> { try { AgentService.clearChatHistory { success -> diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index fc1579a05..61cf6b5b1 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -293,6 +293,16 @@ class AgentService { } } + /// 停止语音合成 + static Future stopAiSteam() async { + try { + final bool result = await _channel.invokeMethod('stopAiSteam'); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details); + } + } + /// 停止语音合成 static Future stopTts() async { try { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 919247837..a450c0d8a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -80,7 +80,29 @@ class AzureAsrHelper(private val context: Context) { /** 使用外部提供的音频数据 */ EXTERNAL } - + /** + * 获取设备支持的最佳音频格式 + * 优先选择16000Hz,若不支持则降级到8000Hz + */ + private fun getOptimalAudioFormat(): AudioStreamFormat { + // 支持的采样率列表(按优先级排序) + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + val bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + + Log.i(tag, "使用采样率: ${sampleRate}Hz") + + // 创建对应的音频格式 + return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) + } /** * 初始化Azure语音服务 * @@ -106,7 +128,7 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() // 1. 创建可控制的音频流 - val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1) + val format = getOptimalAudioFormat() audioStream = AudioInputStream.createPushStream(format) // 初始化音频管理器 audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -157,7 +179,7 @@ class AzureAsrHelper(private val context: Context) { // 录音文件类 recordfile = RecordFile; //进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请 - // 创建识别器 + // // 创建识别器 // val setupSuccess = setupRecognizer() // if (setupSuccess) { // // 优化:初始化完成后进行预热 @@ -369,6 +391,8 @@ class AzureAsrHelper(private val context: Context) { audioRecord?.startRecording() startCaptureThread() // 再启动数据读取线程 isContinuousRecognitionActive = true +Log.d(tag, "识别器创建成功: ${recognizer != null}") +Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被调用") return true } catch (e: Exception) { @@ -705,8 +729,19 @@ class AzureAsrHelper(private val context: Context) { * 开始麦克风捕获 */ private fun initAudioRecord() { - // 设置音频参数 - val sampleRate = 16000 // 16kHz采样率(语音识别常用) + // 获取已确定的采样率 + val format = getOptimalAudioFormat() + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + val bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入 val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式 @@ -732,35 +767,42 @@ class AzureAsrHelper(private val context: Context) { } // 3. 单独封装线程启动逻辑 - private fun startCaptureThread() { - captureThread = Thread { - // 循环条件确保录音已开始 - - val buffer = ByteArray(1024) // 4KB数据缓冲区 - while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { - // 从麦克风读取数据 - val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 - - // 仅在未暂停时处理数据 - if (!isPaused && bytesRead > 0) { - // 写入音频流(可能是网络传输或本地处理) - audioStream?.write(buffer) - // 保存到WAV文件(如果启用了录制功能) - recordfile?.saveAudioDataToWav(buffer) - } - - // 暂停时短暂休眠以减少CPU占用 - if (isPaused) { - try { - Thread.sleep(50) - } catch (e: InterruptedException) { - break // 线程被中断时退出循环 - } - } + private fun startCaptureThread() { + captureThread = Thread { + // 根据实际采样率计算缓冲区大小 + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + var bufferSize = 0 + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + // (sampleRate * 0.032).toInt() * 2 // 32ms数据量 + + val buffer = ByteArray(bufferSize) + + while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + + // 读取音频 + val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 +Log.i("AzureASR", "AudioRecord state: ${audioRecord?.state}, recordingState: ${audioRecord?.recordingState}") +Log.i("AzureASR", "BytesRead = $bytesRead, buffer.isSilent = ${buffer.all { it == 0.toByte() }}") + + if (!isPaused && bytesRead > 0) { + + // 写入Azure流 + audioStream?.write(buffer) + + // 保存录音 + recordfile?.saveAudioDataToWav(buffer) } - - }.apply { start() } - } + } + }.apply { start() } +} /** * 停止麦克风捕获并释放所有相关资源