Browse Source

Merge commit '025ac6b26d6abaeaf9821d451bdcfbd337575673' into new_dev

newdev_shunjiawei
lxm 1 year ago
parent
commit
e361215196
  1. 152
      lib/data/services/language_manager.dart
  2. 42
      lib/modules/settings/views/settings_view.dart
  3. 24
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  4. 13
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  5. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  6. 51
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  7. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  8. 4
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  9. 141
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  10. 46
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  11. 174
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  12. 56
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift
  13. 54
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  14. 63
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  15. 24
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

152
lib/data/services/language_manager.dart

@ -5,22 +5,22 @@ import '../../core/utils/logger.dart';
class LanguageInfo {
/// 短代码,如 'zh', 'en'
final String shortCode;
/// 中文名称
final String chineseName;
/// 英文名称
final String englishName;
/// Azure ASR 语言代码,如 'zh-CN', 'en-US'
final String asrCode;
/// Azure TTS 语音名称
final String ttsVoiceName;
/// 备注信息
final String? note;
LanguageInfo({
required this.shortCode,
required this.chineseName,
@ -35,30 +35,30 @@ class LanguageInfo {
class LanguageManager extends GetxService {
// 单例实例
static LanguageManager? _instance;
// 单例工厂方法
factory LanguageManager() {
_instance ??= LanguageManager._internal();
return _instance!;
}
// 私有构造函数
LanguageManager._internal() {
_initLanguageMap();
}
// 语言信息映射表,以 ASR 代码为键
final Map<String, LanguageInfo> _languageMap = {};
// 中文名称到 ASR 代码的映射
final Map<String, String> _chineseNameToAsrCodeMap = {};
// 英文名称到 ASR 代码的映射
final Map<String, String> _englishNameToAsrCodeMap = {};
// 短代码到 ASR 代码的映射
final Map<String, String> _shortCodeToAsrCodeMap = {};
/// 初始化语言映射表
void _initLanguageMap() {
// 中文相关
@ -69,7 +69,7 @@ class LanguageManager extends GetxService {
asrCode: 'zh-CN',
ttsVoiceName: 'zh-CN-XiaoxiaoNeural',
);
_addLanguage(
shortCode: 'zh-Hant',
chineseName: '中文(繁体)',
@ -77,7 +77,7 @@ class LanguageManager extends GetxService {
asrCode: 'zh-TW',
ttsVoiceName: 'zh-TW-HsiaoChenNeural',
);
_addLanguage(
shortCode: 'zh-Hant-hk',
chineseName: '中文(香港繁体)',
@ -85,7 +85,7 @@ class LanguageManager extends GetxService {
asrCode: 'zh-HK',
ttsVoiceName: 'zh-HK-HiuMaanNeural',
);
// 英语相关
_addLanguage(
shortCode: 'en',
@ -94,7 +94,7 @@ class LanguageManager extends GetxService {
asrCode: 'en-US',
ttsVoiceName: 'en-US-AriaNeural',
);
// 日语相关
_addLanguage(
shortCode: 'ja',
@ -103,7 +103,7 @@ class LanguageManager extends GetxService {
asrCode: 'ja-JP',
ttsVoiceName: 'ja-JP-NanamiNeural',
);
// 韩语相关
_addLanguage(
shortCode: 'ko',
@ -112,7 +112,7 @@ class LanguageManager extends GetxService {
asrCode: 'ko-KR',
ttsVoiceName: 'ko-KR-SunHiNeural',
);
// 欧洲语言
_addLanguage(
shortCode: 'fr',
@ -121,7 +121,7 @@ class LanguageManager extends GetxService {
asrCode: 'fr-FR',
ttsVoiceName: 'fr-FR-DeniseNeural',
);
_addLanguage(
shortCode: 'es',
chineseName: '西班牙语',
@ -129,7 +129,7 @@ class LanguageManager extends GetxService {
asrCode: 'es-ES',
ttsVoiceName: 'es-ES-ElviraNeural',
);
_addLanguage(
shortCode: 'pt',
chineseName: '葡萄牙语',
@ -137,7 +137,7 @@ class LanguageManager extends GetxService {
asrCode: 'pt-BR',
ttsVoiceName: 'pt-BR-FranciscaNeural',
);
_addLanguage(
shortCode: 'it',
chineseName: '意大利语',
@ -145,7 +145,7 @@ class LanguageManager extends GetxService {
asrCode: 'it-IT',
ttsVoiceName: 'it-IT-ElsaNeural',
);
_addLanguage(
shortCode: 'de',
chineseName: '德语',
@ -153,7 +153,7 @@ class LanguageManager extends GetxService {
asrCode: 'de-DE',
ttsVoiceName: 'de-DE-KatjaNeural',
);
_addLanguage(
shortCode: 'ru',
chineseName: '俄语',
@ -161,7 +161,7 @@ class LanguageManager extends GetxService {
asrCode: 'ru-RU',
ttsVoiceName: 'ru-RU-SvetlanaNeural',
);
_addLanguage(
shortCode: 'pl',
chineseName: '波兰语',
@ -169,7 +169,7 @@ class LanguageManager extends GetxService {
asrCode: 'pl-PL',
ttsVoiceName: 'pl-PL-AgnieszkaNeural',
);
_addLanguage(
shortCode: 'nl',
chineseName: '荷兰语',
@ -177,7 +177,7 @@ class LanguageManager extends GetxService {
asrCode: 'nl-NL',
ttsVoiceName: 'nl-NL-ColetteNeural',
);
_addLanguage(
shortCode: 'sv',
chineseName: '瑞典语',
@ -185,7 +185,7 @@ class LanguageManager extends GetxService {
asrCode: 'sv-SE',
ttsVoiceName: 'sv-SE-SofieNeural',
);
_addLanguage(
shortCode: 'cs',
chineseName: '捷克语',
@ -193,7 +193,7 @@ class LanguageManager extends GetxService {
asrCode: 'cs-CZ',
ttsVoiceName: 'cs-CZ-VlastaNeural',
);
_addLanguage(
shortCode: 'el',
chineseName: '现代希腊语',
@ -201,7 +201,7 @@ class LanguageManager extends GetxService {
asrCode: 'el-GR',
ttsVoiceName: 'el-GR-AthinaNeural',
);
_addLanguage(
shortCode: 'ro',
chineseName: '罗马尼亚语',
@ -209,7 +209,7 @@ class LanguageManager extends GetxService {
asrCode: 'ro-RO',
ttsVoiceName: 'ro-RO-AlinaNeural',
);
_addLanguage(
shortCode: 'hu',
chineseName: '匈牙利语',
@ -217,7 +217,7 @@ class LanguageManager extends GetxService {
asrCode: 'hu-HU',
ttsVoiceName: 'hu-HU-NoemiNeural',
);
_addLanguage(
shortCode: 'uk',
chineseName: '乌克兰语',
@ -225,7 +225,7 @@ class LanguageManager extends GetxService {
asrCode: 'uk-UA',
ttsVoiceName: 'uk-UA-PolinaNeural',
);
_addLanguage(
shortCode: 'da',
chineseName: '丹麦语',
@ -233,7 +233,7 @@ class LanguageManager extends GetxService {
asrCode: 'da-DK',
ttsVoiceName: 'da-DK-ChristelNeural',
);
_addLanguage(
shortCode: 'fi',
chineseName: '芬兰语',
@ -241,7 +241,7 @@ class LanguageManager extends GetxService {
asrCode: 'fi-FI',
ttsVoiceName: 'fi-FI-NooraNeural',
);
_addLanguage(
shortCode: 'no',
chineseName: '挪威语',
@ -249,7 +249,7 @@ class LanguageManager extends GetxService {
asrCode: 'nb-NO',
ttsVoiceName: 'nb-NO-IselinNeural',
);
_addLanguage(
shortCode: 'hr',
chineseName: '克罗地亚语',
@ -257,7 +257,7 @@ class LanguageManager extends GetxService {
asrCode: 'hr-HR',
ttsVoiceName: 'hr-HR-GabrijelaNeural',
);
_addLanguage(
shortCode: 'ca',
chineseName: '加泰隆语',
@ -265,7 +265,7 @@ class LanguageManager extends GetxService {
asrCode: 'ca-ES',
ttsVoiceName: 'ca-ES-JoanaNeural',
);
// 亚洲语言
_addLanguage(
shortCode: 'ar',
@ -274,7 +274,7 @@ class LanguageManager extends GetxService {
asrCode: 'ar-EG',
ttsVoiceName: 'ar-EG-SalmaNeural',
);
_addLanguage(
shortCode: 'th',
chineseName: '泰语',
@ -282,7 +282,7 @@ class LanguageManager extends GetxService {
asrCode: 'th-TH',
ttsVoiceName: 'th-TH-PremwadeeNeural',
);
_addLanguage(
shortCode: 'vi',
chineseName: '越南语',
@ -290,7 +290,7 @@ class LanguageManager extends GetxService {
asrCode: 'vi-VN',
ttsVoiceName: 'vi-VN-HoaiMyNeural',
);
_addLanguage(
shortCode: 'id',
chineseName: '印尼语',
@ -298,7 +298,7 @@ class LanguageManager extends GetxService {
asrCode: 'id-ID',
ttsVoiceName: 'id-ID-GadisNeural',
);
_addLanguage(
shortCode: 'hi',
chineseName: '印地语',
@ -306,7 +306,7 @@ class LanguageManager extends GetxService {
asrCode: 'hi-IN',
ttsVoiceName: 'hi-IN-SwaraNeural',
);
_addLanguage(
shortCode: 'tr',
chineseName: '土耳其语',
@ -314,7 +314,7 @@ class LanguageManager extends GetxService {
asrCode: 'tr-TR',
ttsVoiceName: 'tr-TR-EmelNeural',
);
_addLanguage(
shortCode: 'ms',
chineseName: '马来语',
@ -322,7 +322,7 @@ class LanguageManager extends GetxService {
asrCode: 'ms-MY',
ttsVoiceName: 'ms-MY-YasminNeural',
);
_addLanguage(
shortCode: 'bn',
chineseName: '孟加拉语',
@ -330,7 +330,7 @@ class LanguageManager extends GetxService {
asrCode: 'bn-IN',
ttsVoiceName: 'bn-IN-TanishaaNeural',
);
_addLanguage(
shortCode: 'ta',
chineseName: '泰米尔语',
@ -338,7 +338,7 @@ class LanguageManager extends GetxService {
asrCode: 'ta-IN',
ttsVoiceName: 'ta-IN-PallaviNeural',
);
_addLanguage(
shortCode: 'te',
chineseName: '泰卢固语',
@ -346,7 +346,7 @@ class LanguageManager extends GetxService {
asrCode: 'te-IN',
ttsVoiceName: 'te-IN-ShrutiNeural',
);
_addLanguage(
shortCode: 'mr',
chineseName: '马拉提语',
@ -354,7 +354,7 @@ class LanguageManager extends GetxService {
asrCode: 'mr-IN',
ttsVoiceName: 'mr-IN-AarohiNeural',
);
_addLanguage(
shortCode: 'ur',
chineseName: '乌尔都语',
@ -362,7 +362,7 @@ class LanguageManager extends GetxService {
asrCode: 'ur-IN',
ttsVoiceName: 'ur-IN-GulNeural',
);
_addLanguage(
shortCode: 'fa',
chineseName: '波斯语',
@ -370,7 +370,7 @@ class LanguageManager extends GetxService {
asrCode: 'fa-IR',
ttsVoiceName: 'fa-IR-DilaraNeural',
);
// 非洲语言
_addLanguage(
shortCode: 'sw',
@ -379,10 +379,10 @@ class LanguageManager extends GetxService {
asrCode: 'sw-KE',
ttsVoiceName: 'sw-KE-ZuriNeural',
);
Logger.info('语言管理器初始化完成,共加载 ${_languageMap.length} 种语言');
}
/// 添加语言信息到映射表
void _addLanguage({
required String shortCode,
@ -400,133 +400,133 @@ class LanguageManager extends GetxService {
ttsVoiceName: ttsVoiceName,
note: note,
);
_languageMap[asrCode] = languageInfo;
_chineseNameToAsrCodeMap[chineseName] = asrCode;
_englishNameToAsrCodeMap[englishName] = asrCode;
_shortCodeToAsrCodeMap[shortCode] = asrCode;
}
/// 获取所有支持的语言信息
Map<String, LanguageInfo> getAllLanguages() {
return Map.from(_languageMap);
}
/// 获取所有支持的语言的中文名称
List<String> getAllChineseNames() {
return _chineseNameToAsrCodeMap.keys.toList();
}
/// 获取所有支持的语言的英文名称
List<String> getAllEnglishNames() {
return _englishNameToAsrCodeMap.keys.toList();
}
/// 根据 ASR 代码获取语言信息
LanguageInfo? getLanguageByAsrCode(String asrCode) {
return _languageMap[asrCode];
}
/// 根据中文名称获取语言信息
LanguageInfo? getLanguageByChineseName(String chineseName) {
final asrCode = _chineseNameToAsrCodeMap[chineseName];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据英文名称获取语言信息
LanguageInfo? getLanguageByEnglishName(String englishName) {
final asrCode = _englishNameToAsrCodeMap[englishName];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据短代码获取语言信息
LanguageInfo? getLanguageByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据中文名称获取 ASR 代码
String? getAsrCodeByChineseName(String chineseName) {
return _chineseNameToAsrCodeMap[chineseName];
}
/// 根据英文名称获取 ASR 代码
String? getAsrCodeByEnglishName(String englishName) {
return _englishNameToAsrCodeMap[englishName];
}
/// 根据短代码获取 ASR 代码
String? getAsrCodeByShortCode(String shortCode) {
return _shortCodeToAsrCodeMap[shortCode];
}
/// 根据 ASR 代码获取中文名称
String? getChineseNameByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.chineseName;
}
/// 根据 ASR 代码获取英文名称
String? getEnglishNameByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.englishName;
}
/// 根据 ASR 代码获取短代码
String? getShortCodeByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.shortCode;
}
/// 根据 ASR 代码获取 TTS 语音名称
String? getTtsVoiceNameByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.ttsVoiceName;
}
/// 根据中文名称获取 TTS 语音名称
String? getTtsVoiceNameByChineseName(String chineseName) {
final asrCode = _chineseNameToAsrCodeMap[chineseName];
if (asrCode == null) return null;
return _languageMap[asrCode]?.ttsVoiceName;
}
/// 根据短代码获取中文名称
String? getChineseNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode]?.chineseName;
}
/// 根据短代码获取英文名称
String? getEnglishNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode]?.englishName;
}
/// 根据短代码获取 TTS 语音名称
String? getTtsVoiceNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode]?.ttsVoiceName;
}
/// 获取中文名称到 ASR 代码的映射
Map<String, String> getChineseNameToAsrCodeMap() {
return Map.from(_chineseNameToAsrCodeMap);
}
/// 获取英文名称到 ASR 代码的映射
Map<String, String> getEnglishNameToAsrCodeMap() {
return Map.from(_englishNameToAsrCodeMap);
}
/// 获取短代码到 ASR 代码的映射
Map<String, String> getShortCodeToAsrCodeMap() {
return Map.from(_shortCodeToAsrCodeMap);
}
}
}

42
lib/modules/settings/views/settings_view.dart

@ -515,27 +515,27 @@ class SettingsView extends GetView<SettingsController> {
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
//BLE测试
// _buildSimpleNavigationSetting(
// title: 'opus解码测试',
// subtitle: '测试opus解码',
// icon: Icons.bluetooth_searching,
// iconBgColor: isDarkMode
// ? Colors.green[900]!.withOpacity(0.3)
// : Colors.green[100]!,
// iconColor:
// isDarkMode ? Colors.green[300]! : Colors.green[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// Get.toNamed(Routes.opusTest);
// },
// isDarkMode: isDarkMode,
// ),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor:
isDarkMode ? Colors.green[300]! : Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// BLE测试
// _buildSimpleNavigationSetting(

24
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -130,6 +130,8 @@ object AgentService : CoroutineScope {
//是否播放音乐中
private var isplaymusic = false;
//是否保留最终结果
private var isKeepResult = true;
// 打断防抖相关
private var lastInterruptTime = 0L
@ -265,6 +267,7 @@ object AgentService : CoroutineScope {
*/
fun dispose() {
try {
BleService.closeCodec()
stopRecognition()
stopTts()
stopAiStream()
@ -474,6 +477,7 @@ object AgentService : CoroutineScope {
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
BleService.closeCodec()
stopRecognition()
sendEvent(
"auto_stop",
@ -516,13 +520,18 @@ object AgentService : CoroutineScope {
* @param mode 识别模式:normal, ble_wakeup, phone_call, push_to_talk
*/
fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean {
if(isExternalActive){
isKeepResult = false
BleService.openEncoder()//不发指令。只是录制opus
}
if (!isInitialized) {
Log.d(TAG, "服务未初始化")
return false
}
Log.d("TAG", "开始语音识别startRecognition?")
stopRecognition()
BleService.openEncoder()
// if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){
// Log.d(TAG, "开始语音识别stopRecognition")
// // BleService.closeCodec()
@ -559,6 +568,9 @@ object AgentService : CoroutineScope {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
Log.d("TAG", "azureAsrHelper?.startContinuousRecognition")
if(isExternalActive){
isKeepResult = true
}
azureAsrHelper?.startContinuousRecognition(audioSourceType)
// 根据模式启动相应的空闲检测
@ -586,7 +598,7 @@ object AgentService : CoroutineScope {
val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态
_hasSpeechDetected.set(true)
@ -630,7 +642,7 @@ object AgentService : CoroutineScope {
}
override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) {
if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读
sendEvent(
"recognition_result", mapOf(
"text" to result,
@ -709,11 +721,9 @@ object AgentService : CoroutineScope {
Log.d(TAG, "停止语音识别,当前模式: $currentRecognitionMode")
try {
_isRecognitionActive.set(false)
azureAsrHelper?.stopContinuousRecognition()
BleService.closeCodec()
_isRecognitionActive.set(false)
stopIdleCheck()
// 重置识别模式
currentRecognitionMode = "normal"
return
@ -721,7 +731,6 @@ object AgentService : CoroutineScope {
Log.e(TAG, "停止语音识别异常: ${e.message}")
_isRecognitionActive.set(false)
stopIdleCheck()
// 重置识别模式
currentRecognitionMode = "normal"
return
@ -1055,6 +1064,7 @@ object AgentService : CoroutineScope {
)
)
if (name == "exit_interaction") {
BleService.closeCodec()
stopRecognition()
}
} catch (e: Exception) {

13
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -14,6 +14,8 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import android.content.Intent
import android.os.Build
// 添加BleService的导入语句
import com.yunqiinnovation.ble_service.BleService
class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler,
AgentServiceListener {
@ -124,13 +126,14 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("START_ERROR", "启动对话失败: ${e.message}", null)
}
}
"recognizeCallback" -> {
val isStarted = AgentService.recognizeCallback()
result.success(isStarted)
}
"recognizeCallback" -> {
val isStarted = AgentService.recognizeCallback()
result.success(isStarted)
}
"stopConversation" -> {
try {
BleService.closeCodec()
AgentService.stopRecognition()
result.success(true)
} catch (e: Exception) {
@ -309,4 +312,4 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
eventChannel.setStreamHandler(null)
AgentService.removeListener(this)
}
}
}

1
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -77,6 +77,7 @@ Log.d(TAG, "手动启动语音识别: ")
Log.e(TAG, "语音代理未初始化")
return
}
BleService.closeCodec()
AgentService.stopRecognition()
return
}

51
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -69,7 +69,9 @@ class AgentServiceImpl: NSObject {
// 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题
private var isStartingRecognition: Bool = false
private var stopRequestedDuringStart: Bool = false
//是否保留最终结果
private var isKeepResult = true;
var currsessionId: String = ""
var language: String = ""
private var serverurl = ""
@ -354,7 +356,8 @@ class AgentServiceImpl: NSObject {
if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode)
BleService.shared.closeCodec()
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
@ -378,6 +381,14 @@ class AgentServiceImpl: NSObject {
}
}
func startRecognition(useBle: Bool = false, mode: String = "normal") -> Bool {
// 接收到唤醒信号,打开编码器 (设备侧)
print("ai启动语音\(useBle))")
var openResult = false // 添加openResult变量定义
if useBle{
isKeepResult = false
// 捕获openEncoder的返回值
openResult = BleService.shared.openEncoder() // 正确捕获返回值
}
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
@ -388,13 +399,12 @@ class AgentServiceImpl: NSObject {
sendError("无麦克风权限", code: "PERMISSION_DENIED")
return false
}
}
stopTts();
if isRecognizing {
stopRecognition()
}
}
stopRecognition()
stopTts()
os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败")
wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying
// 如果音乐正在播放,则暂停
@ -416,7 +426,7 @@ class AgentServiceImpl: NSObject {
// 新增:标记为"启动中",用于允许 stop 在启动未完成时也能生效
isStartingRecognition = true
print("ai启动语音=\(audioSourceType)=\(useBle)")
guard let success = azureAsrHelper?.startContinuousRecognition(
// callback: self,
@ -427,7 +437,9 @@ class AgentServiceImpl: NSObject {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false
}
if(useBle){
isKeepResult = true
}
// 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动)
startIdleCheckForMode(mode: mode)
@ -449,6 +461,7 @@ class AgentServiceImpl: NSObject {
/// 特性:
/// 1) 即便处于"启动中"(isStartingRecognition)也会打印日志并尝试停止
/// 2) 如果底层尚未完全进入可停止状态,记录一次待停止请求,待会话启动后立即停止
print("停止识别语音,isRecognizing=\(isRecognizing),isStartingRecognition=\(isStartingRecognition)")
if !(isRecognizing || isStartingRecognition) {
// 既不在识别中,也不在启动中,视为幂等处理
return true
@ -472,7 +485,6 @@ class AgentServiceImpl: NSObject {
MusicService.shared.resume()
}
// 重置识别模式
currentRecognitionMode = "normal"
@ -490,7 +502,7 @@ class AgentServiceImpl: NSObject {
return false
}
guard let audioStream = azureAsrHelper?.audioStream else {
os_log("音频流未初始化", log: logger,type: .error)
os_log("AI音频流未初始化", log: logger,type: .error)
return false
}
audioStream.saveAudioDataTo(data: audioData)
@ -1053,6 +1065,8 @@ private func jsonToString(_ json: [String: Any]) -> String? {
// 如果正在识别,需要先停止
let wasRecognizing = isRecognizing
if wasRecognizing {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
_ = stopRecognition()
}
@ -1227,6 +1241,8 @@ private func jsonToString(_ json: [String: Any]) -> String? {
func dispose() -> Bool {
print("ai释放资源")
if isRecognizing {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
stopRecognition()
}
@ -1388,6 +1404,8 @@ class ChatApiStreamCallback: StreamCallback {
print("onFunctionCall=functionCall\(functionCall)")
if (name == "exit_interaction") {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
agentService.stopRecognition()
}
@ -1513,9 +1531,6 @@ extension AgentServiceImpl: BleService.Callback {
func onWakeupSignalReceived() {
stopTts()
stopRecognition()
startRecognition(useBle: true, mode: "ble_wakeup")
}
@ -1715,7 +1730,7 @@ class AudioPlayer {
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
func onResult(_ text: String, _ detectedLanguage: String) {
if !text.isEmpty {
if !text.isEmpty && isKeepResult {
var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty {
data["language"] = detectedLanguage
@ -1734,7 +1749,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
if !recognizing.isEmpty {
if !recognizing.isEmpty && isKeepResult {
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
@ -1770,6 +1785,8 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
// 新增:若在启动期间已经收到停止请求,则立即执行停止
if stopRequestedDuringStart {
stopRequestedDuringStart = false
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
_ = stopRecognition()
}
}

4
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -2,7 +2,7 @@ import Flutter
import UIKit
import Foundation
import os.log
import ble_service
/// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现
public class AgentServicePlugin: NSObject, FlutterPlugin {
@ -90,6 +90,8 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
return
}
print("stopConversation")
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
impl.stopRecognition()
result(true)

4
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -215,7 +215,7 @@ class AzureAsrHelper(private val context: Context) {
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream)
} catch (e: Exception) {
@ -488,7 +488,7 @@ class AzureAsrHelper(private val context: Context) {
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile()
}

141
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt

@ -475,26 +475,57 @@ class IntegratedSpeechTranslationService(
}
/**
* 停止连续语音翻译
* 停止连续语音翻译(非阻塞)
* 在 IO 线程内顺序停止识别器、录音线程与文件,并对翻译/合成收尾增加统一超时保护,避免阻塞主线程与潜在 ANR。
*/
fun stopContinuousTranslation() {
try {
recognizer?.stopContinuousRecognitionAsync()
audioProcessor?.stopRecording()
recordfile1!!.closeFile(true)
Log.d(TAG, "停止连续语音翻译-开始")
// 先标记识别关闭,避免后续流程继续入队处理
serviceState.isRecognizing.set(false)
// 等待当前处理完成
while (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) {
Thread.sleep(100)
}
Log.d(TAG, "停止连续语音翻译")
// 在 IO 线程中做真实的停止动作,避免阻塞主线程
launch(Dispatchers.IO) {
// 1) 停止识别器:等待短超时,避免卡住
try {
recognizer?.stopContinuousRecognitionAsync()
?.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS)
} catch (t: Throwable) {
Log.w(TAG, "停止识别器超时或失败", t)
}
// 2) 停止录音线程(打断并短等待退出)
runCatching { audioProcessor?.stopRecording() }
.onFailure { Log.w(TAG, "停止录音失败", it) }
// 3) 关闭录音文件(若开启过录音)
runCatching { recordfile1?.closeFile(true) }
.onFailure { Log.w(TAG, "关闭录音文件失败", it) }
// 4) 等待翻译/合成收尾,整体不超过 2s
val start = System.currentTimeMillis()
while ((serviceState.isTranslating.get() || serviceState.isSynthesizing.get())
&& System.currentTimeMillis() - start < 2000
) {
delay(100)
}
if (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) {
Log.w(TAG, "等待翻译/合成完成超时,强制标记结束")
}
// 5) 统一落幕:确保状态关闭
serviceState.isTranslating.set(false)
serviceState.isSynthesizing.set(false)
Log.d(TAG, "停止连续语音翻译-完成")
}
} catch (e: Exception) {
Log.e(TAG, "停止翻译失败", e)
eventCallback?.onError("Service", "停止失败: ${e.message}")
}
}
/**
* 处理翻译和合成流程
*/
@ -1184,58 +1215,57 @@ fun getSupportedLanguages(): List<String> {
}
/**
* 改进的资源释放方法
* 安全释放音频处理器资源
* 添加更强的异常处理和超时保护
*/
fun dispose() {
try {
Log.d(TAG, "开始释放音频处理器资源...")
// 1. 停止运行标志
isRunning.set(false)
// 1. 设置停止标志
if (!isRunning.compareAndSet(true, false)) {
Log.d(TAG, "音频处理器已停止,跳过释放")
return
}
// 2. 中断处理线程
processingThread?.let { thread ->
if (thread.isAlive) {
// 1) 安全停止处理线程
processingThread?.let { thread ->
if (thread.isAlive) {
try {
thread.interrupt()
try {
// 等待线程结束,最多等待2秒
thread.join(2000)
if (thread.isAlive) {
Log.w(TAG, "音频处理线程未能正常结束")
} else {
Log.d(TAG, "音频处理线程已结束")
}
} catch (e: InterruptedException) {
Log.w(TAG, "等待音频处理线程结束时被中断")
Thread.currentThread().interrupt()
thread.join(500)
if (thread.isAlive) {
Log.w(TAG, "音频处理线程未在500ms内结束")
// 不强制杀死,交由系统回收,但继续清理资源
} else {
Log.d(TAG, "音频处理线程已正常结束")
}
} catch (e: InterruptedException) {
Log.w(TAG, "等待线程结束时被中断", e)
Thread.currentThread().interrupt()
} catch (e: Exception) {
Log.e(TAG, "停止处理线程时发生异常", e)
}
}
processingThread = null
// 3. 关闭音频流
try {
pushAudioStream?.close()
pushAudioStream = null
Log.d(TAG, "音频流已关闭")
} catch (e: Exception) {
Log.e(TAG, "关闭音频流失败", e)
}
// 4. 清理音频队列
try {
val remainingData = audioQueue.size
audioQueue.clear()
Log.d(TAG, "音频队列已清理,丢弃 $remainingData 个数据包")
} catch (e: Exception) {
Log.e(TAG, "清理音频队列失败", e)
}
Log.d(TAG, "音频处理器资源释放完成")
} catch (e: Exception) {
Log.e(TAG, "释放音频处理器资源时发生异常", e)
}
processingThread = null
// 2) 关闭输入流
try {
pushAudioStream?.close()
} catch (t: Throwable) {
Log.w(TAG, "关闭 PushAudioInputStream 时异常", t)
} finally {
pushAudioStream = null
}
// 3) 清空队列
audioQueue.clear()
Log.d(TAG, "音频处理器资源释放完成")
} catch (e: Exception) {
Log.e(TAG, "释放音频处理器资源失败", e)
}
}
}
}
@ -1315,7 +1345,16 @@ class VolcanoTranslationServiceImpl :
"ms-MY" to "ms", // 马来语(马来西亚)
"id-ID" to "id", // 印尼语(印尼)
"fil-PH" to "tl", // 菲律宾语(菲律宾)
"sv-SE" to "sv", // 瑞典语(瑞典)
"ro-RO" to "ro", // 罗马尼亚语(罗马尼亚)
"hu-HU" to "hu", // 匈牙利语(匈牙利)
"da-DK" to "da", // 丹麦语(丹麦)
"fi-FI" to "fi", // 芬兰语(芬兰)
"nb-NO" to "nb", // 挪威语(挪威)
"hr-HR" to "hr", // 克罗地亚语(克罗地亚)
"ca-ES" to "ca", // 加泰隆语
"mr-IN" to "mr", // 马拉地语(印度)
"ur-IN" to "ur", // 乌尔都语(印度)
// 欧洲
"en-US" to "en", // 英语(美国)
"en-GB" to "en", // 英语(英国)

46
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -88,8 +88,6 @@ class SimpleAudioReceiver(private val context: Context) {
* 初始化音频录制
*/
fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
isRunning.set(true)
Log.d(TAG, "initAudioRecord: ${isRunning}")
startWriteThread() // 再启动数据读取线程
@ -97,6 +95,17 @@ class SimpleAudioReceiver(private val context: Context) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
isInitialized = true
}
/**
* 设置音频流
*/
fun setupStream() {
// 安全关闭并清理之前的流
pushAudioStream?.close()
pushAudioStream = null
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
}
fun isInitialized(): Boolean = isInitialized
/**
@ -143,7 +152,7 @@ class SimpleAudioReceiver(private val context: Context) {
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try {
Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}")
//Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}")
if(audioDataCallback!=null){
audioDataCallback?.onAudio(finalData)
}
@ -183,22 +192,36 @@ class SimpleAudioReceiver(private val context: Context) {
writeQueue.offer(buffer.copyOf())
}
/**
* 开始音频输入
* @param audioSourceType 音频源类型
* @param callback 音频数据回调,可为空
*/
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) {
/**
* 开始音频输入
* @param audioSourceType 音频源类型
* @param callback 音频数据回调,可为空
*/
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) {
try {
this.audioSourceType = audioSourceType
this.audioDataCallback = callback
writeQueue.clear()
this.audioDataCallback = callback
isWriting.set(true)
if(writeQueue.isNotEmpty()){
Log.d("tag", "存下来的数据: ${writeQueue?.size}")
}
when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture()
}
} catch (e: Exception) {
Log.e(TAG, "启动音频录制失败: ${e.message}", e)
// 清理资源
pushAudioStream?.close()
pushAudioStream = null
isWriting.set(false)
throw e // 重新抛出异常让调用者处理
}
}
// ... existing code ...
/**
* 配置通话音频模式以优化回声消除
*/
@ -367,6 +390,7 @@ class SimpleAudioReceiver(private val context: Context) {
*/
fun stopMicrophoneCapture() {
try {
writeQueue.clear()
isWriting.set(false)
// 停止录音

174
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -605,13 +605,36 @@ import os.log
audioProcessor?.stopMicrophoneCapture()
recordFile?.closeFile(isSave: true)
// 等待当前处理完成
while serviceState.isTranslating || serviceState.isSynthesizing {
Thread.sleep(forTimeInterval: 0.1)
// 使用后台队列等待当前处理完成,避免阻塞主线程
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// 设置超时时间,避免无限等待
let timeout = DispatchTime.now() + .seconds(5)
while (self.serviceState.isTranslating || self.serviceState.isSynthesizing) && DispatchTime.now() < timeout {
Thread.sleep(forTimeInterval: 0.1)
}
// 强制停止合成器(如果仍在运行)
if self.serviceState.isSynthesizing {
do {
try self.synthesizer?.stopSpeaking()
} catch {
os_log("停止语音合成失败: %@", log: self.log, type: .error, error.localizedDescription)
}
self.serviceState.isSynthesizing = false
DispatchQueue.main.async {
self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false)
}
}
DispatchQueue.main.async {
os_log("停止连续语音翻译", log: self.log, type: .info)
}
}
os_log("停止连续语音翻译", log: log, type: .info)
} catch {
os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription)
eventCallback?.onError(component: "Service", error: "停止失败: \(error.localizedDescription)")
@ -777,68 +800,93 @@ import os.log
let voice: String
switch language {
// 亚洲地区
case "zh-CN":
voice = "zh-CN-XiaoxiaoNeural"
case "zh-TW":
voice = "zh-TW-HsiaoNeural"
case "zh-HK":
voice = "zh-HK-HiuGaaiNeural"
case "ja-JP":
voice = "ja-JP-NanamiNeural"
case "ko-KR":
voice = "ko-KR-SunHiNeural"
case "hi-IN":
voice = "hi-IN-KalpanaNeural"
case "ta-IN":
voice = "ta-IN-SundarNeural"
case "th-TH":
voice = "th-TH-PattaraNeural"
case "vi-VN":
voice = "vi-VN-AnhNeural"
// 欧洲地区
case "en-US":
voice = "en-US-AriaNeural"
case "en-GB":
voice = "en-GB-SusanNeural"
case "fr-FR":
voice = "fr-FR-DeniseNeural"
case "de-DE":
voice = "de-DE-KatjaNeural"
case "es-ES":
voice = "es-ES-ElviraNeural"
case "it-IT":
voice = "it-IT-DonatellaNeural"
case "pl-PL":
voice = "pl-PL-JozefinaNeural"
case "ru-RU":
voice = "ru-RU-DariaNeural"
// 中文相关
case "zh-CN":
voice = "zh-CN-XiaoxiaoNeural"
case "zh-TW":
voice = "zh-TW-HsiaoChenNeural"
case "zh-HK":
voice = "zh-HK-HiuMaanNeural"
// 中东/非洲
case "ar-SA":
voice = "ar-SA-MajdNeural"
case "he-IL":
voice = "he-IL-NoaNeural"
case "sw-KE":
voice = "sw-KE-SabinaNeural"
// 英语相关
case "en-US":
voice = "en-US-AriaNeural"
// 美洲/大洋洲
case "pt-BR":
voice = "pt-BR-ManuelaNeural"
case "es-MX":
voice = "es-MX-CarlaNeural"
// 亚洲语言
case "ja-JP":
voice = "ja-JP-NanamiNeural"
case "ko-KR":
voice = "ko-KR-SunHiNeural"
case "th-TH":
voice = "th-TH-PremwadeeNeural"
case "vi-VN":
voice = "vi-VN-HoaiMyNeural"
case "id-ID":
voice = "id-ID-GadisNeural"
case "hi-IN":
voice = "hi-IN-SwaraNeural"
case "ms-MY":
voice = "ms-MY-YasminNeural"
case "bn-IN":
voice = "bn-IN-TanishaaNeural"
case "ta-IN":
voice = "ta-IN-PallaviNeural"
case "te-IN":
voice = "te-IN-ShrutiNeural"
case "mr-IN":
voice = "mr-IN-AarohiNeural"
case "ur-IN":
voice = "ur-IN-GulNeural"
case "tr-TR":
voice = "tr-TR-EmelNeural"
case "fa-IR":
voice = "fa-IR-DilaraNeural"
// 其他重要语言
case "tr-TR":
voice = "tr-TR-SerifeNeural"
case "uk-UA":
voice = "uk-UA-OksanaNeural"
case "fa-IR":
voice = "fa-IR-SaraNeural"
case "nl-NL":
voice = "nl-NL-LotteNeural"
// 欧洲语言
case "fr-FR":
voice = "fr-FR-DeniseNeural"
case "es-ES":
voice = "es-ES-ElviraNeural"
case "pt-BR":
voice = "pt-BR-FranciscaNeural"
case "it-IT":
voice = "it-IT-ElsaNeural"
case "de-DE":
voice = "de-DE-KatjaNeural"
case "ru-RU":
voice = "ru-RU-SvetlanaNeural"
case "pl-PL":
voice = "pl-PL-AgnieszkaNeural"
case "nl-NL":
voice = "nl-NL-ColetteNeural"
case "sv-SE":
voice = "sv-SE-SofieNeural"
case "cs-CZ":
voice = "cs-CZ-VlastaNeural"
case "el-GR":
voice = "el-GR-AthinaNeural"
case "ro-RO":
voice = "ro-RO-AlinaNeural"
case "hu-HU":
voice = "hu-HU-NoemiNeural"
case "uk-UA":
voice = "uk-UA-PolinaNeural"
case "da-DK":
voice = "da-DK-ChristelNeural"
case "fi-FI":
voice = "fi-FI-NooraNeural"
case "nb-NO":
voice = "nb-NO-IselinNeural"
case "hr-HR":
voice = "hr-HR-GabrijelaNeural"
case "ca-ES":
voice = "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
case "ar-EG":
voice = "ar-EG-SalmaNeural"
case "sw-KE":
voice = "sw-KE-ZuriNeural"
default:
voice = "en-US-AriaNeural" // 默认回退英语
}

56
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift

@ -24,6 +24,8 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
private var region = "cn-north-1"
private var maxRetryAttempts = 3
private var timeout: TimeInterval = 10.0
private var isDisposed = false // 添加释放状态标志
private let disposeLock = NSLock() // 添加线程安全锁
// HTTP客户端
private lazy var urlSession: URLSession = {
@ -52,7 +54,16 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
"ms-MY": "ms", // 马来语(马来西亚)
"id-ID": "id", // 印尼语(印尼)
"fil-PH": "tl", // 菲律宾语(菲律宾)
"sv-SE": "sv", // 瑞典语(瑞典)
"ro-RO": "ro", // 罗马尼亚语(罗马尼亚)
"hu-HU": "hu", // 匈牙利语(匈牙利)
"da-DK": "da", // 丹麦语(丹麦)
"fi-FI": "fi", // 芬兰语(芬兰)
"nb-NO": "nb", // 挪威语(挪威)
"hr-HR": "hr", // 克罗地亚语(克罗地亚)
"ca-ES": "ca", // 加泰隆语
"mr-IN": "mr", // 马拉地语(印度)
"ur-IN": "ur", // 乌尔都语(印度)
// 欧洲
"en-US": "en", // 英语(美国)
"en-GB": "en", // 英语(英国)
@ -150,11 +161,25 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
/**
* 释放资源
* 添加线程安全保护,防止重复释放
*/
public func dispose() {
disposeLock.lock()
defer { disposeLock.unlock() }
if isDisposed {
os_log("翻译服务已经释放,跳过重复释放", log: log, type: .info)
return
}
os_log("开始释放翻译服务资源", log: log, type: .info)
isDisposed = true
isInitialized = false
// 取消并失效URLSession
urlSession.invalidateAndCancel()
os_log("火山翻译服务已释放", log: log, type: .info)
os_log("翻译服务资源释放完成", log: log, type: .info)
}
// MARK: - Private Methods
@ -203,6 +228,7 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
/**
* 执行翻译
* 添加服务状态检查,防止在已释放的会话中创建任务
*/
private func performTranslation(
text: String,
@ -210,6 +236,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
targetLanguage: String
) async -> IntegratedSpeechTranslationService.TranslationResult {
// 检查服务是否已被释放
disposeLock.lock()
let disposed = isDisposed
disposeLock.unlock()
if disposed {
os_log("翻译服务已释放,取消翻译请求", log: log, type: .error)
return IntegratedSpeechTranslationService.TranslationResult(
success: false,
error: "翻译服务已释放"
)
}
do {
// 转换语言代码
let sourceCode = getLanguageCode(languageCode: sourceLanguage)
@ -263,6 +302,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString)
// 再次检查服务状态,防止在请求构建过程中被释放
disposeLock.lock()
let stillDisposed = isDisposed
disposeLock.unlock()
if stillDisposed {
os_log("翻译服务在请求过程中被释放,取消网络请求", log: log, type: .error)
return IntegratedSpeechTranslationService.TranslationResult(
success: false,
error: "翻译服务已释放"
)
}
// 发送请求
let (data, response) = try await urlSession.data(for: request)

54
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -144,11 +144,11 @@ public class SimpleAudioReceiver: NSObject {
while self.isRunning {
if !self._isWriting {
// print("startWriteThread 111")
usleep(10_000)
continue
}
// print("startWriteThread 222")
guard let dataToWrite = self.writeQueue.take() else { continue }
do {
@ -157,14 +157,12 @@ public class SimpleAudioReceiver: NSObject {
self.audioDataCallback!.onAudio(dataToWrite)
}
if self.pushAudioStream != nil && isContinuousRecognitionActive {
try self.pushAudioStream?.write(dataToWrite)
}
if self.recordfile != nil && isRecord{
// print("写入recordfile数据长度: \(dataToWrite.count)")
recordfile?.saveAudioDataToWav(dataToWrite)
}
// print("写入数据长度: \(dataToWrite.count)")
} catch {
print("推送音频数据失败: \(error.localizedDescription)")
}
@ -183,9 +181,8 @@ public class SimpleAudioReceiver: NSObject {
if audioSourceType != .external {
return
}
// print("外部data=\(data)")
// 放入队列,由写线程写入
writeQueue.put(data)
self.writeQueue.put(data)
}
/**
@ -220,18 +217,22 @@ public class SimpleAudioReceiver: NSObject {
}
}
/**
* 运行外部音频捕获
* 修复OSStatus error -50问题
*/
private func runExternalCapture() {
if audioSourceType == .external {
do {
try audioSession.setCategory(.playback,
mode: .videoChat,
options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路
]) // 添加音频优先级控制
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
} catch {
print("设置音频会话失败: \(error.localizedDescription)")
}
// do {
// try audioSession.setCategory(.playback,
// mode: .videoChat,
// options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路
// ]) // 添加音频优先级控制
// try audioSession.overrideOutputAudioPort(.none)
// try audioSession.setActive(true)
// } catch {
// print("设置音频会话失败: \(error.localizedDescription)")
// }
//pushAudioData(data: Data())
micCapture.stopCapture()
@ -336,7 +337,7 @@ public func restoreOriginalAudioState() {
public func stopMicrophoneCapture() {
self.writeQueue.clear()
_isWriting=false
micCapture.stopCapture()
print("stopMicrophoneCapture")
@ -517,6 +518,23 @@ private class LinkedBlockingQueue<T> {
// 通知有新元素可用
availableItems.signal()
}
/**
* 清空队列
*/
func clear() {
queueLock.lock()
defer { queueLock.unlock() }
// 检查队列是否已关闭
if isClosed {
return
}
// 添加元素(无容量限制,与Android一致)
queue.removeAll()
}
/**

63
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -178,7 +178,7 @@ object BleService {
*/
fun initialize(appContext: Context): Boolean {
if (isInitialized) return true
try {
this.context = appContext.applicationContext
// 初始化蓝牙管理器和适配器
@ -190,6 +190,11 @@ object BleService {
// 初始化Handler
scanHandler = Handler(Looper.getMainLooper())
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
// 启动队列处理
startAudioQueueProcessing()
@ -202,13 +207,7 @@ object BleService {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false
}
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
isInitialized = true
Log.d(TAG, "BLE服务初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "BLE服务初始化失败: ${e.message}", e)
@ -838,22 +837,7 @@ startBytesStatistics()
recordfile?.saveAudioDataToWav(data)
// 将完整的帧加入队列,由专门的线程处理
audioDataQueue.offer(data)
// // 将接收到的数据添加到缓存中
// synchronized(audioDataBuffer) {
// audioDataBuffer.addAll(data.toList())
// // 当缓存达到阈值时,提取完整的帧进行解码
// while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) {
// // 提取一个完整的帧(1280字节)
// val frameData = ByteArray(AUDIO_BUFFER_SIZE)
// for (i in 0 until AUDIO_BUFFER_SIZE) {
// frameData[i] = audioDataBuffer.removeAt(0)
// }
// Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码")
// }
// }
} else {
// Log.d(TAG, "Opus解码流未启动,忽略音频数据")
}
@ -1243,7 +1227,7 @@ startBytesStatistics()
// Opus 解码相关
// ======================================================================================================
private fun startOpusStreamDecoding(
public fun startOpusStreamDecoding(
hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头
channel: Int = 1,
sampleRate: Int = 16000, // 确认设备端Opus编码采样率
@ -1633,7 +1617,12 @@ startBytesStatistics()
) // 或其他默认声道
)
}
fun closeCodec1(): Boolean {
recordfile1!!.closeFile()
recordfile!!.closeFile()
return true
}
/**
* 控制编解码 - 打开解码
*/
@ -1706,16 +1695,20 @@ startBytesStatistics()
*/
fun openEncoder(): Boolean {
Log.i(TAG, "打开编码0xB1")
recordfile1!!.closeFile()
recordfile1!!.creatingFiles()
recordfile!!.closeFile()
recordfile!!.creatingFiles()
startOpusStreamDecoding(false, 1, 16000, 40)
// 目前仅发送命令通知设备开始编码。
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf(
BleConst.CODEC_CONTROL_ENCODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道
)
)
// // 目前仅发送命令通知设备开始编码。
// return sendCommand(
// BleConst.CMD_CONTROL_CODEC.toByte(),
// byteArrayOf(
// BleConst.CODEC_CONTROL_ENCODE_ON.toByte(),
// BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道
// )
// )
return true
}
@ -2101,7 +2094,7 @@ class RecordingFile(private val context: Context) {
}
/**
* 保存音频数据到 WAV 文件(异步)
* 保存音频数据到 opus 文件(异步)
*/
internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return

24
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -586,22 +586,24 @@ private var cmdReplyType: UInt8 = 0
}
/// 打开编码器
func openEncoder() -> Bool {
/// - Returns: 操作是否成功
public func openEncoder() -> Bool {
os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info)
// 确保Opus解码器处于活跃状态(如果不是则启动)
// if opusProcessor?.isDecoding() != true {
startOpusDecoding()
//}
let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
// let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
// let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
if result {
os_log("✅ 编码器命令发送成功", log: logger, type: .info)
} else {
os_log("❌ 编码器命令发送失败", log: logger, type: .error)
}
// if result {
// os_log("✅ 编码器命令发送成功", log: logger, type: .info)
// } else {
// os_log("❌ 编码器命令发送失败", log: logger, type: .error)
// }
return result
//return result
return true
}
/// 打开解码器
@ -1108,9 +1110,7 @@ private var cmdReplyType: UInt8 = 0
notifyCharacteristic != nil ? "✅" : "❌",
audioCharacteristic != nil ? "✅" : "❌")
// 接收到唤醒信号,打开编码器 (设备侧)
let openResult = openEncoder()
os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败")
notifyWakeupSignalReceived()
default:

Loading…
Cancel
Save