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
void onClose() {
AgentService.stopAiSteam(); //停止AI流
AgentService.stopConversation(); // 停止当前会话
AgentService.stopTts(); // 停止当前语音
textController.dispose();

3
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 {
// 更新当前项

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.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()
}
}
}

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)
}
}
"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 ->

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 {
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
}
/**
* 获取设备支持的最佳音频格式
* 优先选择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() }
}
/**
* 停止麦克风捕获并释放所有相关资源

Loading…
Cancel
Save