Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
263a80eee7
  1. 1
      lib/modules/agent/controllers/agent_controller.dart
  2. 3
      lib/modules/translation/controllers/translation_controller.dart
  3. 204
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  4. 10
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  5. 10
      local_plugins/agent_service/lib/agent_service.dart
  6. 108
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

1
lib/modules/agent/controllers/agent_controller.dart

@ -155,6 +155,7 @@ class AgentController extends GetxController {
@override @override
void onClose() { void onClose() {
AgentService.stopAiSteam(); //停止AI流
AgentService.stopConversation(); // 停止当前会话 AgentService.stopConversation(); // 停止当前会话
AgentService.stopTts(); // 停止当前语音 AgentService.stopTts(); // 停止当前语音
textController.dispose(); textController.dispose();

3
lib/modules/translation/controllers/translation_controller.dart

@ -161,7 +161,7 @@ class TranslationController extends GetxController {
} }
// 加载历史记录 // 加载历史记录
loadTranslationHistory(); //loadTranslationHistory();
// 初始化服务 // 初始化服务
_initServices(); _initServices();
@ -469,6 +469,7 @@ class TranslationController extends GetxController {
isIntermediate: true, isIntermediate: true,
); );
// 添加新项后滚动到底部 // 添加新项后滚动到底部
translationHistory.add(newItem);
_scrollToBottom(); _scrollToBottom();
} else { } else {
// 更新当前项 // 更新当前项

204
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.speech.tts.TtsEventType
import com.deep_voice.bytedance_speech.BytedanceTTS import com.deep_voice.bytedance_speech.BytedanceTTS
import java.util.UUID import java.util.UUID
import com.yunqiinnovation.agent_service.Utils
/** /**
* 代理服务事件监听接口 * 代理服务事件监听接口
@ -571,7 +572,7 @@ object AgentService : CoroutineScope {
/** /**
* 停止AI流输出 * 停止AI流输出
*/ */
private fun stopAiStream() { fun stopAiStream() {
if (isAiStreaming) { if (isAiStreaming) {
try { try {
// 先更新状态,避免回调时的状态不一致 // 先更新状态,避免回调时的状态不一致
@ -580,7 +581,8 @@ object AgentService : CoroutineScope {
// 取消当前AI生成任务 // 取消当前AI生成任务
currentAiJob?.cancel() currentAiJob?.cancel()
currentAiJob = null currentAiJob = null
currsessionId = "" //取消会话
// 通知ChatAPI服务终止当前流式请求 // 通知ChatAPI服务终止当前流式请求
chatApiService.cancelCurrentStream() chatApiService.cancelCurrentStream()
@ -756,9 +758,6 @@ object AgentService : CoroutineScope {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse && broadcast) { if (speakResponse && broadcast) {
ttsService?.speakStream(token) ttsService?.speakStream(token)
if (token.length > 0) {
audioPlayer?.stopAudio()
}
} }
if (broadcast) { if (broadcast) {
// 发送流式回复token // 发送流式回复token
@ -774,6 +773,7 @@ object AgentService : CoroutineScope {
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse && broadcast && sessionid == currsessionId){ if (speakResponse && broadcast && sessionid == currsessionId){
ttsService?.flushStream() ttsService?.flushStream()
audioPlayer?.stopAudio()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记 // 发送完整回复,包含是否有图片的标记
@ -1164,6 +1164,22 @@ object AgentService : CoroutineScope {
class AudioPlayer(private val context: Context) { class AudioPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var isInitialized = false 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 * @param volume 音量大小,范围0.0-1.0,默认1.0
*/ */
fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) {
try { synchronized(playbackLock) {
// 释放之前的资源 try {
release() // 强制停止并释放之前的资源
forceStop()
// 创建播放器并设置资源 // 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
// 设置资源 // 设置资源
context.resources.openRawResourceFd(resId)?.use { fd -> context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
} }
this.isLooping = isLooping // 设置循环属性 this.isLooping = isLooping // 设置循环属性
setVolume(volume, volume) // 设置音量(左声道,右声道) setVolume(volume, volume) // 设置音量(左声道,右声道)
setOnCompletionListener { setOnCompletionListener {
if (!isLooping) { if (!isLooping) {
release() release()
}
}
setOnErrorListener { _, what, extra ->
Log.e(TAG, "MediaPlayer 错误: what=$what, extra=$extra")
forceRelease()
true // 返回true表示错误已处理
} }
}
prepare() prepare()
start() 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() { fun stopAudio() {
try { synchronized(playbackLock) {
mediaPlayer?.apply { try {
when { mediaPlayer?.apply {
isPlaying -> { if (isMediaPlayerPlaying()) {
stop() stop()
Log.d(TAG, "音频已停止") Log.d(TAG, "音频已停止")
} } else {
else -> {
Log.d(TAG, "音频未在播放状态,无需停止") 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 { try {
mediaPlayer?.apply { mediaPlayer?.apply {
if (isPlaying) { if (isMediaPlayerPlaying()) {
stop() 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) { } catch (e: Exception) {
Log.e(TAG, "释放音频资源异常: ${e.message}", e) Log.w(TAG, "强制停止过程中异常: ${e.message}")
mediaPlayer = null }
isInitialized = false }
/**
* 释放资源
*/
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()
} }
} }
} }

10
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) 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" -> { "clearChatHistory" -> {
try { try {
AgentService.clearChatHistory { success -> AgentService.clearChatHistory { success ->

10
local_plugins/agent_service/lib/agent_service.dart

@ -293,6 +293,16 @@ class AgentService {
} }
} }
/// 停止语音合成
static Future<bool> 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<bool> stopTts() async { static Future<bool> stopTts() async {
try { try {

108
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 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语音服务 * 初始化Azure语音服务
* *
@ -106,7 +128,7 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 1. 创建可控制的音频流 // 1. 创建可控制的音频流
val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1) val format = getOptimalAudioFormat()
audioStream = AudioInputStream.createPushStream(format) audioStream = AudioInputStream.createPushStream(format)
// 初始化音频管理器 // 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@ -157,7 +179,7 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件类 // 录音文件类
recordfile = RecordFile; recordfile = RecordFile;
//进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请 //进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请
// 创建识别器 // // 创建识别器
// val setupSuccess = setupRecognizer() // val setupSuccess = setupRecognizer()
// if (setupSuccess) { // if (setupSuccess) {
// // 优化:初始化完成后进行预热 // // 优化:初始化完成后进行预热
@ -369,6 +391,8 @@ class AzureAsrHelper(private val context: Context) {
audioRecord?.startRecording() audioRecord?.startRecording()
startCaptureThread() // 再启动数据读取线程 startCaptureThread() // 再启动数据读取线程
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
Log.d(tag, "识别器创建成功: ${recognizer != null}")
Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被调用")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -705,8 +729,19 @@ class AzureAsrHelper(private val context: Context) {
* 开始麦克风捕获 * 开始麦克风捕获
*/ */
private fun initAudioRecord() { 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 channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入
val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式 val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式
@ -732,35 +767,42 @@ class AzureAsrHelper(private val context: Context) {
} }
// 3. 单独封装线程启动逻辑 // 3. 单独封装线程启动逻辑
private fun startCaptureThread() { private fun startCaptureThread() {
captureThread = Thread { captureThread = Thread {
// 循环条件确保录音已开始 // 根据实际采样率计算缓冲区大小
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val buffer = ByteArray(1024) // 4KB数据缓冲区 var bufferSize = 0
while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { // 查找设备支持的最佳采样率
// 从麦克风读取数据 val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 bufferSize = AudioRecord.getMinBufferSize(
rate,
// 仅在未暂停时处理数据 AudioFormat.CHANNEL_IN_MONO,
if (!isPaused && bytesRead > 0) { AudioFormat.ENCODING_PCM_16BIT
// 写入音频流(可能是网络传输或本地处理) )
audioStream?.write(buffer) bufferSize > 0 // 返回正值表示支持
// 保存到WAV文件(如果启用了录制功能) } ?: 16000 // 默认回退值
recordfile?.saveAudioDataToWav(buffer) // (sampleRate * 0.032).toInt() * 2 // 32ms数据量
}
val buffer = ByteArray(bufferSize)
// 暂停时短暂休眠以减少CPU占用
if (isPaused) { while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
try {
Thread.sleep(50) // 读取音频
} catch (e: InterruptedException) { val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
break // 线程被中断时退出循环 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() }
} }
/** /**
* 停止麦克风捕获并释放所有相关资源 * 停止麦克风捕获并释放所有相关资源

Loading…
Cancel
Save