Browse Source

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

weicu
tanlongsheng 1 year ago
parent
commit
50820aa745
  1. 2
      lib/data/services/network/nw_api.dart
  2. 91
      lib/modules/agent/controllers/agent_controller.dart
  3. 51
      lib/modules/agent/views/agent_view.dart
  4. 1
      lib/modules/meeting/controllers/meeting_record_controller.dart
  5. 150
      lib/modules/settings/views/settings_view.dart
  6. 2
      lib/modules/translation/controllers/translation_controller.dart
  7. 80
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  8. 18
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  9. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  10. 26
      local_plugins/agent_service/lib/agent_service.dart
  11. 737
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  12. 6
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  13. 9
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  14. 16
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  15. 9
      local_plugins/music_service/android/src/main/res/drawable/ic_next.xml
  16. 9
      local_plugins/music_service/android/src/main/res/drawable/ic_pause.xml
  17. 9
      local_plugins/music_service/android/src/main/res/drawable/ic_play.xml
  18. 9
      local_plugins/music_service/android/src/main/res/drawable/ic_prev.xml

2
lib/data/services/network/nw_api.dart

@ -1,5 +1,5 @@
class NWApi { class NWApi {
// static const baseApi = "https://www.deapsound.com"; // static const baseApi = "https://www.deapsound.com";
// static const baseApi = "https://api.deapsound.com"; //static const baseApi = "https://api.deapsound.com";
static const baseApi = "https://api.ideapsound.com"; static const baseApi = "https://api.ideapsound.com";
} }

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

@ -93,7 +93,7 @@ class AgentController extends GetxController {
final isPushToTalkActive = false.obs; // 是否正在按住说话 final isPushToTalkActive = false.obs; // 是否正在按住说话
// 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住) // 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住)
final isPushToTalkMode = false.obs; // 是否处于按住说话模式 //final isPushToTalkMode = false.obs; // 是否处于按住说话模式
// 蓝牙唤醒状态 // 蓝牙唤醒状态
final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话 final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话
@ -143,6 +143,7 @@ class AgentController extends GetxController {
_initMusicStatus(); //初始化音乐状态 _initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调 _setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器 _setupTextInputListener(); //设置文本输入监听器
startVoiceInput(); //开启语音输入
} }
// 初始化音乐播放状态 // 初始化音乐播放状态
@ -355,6 +356,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.recognitionStarted: case AgentServiceEventType.recognitionStarted:
isListening.value = true; isListening.value = true;
isTextInputMode.value = false; isTextInputMode.value = false;
print("订阅代理服务事件识别开始");
// isTextInputMode.refresh(); // isTextInputMode.refresh();
// 只标记识别开始,不添加消息 // 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始'); Logger.i(TAG, '识别开始');
@ -371,7 +373,7 @@ class AgentController extends GetxController {
} else { } else {
// 只有在非按住说话模式且非打电话模式下才自动切换到文字模式 // 只有在非按住说话模式且非打电话模式下才自动切换到文字模式
// 按住说话模式下应该保持在语音模式,等待下次按住 // 按住说话模式下应该保持在语音模式,等待下次按住
if (!isPushToTalkMode.value && !isPhoneCallMode.value) { if (!isPhoneCallMode.value) {
isTextInputMode.value = true; isTextInputMode.value = true;
} }
} }
@ -890,19 +892,18 @@ class AgentController extends GetxController {
// 切换到文本模式时,禁用按住说话模式并停止任何正在进行的语音输入 // 切换到文本模式时,禁用按住说话模式并停止任何正在进行的语音输入
if (isTextInputMode.value) { if (isTextInputMode.value) {
// 切换到文本模式 // 切换到文本模式
isPushToTalkMode.value = false; isTextInputMode.value = true;
stopVoiceInput(); // stopVoiceInput();
} else { } else {
// 切换到语音模式,启用按住说话模式 // 切换到语音模式,启用按住说话模式
isPushToTalkMode.value = true; isTextInputMode.value = false;
} }
} }
// 开始语音输入 // 开始语音输入
Future<void> startVoiceInput() async { Future<void> startVoiceInput() async {
if (isListening.value) return; if (isListening.value) return;
print("开始语音输入"); // 检查麦克风权限
// 检查麦克风权限
final hasPermission = await PermissionUtil.instance.requestPermission( final hasPermission = await PermissionUtil.instance.requestPermission(
// context: Get.context!, // context: Get.context!,
permissionType: Permission.microphone, permissionType: Permission.microphone,
@ -1129,13 +1130,14 @@ class AgentController extends GetxController {
// 保存当前输入模式 // 保存当前输入模式
void _saveCurrentInputMode() { void _saveCurrentInputMode() {
if (isPushToTalkMode.value) { if (!isTextInputMode.value) {
_previousInputMode = VoiceInputMode.pushToTalk; _previousInputMode = VoiceInputMode.pushToTalk;
} else if (isTextInputMode.value) {
_previousInputMode = VoiceInputMode.text;
} else { } else {
_previousInputMode = null; _previousInputMode = VoiceInputMode.text;
} }
// else {
// _previousInputMode = null;
// }
} }
// 恢复之前的输入模式 // 恢复之前的输入模式
@ -1143,16 +1145,16 @@ class AgentController extends GetxController {
switch (_previousInputMode) { switch (_previousInputMode) {
case VoiceInputMode.pushToTalk: case VoiceInputMode.pushToTalk:
isTextInputMode.value = false; isTextInputMode.value = false;
isPushToTalkMode.value = true; //isPushToTalkMode.value = true;
break; break;
case VoiceInputMode.text: case VoiceInputMode.text:
isTextInputMode.value = true; isTextInputMode.value = true;
isPushToTalkMode.value = false; // isPushToTalkMode.value = false;
break; break;
default: default:
// 默认恢复到文字模式 // 默认恢复到文字模式
isTextInputMode.value = true; isTextInputMode.value = true;
isPushToTalkMode.value = false; //isPushToTalkMode.value = false;
break; break;
} }
_previousInputMode = null; // 清除历史记录 _previousInputMode = null; // 清除历史记录
@ -1171,8 +1173,8 @@ class AgentController extends GetxController {
// 进入打电话模式 // 进入打电话模式
isPhoneCallMode.value = true; isPhoneCallMode.value = true;
// 如果当前是文本模式,切换到语音模式 // 如果当前是文本模式,切换到语音模式????
if (isTextInputMode.value) { if (isListening.value) {
// 检查麦克风权限 // 检查麦克风权限
final hasPermission = await PermissionUtil.instance.requestPermission( final hasPermission = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone, permissionType: Permission.microphone,
@ -1193,8 +1195,10 @@ class AgentController extends GetxController {
// 切换到语音模式并开始语音输入 // 切换到语音模式并开始语音输入
isTextInputMode.value = false; isTextInputMode.value = false;
// 打电话模式不使用按住说话模式,而是持续语音输入 // 打电话模式不使用按住说话模式,而是持续语音输入
isPushToTalkMode.value = false; //isPushToTalkMode.value = false;
await startVoiceInput(); // 开始语音识别
print("开始打电话模式,开始语音输入");
await AgentService.audioContinue();
} }
} }
@ -1206,8 +1210,9 @@ class AgentController extends GetxController {
isPhoneCallMode.value = false; isPhoneCallMode.value = false;
// 停止语音输入 // 停止语音输入
await stopVoiceInput(); //await stopVoiceInput();
// 开始语音识别
await AgentService.audioPause();
// 恢复之前的输入模式,而不是强制切换到文字模式 // 恢复之前的输入模式,而不是强制切换到文字模式
_restorePreviousInputMode(); _restorePreviousInputMode();
} }
@ -1240,23 +1245,23 @@ class AgentController extends GetxController {
// 验证语音输入状态一致性 // 验证语音输入状态一致性
void _validateVoiceInputStates() { void _validateVoiceInputStates() {
// 检查状态冲突 // 检查状态冲突
if (isPhoneCallMode.value && isPushToTalkMode.value) { // if (isPhoneCallMode.value && isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式'); // Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式');
isPushToTalkMode.value = false; // isPushToTalkMode.value = false;
} // }
// 检查文字模式与语音模式的一致性 // // 检查文字模式与语音模式的一致性
if (isTextInputMode.value && // if (isTextInputMode.value &&
(isPushToTalkMode.value || isPhoneCallMode.value)) { // (isPushToTalkMode.value || isPhoneCallMode.value)) {
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); // Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false; // isTextInputMode.value = false;
} // }
// 检查按住说话状态的合理性 // // 检查按住说话状态的合理性
if (isPushToTalkActive.value && !isPushToTalkMode.value) { // if (isPushToTalkActive.value && !isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式'); // Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式');
isPushToTalkActive.value = false; // isPushToTalkActive.value = false;
} // }
} }
// 安全的状态切换方法 // 安全的状态切换方法
@ -1266,21 +1271,21 @@ class AgentController extends GetxController {
switch (targetMode) { switch (targetMode) {
case VoiceInputMode.text: case VoiceInputMode.text:
isTextInputMode.value = true; isTextInputMode.value = true;
isPushToTalkMode.value = false; // isPushToTalkMode.value = false;
isPhoneCallMode.value = false; isPhoneCallMode.value = false;
isPushToTalkActive.value = false; isPushToTalkActive.value = false;
break; break;
case VoiceInputMode.pushToTalk: case VoiceInputMode.pushToTalk:
isTextInputMode.value = false; isTextInputMode.value = false;
isPushToTalkMode.value = true; // isPushToTalkMode.value = true;
isPhoneCallMode.value = false; isPhoneCallMode.value = false;
isPushToTalkActive.value = false; isPushToTalkActive.value = false;
break; break;
case VoiceInputMode.phoneCall: case VoiceInputMode.phoneCall:
isTextInputMode.value = false; isTextInputMode.value = false;
isPushToTalkMode.value = false; // isPushToTalkMode.value = false;
isPhoneCallMode.value = true; isPhoneCallMode.value = true;
isPushToTalkActive.value = false; isPushToTalkActive.value = false;
break; break;
@ -1295,7 +1300,7 @@ class AgentController extends GetxController {
Logger.i(TAG, '🎤 开始按住说话'); Logger.i(TAG, '🎤 开始按住说话');
// 只有在语音模式且按住说话模式下才能按住说话 // 只有在语音模式且按住说话模式下才能按住说话
if (isTextInputMode.value || !isPushToTalkMode.value) return; if (isTextInputMode.value) return;
// 如果已经在按住说话,不重复开始 // 如果已经在按住说话,不重复开始
if (isPushToTalkActive.value) return; if (isPushToTalkActive.value) return;
@ -1303,8 +1308,10 @@ class AgentController extends GetxController {
// 设置按住说话状态 // 设置按住说话状态
isPushToTalkActive.value = true; isPushToTalkActive.value = true;
print("开始按住说话,状态已设置为true");
// 开始语音识别 // 开始语音识别
await startVoiceInput(); await AgentService.audioContinue();
// await startVoiceInput();
} }
// 结束按住说话 // 结束按住说话
@ -1316,9 +1323,9 @@ class AgentController extends GetxController {
// 清除按住说话状态(但保持按住说话模式) // 清除按住说话状态(但保持按住说话模式)
isPushToTalkActive.value = false; isPushToTalkActive.value = false;
await AgentService.audioPause();
// 停止语音识别 // 停止语音识别
await stopVoiceInput(); // await stopVoiceInput();
} }
// 获取音乐播放状态 // 获取音乐播放状态

51
lib/modules/agent/views/agent_view.dart

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -374,7 +375,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
final isTextMode = final isTextMode =
controller.isTextInputMode.value; controller.isTextInputMode.value;
if (isTextMode) { if (controller.isTextInputMode.value) {
// 文本输入模式 - 限制最大高度并只向上扩展 // 文本输入模式 - 限制最大高度并只向上扩展
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -466,16 +467,44 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
), ),
); );
} else { } else {
Timer? _holdTimer; // 增加延迟触发的计时器
// 语音输入模式 - 按住说话模式 // 语音输入模式 - 按住说话模式
return Listener( return Listener(
onPointerDown: (_) { onPointerDown: (_) {
// 开始按住说话 // 设置300ms延迟防止误触
controller.startPushToTalk(); _holdTimer = Timer(
const Duration(milliseconds: 200),
() {
controller.startPushToTalk();
// if (activeSpeaker == 0) {
// if (speakerId == 1) {
// controller
// .restoreOriginalAudioState();
// } else {
// controller
// .disableBluetoothAudio();
// }
// controller
// .setActiveSpeaker(speakerId);
// }
});
}, },
onPointerUp: (_) { onPointerUp: (_) {
// 结束按住说话 _holdTimer?.cancel();
controller.endPushToTalk(); controller.endPushToTalk();
}, },
onPointerCancel: (_) {
_holdTimer?.cancel();
controller.endPushToTalk();
},
// onLongPressStart: (_) {
// // 开始按住说话
// controller.startPushToTalk();
// },
// onLongPressEnd: (_) {
// // 结束按住说话
// controller.endPushToTalk();
// },
// onLongPressCancel: () { // onLongPressCancel: () {
// // 取消按住说话 // // 取消按住说话
// controller.endPushToTalk(); // controller.endPushToTalk();
@ -1020,13 +1049,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Obx(() { Obx(() {
final isActive = controller.isListening.value; final isActive = controller.isListening.value;
return GestureDetector( return GestureDetector(
onTap: () { // onTap: () {
if (isActive) { // if (isActive) {
controller.stopVoiceInput(); // controller.stopVoiceInput();
} else { // } else {
controller.startVoiceInput(); // controller.startVoiceInput();
} // }
}, // },
child: Container( child: Container(
width: 80, width: 80,
height: 80, height: 80,

1
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -138,7 +138,6 @@ class MeetingRecordController extends GetxController
_bleManager.closeCodec(); _bleManager.closeCodec();
} }
_asrService.stopRecord(true); _asrService.stopRecord(true);
_asrService.setPause();
_asrService.stopContinuousRecognition(); _asrService.stopContinuousRecognition();
isRecording.value = false; isRecording.value = false;
} }

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

@ -45,8 +45,8 @@ class SettingsView extends GetView<SettingsController> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [ children: [
_buildUserInfoCard(isDarkMode), _buildUserInfoCard(isDarkMode),
SizedBox(height: 20.h), // SizedBox(height: 20.h),
_buildAISettingsSection(isDarkMode), // _buildAISettingsSection(isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
_buildSystemSettingsSection(isDarkMode), _buildSystemSettingsSection(isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
@ -505,9 +505,9 @@ class SettingsView extends GetView<SettingsController> {
// 清除配对设备 // 清除配对设备
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: '设备管理', title: 'deviceManagement'.tr, // 设备管理
subtitle: '设备管理列表', subtitle: 'deviceManagementInfo'.tr, // 设备管理列表
icon: Icons.bluetooth_disabled, icon: Icons.settings_bluetooth,
iconBgColor: isDarkMode iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3) ? Colors.red[900]!.withOpacity(0.3)
: Colors.red[100]!, : Colors.red[100]!,
@ -633,8 +633,8 @@ class SettingsView extends GetView<SettingsController> {
// : Colors.grey[200]), // : Colors.grey[200]),
// 测试ble耳机固件版本升级 // 测试ble耳机固件版本升级
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: 'ota升级', title: '固件升级'.tr,
subtitle: '测试ble耳机固件版本升级', subtitle: 'ble耳机固件版本升级'.tr,
icon: Icons.system_update_alt, icon: Icons.system_update_alt,
iconBgColor: isDarkMode iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3) ? Colors.orange[900]!.withOpacity(0.3)
@ -660,140 +660,6 @@ class SettingsView extends GetView<SettingsController> {
Get.toNamed(Routes.permissions); Get.toNamed(Routes.permissions);
}, },
), ),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 退出登录
// _buildSimpleNavigationSetting(
// title: 'logout'.tr, // 退出登录
// icon: Icons.logout,
// iconBgColor: isDarkMode
// ? Colors.red[900]!.withOpacity(0.3)
// : Colors.red[50]!,
// iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// titleColor:
// isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// onTap: () {
// // 退出登录确认对话框
// Get.dialog(
// AlertDialog(
// backgroundColor:
// isDarkMode ? Colors.grey[900] : Colors.white,
// title: Text(
// 'logout'.tr, // 退出登录
// style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black87,
// ),
// ),
// content: Text(
// 'logoutTips'.tr, // 确定要退出当前账号吗?
// style: TextStyle(
// color:
// isDarkMode ? Colors.white70 : Colors.black54,
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Get.back(),
// child: Text(
// 'cancel'.tr, // 取消
// style: TextStyle(
// color: isDarkMode
// ? Colors.white70
// : Colors.black54,
// ),
// ),
// ),
// TextButton(
// onPressed: () {
// Get.back();
// controller.signOut();
// },
// child: Text(
// 'confirm'.tr, // 确定
// style: TextStyle(
// color: isDarkMode
// ? Colors.red[300]
// : Colors.red[600],
// ),
// ),
// ),
// ],
// ),
// );
// },
// isDarkMode: isDarkMode,
// ),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// 注销账号
// _buildSimpleNavigationSetting(
// title: 'deleteAccount'.tr, // 注销账号
// icon: Icons.delete_forever,
// iconBgColor: isDarkMode
// ? Colors.red[900]!.withOpacity(0.3)
// : Colors.red[50]!,
// iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// titleColor:
// isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// onTap: () {
// // 注销账号确认对话框
// Get.dialog(
// AlertDialog(
// backgroundColor:
// isDarkMode ? Colors.grey[900] : Colors.white,
// title: Text(
// 'deleteAccount'.tr, // 注销账号
// style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black87,
// ),
// ),
// content: Text(
// 'deleteAccountTips'.tr, // 确定要永久删除您的账号吗?此操作不可撤销
// style: TextStyle(
// color:
// isDarkMode ? Colors.white70 : Colors.black54,
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Get.back(),
// child: Text(
// 'cancel'.tr, // 取消
// style: TextStyle(
// color: isDarkMode
// ? Colors.white70
// : Colors.black54,
// ),
// ),
// ),
// TextButton(
// onPressed: () {
// Get.back();
// controller.deleteAccount();
// },
// child: Text(
// 'confirm'.tr, // 确定
// style: TextStyle(
// color: isDarkMode
// ? Colors.red[300]
// : Colors.red[600],
// ),
// ),
// ),
// ],
// ),
// );
// },
// isDarkMode: isDarkMode,
// ),
], ],
), ),
), ),
@ -873,7 +739,7 @@ class SettingsView extends GetView<SettingsController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'DeepSound', 'DeapSound',
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

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

@ -430,7 +430,7 @@ class TranslationController extends GetxController {
try { try {
await _asrService.stopContinuousRecognition(); await _asrService.stopContinuousRecognition();
_asrService.setPause();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
isRecognizing.value = false; isRecognizing.value = false;

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

@ -58,7 +58,7 @@ object AgentService : CoroutineScope {
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>()) private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// Azure服务 // Azure服务
private var azureAsrHelper: AzureAsrHelper? = null var azureAsrHelper: AzureAsrHelper? = null
// 使用BytedanceTTS作为唯一的TTS实现 // 使用BytedanceTTS作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null private var ttsService: AzureTtsHelper? = null
@ -111,6 +111,7 @@ object AgentService : CoroutineScope {
private var lastInterruptTime = 0L private var lastInterruptTime = 0L
private val INTERRUPT_DEBOUNCE_MS = 300L private val INTERRUPT_DEBOUNCE_MS = 300L
private var audioSourceType= AzureAsrHelper.AudioSourceType.MICROPHONE
// 语气词过滤列表(中英文) // 语气词过滤列表(中英文)
private val FILLER_WORDS = setOf( private val FILLER_WORDS = setOf(
// 中文语气词 // 中文语气词
@ -351,15 +352,16 @@ object AgentService : CoroutineScope {
*/ */
private fun startIdleCheck() { private fun startIdleCheck() {
stopIdleCheck() // 先停止现有的检查 stopIdleCheck() // 先停止现有的检查
Log.d(TAG, "启动空闲检测: ${isRecognitionActive}")
if (!isRecognitionActive) return if (!isRecognitionActive) return
idleCheckJob = launch { idleCheckJob = launch {
try { try {
delay(maxIdleSeconds * 1000L) delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
stopRecognition() Log.d(TAG, "启动空闲检测: stopRecognition")
//stopRecognition()
sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds))
} }
} catch (e: CancellationException) { } catch (e: CancellationException) {
@ -393,10 +395,17 @@ object AgentService : CoroutineScope {
*/ */
fun startRecognition(isExternalActive: Boolean): Boolean { fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "服务未初始化") Log.d(TAG, "服务未初始化")
return false return false
} }
Log.d(TAG, "麦克风音频未暂停,无法设置${azureAsrHelper!!.isPaused},$isExternalActive")
if(!(azureAsrHelper!!.isPaused)&&isExternalActive)
{
Log.d(TAG, "麦克风音频未暂停,无法设置")
BleService.closeCodec()
return false
}
Log.d(TAG, "开始语音识别")
if (isRecognitionActive) return true if (isRecognitionActive) return true
// 检查录音权限 // 检查录音权限
@ -410,7 +419,7 @@ object AgentService : CoroutineScope {
_hasSpeechDetected.set(false) _hasSpeechDetected.set(false)
try { try {
val audioSourceType = if (isExternalActive) { audioSourceType = if (isExternalActive) {
AzureAsrHelper.AudioSourceType.EXTERNAL AzureAsrHelper.AudioSourceType.EXTERNAL
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
@ -480,6 +489,7 @@ object AgentService : CoroutineScope {
override fun onSessionStarted() { override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started")) sendEvent("recognition_started", mapOf("status" to "started"))
startIdleCheck() startIdleCheck()
Log.d(TAG, "onSessionStarted")
audioPlayer?.playAudio(R.raw.start) audioPlayer?.playAudio(R.raw.start)
} }
@ -489,12 +499,14 @@ object AgentService : CoroutineScope {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "onSessionStopped")
audioPlayer?.playAudio(R.raw.stop) audioPlayer?.playAudio(R.raw.stop)
} }
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(reason: String, errorDetails: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "recognition_canceled")
BleService.closeCodec() BleService.closeCodec()
sendEvent("recognition_canceled", mapOf( sendEvent("recognition_canceled", mapOf(
"reason" to reason, "reason" to reason,
@ -505,6 +517,7 @@ object AgentService : CoroutineScope {
override fun onError(error: String) { override fun onError(error: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "error")
BleService.closeCodec() BleService.closeCodec()
Log.e(TAG, "语音识别错误: $error") Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
@ -536,6 +549,7 @@ object AgentService : CoroutineScope {
try { try {
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
Log.d(TAG, "停止语音识别")
BleService.closeCodec() BleService.closeCodec()
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
@ -545,7 +559,55 @@ object AgentService : CoroutineScope {
stopIdleCheck() stopIdleCheck()
} }
} }
/**
* 继续音频输入
*/
fun audioContinue() {
try {
if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL)
{
Log.d(TAG, "当前是什么输入:$audioSourceType")
BleService.closeCodec()
stopRecognition()
startRecognition(false)
}
// if (!isRecognitionActive) {
// }
azureAsrHelper?.audioContinue()
//BleService.closeCodec()
//_isRecognitionActive.set(false)
//stopIdleCheck()
} catch (e: Exception) {
Log.e(TAG, "暂停音频输入异常: ${e.message}")
// _isRecognitionActive.set(false)
//stopIdleCheck()
}
}
/**
* 暂停音频输入
*/
fun audioPause() {
if (!isRecognitionActive) return
try {
azureAsrHelper?.audioPause()
Log.d(TAG, "暂停音频输入")
BleService.closeCodec()
//_isRecognitionActive.set(false)
//stopIdleCheck()
} catch (e: Exception) {
Log.e(TAG, "暂停音频输入异常: ${e.message}")
// _isRecognitionActive.set(false)
//stopIdleCheck()
}
}
/** /**
* 打断当前响应 * 打断当前响应
* 停止TTS播放和AI流输出 * 停止TTS播放和AI流输出
@ -676,6 +738,7 @@ object AgentService : CoroutineScope {
hasImage: Boolean = false, hasImage: Boolean = false,
imagePath: String? = null imagePath: String? = null
) { ) {
Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它 // 如果有正在进行的AI流式输出,先停止它
stopAiStream() stopAiStream()
stopTts() stopTts()
@ -685,6 +748,7 @@ object AgentService : CoroutineScope {
currsessionId = UUID.randomUUID().toString() currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出 // 设置状态为正在流式输出
_isAiStreaming.set(true) _isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal")
audioPlayer?.playAudio(R.raw.await, true,0.3f) audioPlayer?.playAudio(R.raw.await, true,0.3f)
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
@ -1190,6 +1254,7 @@ object AgentService : CoroutineScope {
fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) {
synchronized(playbackLock) { synchronized(playbackLock) {
try { try {
Log.d(TAG, "playAudio${isLooping}")
// 强制停止并释放之前的资源 // 强制停止并释放之前的资源
forceStop() forceStop()
@ -1233,6 +1298,7 @@ object AgentService : CoroutineScope {
fun stopAudio() { fun stopAudio() {
synchronized(playbackLock) { synchronized(playbackLock) {
try { try {
Log.d(TAG, "stopAudio")
mediaPlayer?.apply { mediaPlayer?.apply {
if (isMediaPlayerPlaying()) { if (isMediaPlayerPlaying()) {
stop() stop()

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

@ -103,6 +103,24 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
Log.e(TAG, "停止对话失败", e) Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
} }
}
"audioContinue" -> {
try {
AgentService.audioContinue()
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "audioContinue失败", e)
result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null)
}
}
"audioPause" -> {
try {
AgentService.audioPause()
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "audioContinue失败", e)
result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null)
}
} }
"processTextInput" -> { "processTextInput" -> {
val text = call.argument<String>("text") val text = call.argument<String>("text")

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

@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()
// AgentService.speakText("我在!") // AgentService.speakText("我在!")
// 启动语音识别 // 启动语音识别
if (AgentService.isRecognitionActive) { if (AgentService.isRecognitionActive&&AgentService.azureAsrHelper!!.isPaused) {
// 如果已经在识别,则重新启动 // 如果已经在识别,则重新启动
stopListening() stopListening()
} }

26
local_plugins/agent_service/lib/agent_service.dart

@ -261,6 +261,32 @@ class AgentService {
} }
} }
/// 停止对话
///
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> audioContinue() async {
try {
final bool result = await _channel.invokeMethod('audioContinue');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
}
}
/// 停止对话
///
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> audioPause() async {
try {
final bool result = await _channel.invokeMethod('audioPause');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
}
}
/// 处理文本输入 /// 处理文本输入
/// ///
/// [text] 文本内容 /// [text] 文本内容

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

@ -34,9 +34,6 @@ class AzureAsrHelper(private val context: Context) {
private val tag = "AzureAsrHelper" private val tag = "AzureAsrHelper"
// 核心组件 // 核心组件
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var recognizer: SpeechRecognizer? = null private var recognizer: SpeechRecognizer? = null
private var audioConfig: AudioConfig? = null private var audioConfig: AudioConfig? = null
@ -55,7 +52,7 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理 // 音频处理
// private var microphoneStream: MicrophoneStream? = null private var audioStream: AudioStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null private var externalAudioStream: ExternalAudioPullStream? = null
// 音频模式管理 // 音频模式管理
@ -64,11 +61,7 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件处理 // 录音文件处理
var recordfile: RecordFile? = null var recordfile: RecordFile? = null
var isPaused = true
private var isPaused = false
private var audioRecord: AudioRecord? = null
private var captureThread: Thread? = null
private var audioStream: PushAudioInputStream? = null
/** /**
* 音频来源类型 * 音频来源类型
@ -129,9 +122,7 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 1. 创建可控制的音频流
val format = getOptimalAudioFormat()
audioStream = AudioInputStream.createPushStream(format)
// 初始化音频管理器 // 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
@ -197,6 +188,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun setupRecognizer(): Boolean { private fun setupRecognizer(): Boolean {
try { try {
// 清理旧的识别器 // 清理旧的识别器
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
@ -211,7 +203,11 @@ class AzureAsrHelper(private val context: Context) {
AudioSourceType.EXTERNAL -> { AudioSourceType.EXTERNAL -> {
// 改用拉流方式处理外部音频 // 改用拉流方式处理外部音频
Log.d(tag, "改用拉流方式处理外部音频")
setupExternalAudioStream() setupExternalAudioStream()
} }
} }
@ -237,13 +233,25 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun setupMicrophoneStream() { private fun setupMicrophoneStream() {
try { try {
Log.d(tag, "设置麦克风流 - 使用拉流方式: }")
restoreOriginalAudioState()
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = AudioStream()
audioStream!!.initAudioRecord()
}
// else
// {
// audioStream.stopCaptureThread()
// audioStream.startCaptureThread()
// }
// 创建麦克风流 // 创建麦克风流
// microphoneStream = MicrophoneStream() // microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
// audioConfig = AudioConfig.fromStreamInput(microphoneStream) // audioConfig = AudioConfig.fromStreamInput(microphoneStream)
audioConfig = AudioConfig.fromStreamInput(audioStream) audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}") Log.e(tag, "设置麦克风流失败: ${e.message}")
@ -278,6 +286,8 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun setupExternalAudioStream() { private fun setupExternalAudioStream() {
try { try {
Log.d(tag, "设置外部音频流 - 使用拉流方式: }")
//disableBluetoothAudio()
// 创建外部音频拉流对象 // 创建外部音频拉流对象
externalAudioStream = ExternalAudioPullStream() externalAudioStream = ExternalAudioPullStream()
@ -299,7 +309,6 @@ class AzureAsrHelper(private val context: Context) {
if (audioSourceType != AudioSourceType.EXTERNAL) { if (audioSourceType != AudioSourceType.EXTERNAL) {
return return
} }
// 使用拉流模式,将数据推入队列 // 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data) externalAudioStream?.pushAudio(data)
@ -379,8 +388,7 @@ class AzureAsrHelper(private val context: Context) {
} }
try { try {
// 5. 初始化麦克风
initAudioRecord()
// 设置各种事件监听 // 设置各种事件监听
setupEventListeners(callback) setupEventListeners(callback)
@ -389,9 +397,7 @@ class AzureAsrHelper(private val context: Context) {
// 开始连续识别 // 开始连续识别
recognizer?.startContinuousRecognitionAsync() recognizer?.startContinuousRecognitionAsync()
// 开始录音 audioStream?.startCaptureThread()
audioRecord?.startRecording()
startCaptureThread() // 再启动数据读取线程
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
@ -442,6 +448,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.sessionStarted?.addEventListener( recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调 // 直接在当前线程调用回调
Log.d(tag, "会话开始事件")
callback.onSessionStarted() callback.onSessionStarted()
} }
) )
@ -450,6 +457,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.sessionStopped?.addEventListener( recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调 // 直接在当前线程调用回调
Log.d(tag, "会话结束事件")
callback.onSessionStopped() callback.onSessionStopped()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopAudioProcessing() stopAudioProcessing()
@ -497,8 +505,6 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音
stopMicrophoneCapture()
// 会话结束事件会设置isContinuousRecognitionActive = false // 会话结束事件会设置isContinuousRecognitionActive = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -508,8 +514,6 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音
stopMicrophoneCapture()
// 尝试强制关闭识别器 // 尝试强制关闭识别器
try { try {
recognizer?.close() recognizer?.close()
@ -538,7 +542,6 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
} }
releaseAudioResources()
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
@ -552,9 +555,6 @@ class AzureAsrHelper(private val context: Context) {
speechConfig?.close() speechConfig?.close()
speechConfig = null speechConfig = null
// 关闭音频流(根据实际实现可能需要)
audioStream?.close()
audioStream = null
// 释放音频配置 // 释放音频配置
audioConfig?.close() audioConfig?.close()
@ -601,16 +601,19 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理 * 停止音频处理
*/ */
private fun stopAudioProcessing() { private fun stopAudioProcessing() {
// microphoneStream?.let { audioStream?.let {
// try { try {
// it.close() it.stopMicrophoneCapture()
// microphoneStream = null it.releaseAudioResources()
// } catch (e: Exception) {
// Log.e(tag, "关闭麦克风流失败: ${e.message}")
// e.printStackTrace()
// }
// }
it.pushAudioStream?.close()
audioStream = null
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
} // 关闭音频流(根据实际实现可能需要)
externalAudioStream?.let { externalAudioStream?.let {
try { try {
it.close() it.close()
@ -622,70 +625,22 @@ class AzureAsrHelper(private val context: Context) {
} }
} }
/**
* 开启录音
*/
fun enableRecord(filePath: String) {
Log.i(tag, "开启录音:")
recordfile!!.closeFile(true)
recordfile!!.creatingFiles(filePath)
}
/**
* 移动文件到新路径
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
Log.i(tag, "移动文件到新路径:")
recordfile!!.moveFile(sourcePath, destPath)
return true
}
/**
* 重命名指定路径的音频文件
*/
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
recordfile!!.renameFile(filePath, newName)
return true
}
/**
* 停止录音
*/
fun pauseRecord() {
Log.i(tag, "停止连续录音:")
recordfile!!.isPause = true /*
} 暂停音频输入
*/
fun audioPause() {
/**
* 关闭录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
}
fun stopAudioPause() {
Log.d(tag, "关闭麦克风stopAudioPause: ") Log.d(tag, "关闭麦克风stopAudioPause: ")
// 清理旧的识别器 // 清理旧的识别器
isPaused = true isPaused = true
} }
fun setAudiocontinue() { /*
继续音频输入
*/
fun audioContinue() {
Log.d(tag, "继续麦克风stopAudioPause: ") Log.d(tag, "继续麦克风stopAudioPause: ")
// 开始连续识别 // 开始连续识别
@ -727,403 +682,289 @@ class AzureAsrHelper(private val context: Context) {
} }
} }
//录音音频文件
/** /**
* 开始麦克风捕获 * 开启录音
*/ */
private fun initAudioRecord() { fun enableRecord(filePath: String) {
// 获取已确定的采样率 Log.i(tag, "开启录音:")
val format = getOptimalAudioFormat()
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入
val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
channelConfig,
audioFormat
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
// 计算最小缓冲区大小(避免出现音频欠载)
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT")
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else {
// 创建 AudioRecord 对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2 // 使用两倍缓冲区防止溢出
)
}
recordfile!!.closeFile(true)
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
recordfile!!.creatingFiles(filePath)
} }
// 3. 单独封装线程启动逻辑 /**
private fun startCaptureThread() { * 移动文件到新路径
captureThread = Thread { */
// 根据实际采样率计算缓冲区大小 fun moveFile(sourcePath: String, destPath: String): Boolean {
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) Log.i(tag, "移动文件到新路径:")
var bufferSize = 0
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
val buffer = ByteArray(bufferSize) recordfile!!.moveFile(sourcePath, destPath)
return true
}
while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
/**
* 重命名指定路径的音频文件
*/
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
if (!isPaused) {
// 读取音频
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
// 写入Azure流
audioStream?.write(buffer)
// 保存录音 recordfile!!.renameFile(filePath, newName)
recordfile?.saveAudioDataToWav(buffer) return true
}
}
}
}.apply { start() }
} }
/** /**
* 停止麦克风捕获并释放所有相关资源 * 停止录音
*/ */
private fun stopMicrophoneCapture() { fun pauseRecord() {
try { Log.i(tag, "停止连续录音:")
// 中断并等待捕获线程结束
captureThread?.interrupt()
captureThread?.join(300) // 最多等待300ms
// 停止录音并释放AudioRecord资源 recordfile!!.isPause = true
audioRecord?.stop() // 必须先停止再释放 }
} catch (e: Exception) { /**
Log.e("AudioConfig", "Error releasing resources: ${e.message}") * 关闭录音
} finally { */
// 确保恢复原始音频状态 fun stopRecord(isSave: Boolean) {
restoreOriginalAudioState() Log.i(tag, "关闭录音:")
}
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
} }
/** /**
* 应用音频效果 - 总是启用所有音频增强功能 * 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
*/ */
private fun applyAudioEffects() { inner class AudioStream {
val sessionId = audioRecord?.audioSessionId ?: return
try { private var echoCanceler: AcousticEchoCanceler? = null
// 设置音频模式为通信模式 private var noiseSuppressor: NoiseSuppressor? = null
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION private var automaticGainControl: AutomaticGainControl? = null
// 启用声学回音消除
if (AcousticEchoCanceler.isAvailable()) {
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true
Log.i(tag, "声学回音消除已启用")
} else {
Log.w(tag, "设备不支持声学回音消除")
}
// 启用噪声抑制 var audioRecord: AudioRecord? = null
if (NoiseSuppressor.isAvailable()) { private var captureThread: Thread? = null
noiseSuppressor = NoiseSuppressor.create(sessionId) var pushAudioStream: PushAudioInputStream? = null
noiseSuppressor?.enabled = true
Log.i(tag, "噪声抑制已启用")
// 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
/**
* 开始麦克风捕获
*/
fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
channelConfig,
audioFormat
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
// 计算最小缓冲区大小(避免出现音频欠载)
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT")
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else { } else {
Log.w(tag, "设备不支持噪声抑制") // 创建 AudioRecord 对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2 // 使用两倍缓冲区防止溢出
)
} }
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) { // 检查初始化状态
automaticGainControl = AutomaticGainControl.create(sessionId) if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
automaticGainControl?.enabled = true throw IllegalStateException("AudioRecord初始化失败")
Log.i(tag, "自动增益控制已启用")
} else {
Log.w(tag, "设备不支持自动增益控制")
} }
Log.i(tag, "回音消除和音频增强功能已全部启用") startCaptureThread() // 再启动数据读取线程
} catch (e: Exception) {
Log.e(tag, "设置音频效果失败: ${e.message}")
} }
}
/** // 3. 单独封装线程启动逻辑
* 释放音频资源 fun startCaptureThread() {
*/ try {
private fun releaseAudioResources() { // 确保之前的线程已停止
try { stopCaptureThread()
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { // 开始录音
audioRecord?.stop() audioRecord?.startRecording()
captureThread = Thread({
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO)
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
val buffer = ByteArray(bufferSize)
try {
while (!Thread.currentThread().isInterrupted &&
audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
if (!isPaused) {
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
// 安全地写入Azure流
try {
pushAudioStream?.write(buffer)
recordfile?.saveAudioDataToWav(buffer)
} catch (e: Exception) {
Log.w(tag, "写入音频流失败: ${e.message}")
}
} else if (bytesRead < 0) {
Log.w(tag, "AudioRecord读取错误: $bytesRead")
break
}
} else {
Thread.sleep(10) // 暂停时短暂休眠
}
}
} catch (e: InterruptedException) {
Log.d(tag, "音频捕获线程被中断")
} catch (e: Exception) {
Log.e(tag, "音频捕获异常: ${e.message}", e)
}
}, "AudioCaptureThread")
captureThread?.start()
} catch (e: Exception) {
Log.e(tag, "启动音频捕获线程失败: ${e.message}", e)
throw e
} }
}
// 释放回音消除器 private fun stopCaptureThread() {
echoCanceler?.let { captureThread?.let { thread ->
it.enabled = false if (thread.isAlive) {
it.release() thread.interrupt()
echoCanceler = null try {
thread.join(500) // 等待最多500ms
} catch (e: InterruptedException) {
Log.w(tag, "等待音频线程结束被中断")
}
}
} }
captureThread = null
}
/**
* 停止麦克风捕获并释放所有相关资源
*/
fun stopMicrophoneCapture() {
try {
if (captureThread != null) {
// 中断并等待捕获线程结束
captureThread?.interrupt()
captureThread?.join(300) // 最多等待300ms
// 停止录音并释放AudioRecord资源
audioRecord?.stop() // 必须先停止再释放
isPaused= true
}
// 释放噪声抑制器
noiseSuppressor?.let {
it.enabled = false
it.release()
noiseSuppressor = null
}
// 释放自动增益控制 } catch (e: Exception) {
automaticGainControl?.let { Log.e("AudioConfig", "Error releasing resources: ${e.message}")
it.enabled = false } finally {
it.release() // 确保恢复原始音频状态
automaticGainControl = null // restoreOriginalAudioState()
} }
}
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode /**
* 释放音频资源
*/
fun releaseAudioResources() {
try {
isPaused= true
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放录音实例 // // 释放回音消除器
audioRecord?.release() // echoCanceler?.let {
audioRecord = null // it.enabled = false
} catch (e: Exception) { // it.release()
Log.e(tag, "释放音频资源失败: ${e.message}") // echoCanceler = null
e.printStackTrace() // }
// // 释放噪声抑制器
// noiseSuppressor?.let {
// it.enabled = false
// it.release()
// noiseSuppressor = null
// }
// // 释放自动增益控制
// automaticGainControl?.let {
// it.enabled = false
// it.release()
// automaticGainControl = null
// }
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
}
} }
}
// /** }
// * 麦克风流 - 拉流模式
// * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
// */
// private inner class MicrophoneStream : PullAudioInputStreamCallback() {
// private var audioRecord: AudioRecord? = null
// private var echoCanceler: AcousticEchoCanceler? = null
// private var noiseSuppressor: NoiseSuppressor? = null
// private var automaticGainControl: AutomaticGainControl? = null
// // 音频配置
// private val sampleRate = 16000
// private val channelConfig = AudioFormat.CHANNEL_IN_MONO
// private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// // 优化:减少缓冲区大小以降低延迟
// private val bufferSize = AudioRecord.getMinBufferSize(
// sampleRate, channelConfig, audioFormat
// ).let { if (it < 0) 2048 else it }
// init {
// initMicrophone()
// }
// /**
// * 初始化麦克风
// */
// private fun initMicrophone() {
// try {
// // 创建录音对象
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// val format = AudioFormat.Builder()
// .setSampleRate(sampleRate)
// .setEncoding(audioFormat)
// .setChannelMask(channelConfig)
// .build()
// audioRecord = AudioRecord.Builder()
// .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
// .setAudioFormat(format)
// .setBufferSizeInBytes(bufferSize)
// .build()
// } else {
// audioRecord = AudioRecord(
// MediaRecorder.AudioSource.VOICE_COMMUNICATION,
// sampleRate,
// channelConfig,
// audioFormat,
// bufferSize
// )
// }
// // 检查初始化状态
// if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
// throw IllegalStateException("AudioRecord初始化失败")
// }
// // 应用音频效果
// applyAudioEffects()
// // 开始录音
// audioRecord?.startRecording()
// // 检查录音状态
// if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
// throw IllegalStateException("录音启动失败")
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// /**
// * 应用音频效果 - 总是启用所有音频增强功能
// */
// private fun applyAudioEffects() {
// val sessionId = audioRecord?.audioSessionId ?: return
// try {
// // 设置音频模式为通信模式
// this@AzureAsrHelper.audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
// // 启用声学回音消除
// if (AcousticEchoCanceler.isAvailable()) {
// echoCanceler = AcousticEchoCanceler.create(sessionId)
// echoCanceler?.enabled = true
// Log.i(tag, "声学回音消除已启用")
// } else {
// Log.w(tag, "设备不支持声学回音消除")
// }
// // 启用噪声抑制
// if (NoiseSuppressor.isAvailable()) {
// noiseSuppressor = NoiseSuppressor.create(sessionId)
// noiseSuppressor?.enabled = true
// Log.i(tag, "噪声抑制已启用")
// } else {
// Log.w(tag, "设备不支持噪声抑制")
// }
// // 启用自动增益控制
// if (AutomaticGainControl.isAvailable()) {
// automaticGainControl = AutomaticGainControl.create(sessionId)
// automaticGainControl?.enabled = true
// Log.i(tag, "自动增益控制已启用")
// } else {
// Log.w(tag, "设备不支持自动增益控制")
// }
// Log.i(tag, "回音消除和音频增强功能已全部启用")
// } catch (e: Exception) {
// Log.e(tag, "设置音频效果失败: ${e.message}")
// }
// }
// /**
// * 获取音频格式
// */
// fun getFormat(): AudioStreamFormat =
// AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
// /**
// * Azure SDK 调用此方法获取音频数据
// * 这是拉流模式的核心方法,由SDK调用以获取音频数据
// */
// /**
// * 优化:简化音频数据读取(移除文件保存)
// */
// override fun read(buffer: ByteArray): Int {
// try {
// val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
// if (result < 0) {
// Log.e(tag, "读取音频数据失败: $result")
// return 0
// }
// recordfile?.saveAudioDataToWav(buffer)
// return result
// } catch (e: Exception) {
// Log.e(tag, "读取音频异常: ${e.message}")
// return 0
// }
// }
// /**
// * 优化:简化资源关闭
// */
// override fun close() {
// releaseAudioResources()
// }
// /**
// * 释放音频资源
// */
// private fun releaseAudioResources() {
// try {
// // 停止录音
// if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
// audioRecord?.stop()
// }
// // 释放回音消除器
// echoCanceler?.let {
// it.enabled = false
// it.release()
// echoCanceler = null
// }
// // 释放噪声抑制器
// noiseSuppressor?.let {
// it.enabled = false
// it.release()
// noiseSuppressor = null
// }
// // 释放自动增益控制
// automaticGainControl?.let {
// it.enabled = false
// it.release()
// automaticGainControl = null
// }
// // 恢复音频模式
// this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// // 释放录音实例
// audioRecord?.release()
// audioRecord = null
// } catch (e: Exception) {
// Log.e(tag, "释放音频资源失败: ${e.message}")
// e.printStackTrace()
// }
// }
// }
/** /**
* 外部音频拉流 * 外部音频拉流
@ -1247,4 +1088,4 @@ class AzureAsrHelper(private val context: Context) {
} }
} }

6
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -256,13 +256,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
"setPause" -> { "setPause" -> {
azureAsrHelper.stopAudioPause(); azureAsrHelper.audioPause();
return return
} }
"setcontinue" -> { "setcontinue" -> {
// azureAsrHelper.stopAudioPause(); azureAsrHelper.audioContinue();
azureAsrHelper.setAudiocontinue();
return return
} }
@ -272,7 +271,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
"restoreOriginalAudioState" -> { "restoreOriginalAudioState" -> {
// azureAsrHelper.stopAudioPause();
azureAsrHelper.restoreOriginalAudioState(); azureAsrHelper.restoreOriginalAudioState();
return return
} }

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

@ -578,15 +578,14 @@ object BleService {
// 根据特征UUID区分处理 // 根据特征UUID区分处理
when (c.uuid) { when (c.uuid) {
// 音频特征数据 // 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> { // BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1") // Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
processAudioData(data) // processAudioData(data)
} // }
// 音频特征数据 // 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> { BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID")
processAudioData(data) processAudioData(data)
} }
// 通知特征数据(命令和控制) // 通知特征数据(命令和控制)

16
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -413,29 +413,25 @@ private fun updateProgress() {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
} }
) )
val builder = NotificationCompat.Builder(this, CHANNEL_ID) val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title) .setContentTitle(title)
.setContentText(artist) .setContentText(artist)
.setSmallIcon(android.R.drawable.ic_media_play) .setSmallIcon(R.drawable.ic_play)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle( .setStyle(
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken) .setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2) .setShowActionsInCompactView(0, 1, 2)
) )
.addAction(android.R.drawable.ic_media_previous, "上一首", prevIntent) .addAction(R.drawable.ic_prev, "上一首", prevIntent)
.addAction( .addAction(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play, if (currentStatus) R.drawable.ic_pause else R.drawable.ic_play,
if (currentStatus) "暂停" else "播放", if (currentStatus) "暂停" else "播放",
playPauseIntent playPauseIntent
) )
.addAction(android.R.drawable.ic_media_next, "下一首", nextIntent) .addAction(R.drawable.ic_next, "下一首", nextIntent)
// 添加删除通知按钮 .setOngoing(false)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "删除", deletePendingIntent)
.setOngoing(currentStatus)
.setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为 .setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为
// 异步加载封面图 // 异步加载封面图
if (!coverUrl.isNullOrEmpty()) { if (!coverUrl.isNullOrEmpty()) {
Thread { Thread {
@ -644,6 +640,8 @@ private fun updateProgress() {
if (player.isPlaying || currentStatus) { if (player.isPlaying || currentStatus) {
player.seekTo(positionMs) player.seekTo(positionMs)
Log.i(TAG, "跳转到位置: ${positionMs}ms") Log.i(TAG, "跳转到位置: ${positionMs}ms")
} else {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "跳转失败: ${e.message}") Log.e(TAG, "跳转失败: ${e.message}")

9
local_plugins/music_service/android/src/main/res/drawable/ic_next.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,18l8.5,-6L6,6v12z M16,6v12h2V6h-2z" />
</vector>

9
local_plugins/music_service/android/src/main/res/drawable/ic_pause.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,19h4V5H6v14zM14,5v14h4V5h-4z" />
</vector>

9
local_plugins/music_service/android/src/main/res/drawable/ic_play.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8,5v14l11,-7z" />
</vector>

9
local_plugins/music_service/android/src/main/res/drawable/ic_prev.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,6l-8.5,6L18,18V6z M6,6v12h2V6H6z" />
</vector>
Loading…
Cancel
Save