Browse Source

优化翻译频繁开启关闭卡死问题,新加输入输出端来回切换,优化面对面翻译,修复录音界面bug

weicu
fdp 1 year ago
parent
commit
bc5c7bdeeb
  1. 7
      lib/data/services/asr_service.dart
  2. 50
      lib/data/services/speech_impl/azure_asr_service.dart
  3. 24
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  4. 24
      lib/data/services/speech_impl/volcano_asr_service.dart
  5. 24
      lib/data/services/speech_impl/xunfei_asr_service.dart
  6. 105
      lib/modules/home/views/home_view.dart
  7. 24
      lib/modules/meeting/controllers/meeting_record_controller.dart
  8. 44
      lib/modules/settings/views/settings_view.dart
  9. 109
      lib/modules/translation/controllers/translation_controller.dart
  10. 231
      lib/modules/translation/views/translation_view.dart
  11. 618
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  12. 46
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  13. 305
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

7
lib/data/services/asr_service.dart

@ -26,6 +26,13 @@ abstract class AsrService {
/// 停止连续语音识别 /// 停止连续语音识别
Future<bool> stopContinuousRecognition(); Future<bool> stopContinuousRecognition();
Future<void> setPause();
Future<void> setcontinue();
Future<void> restoreOriginalAudioState();
Future<void> disableBluetoothAudio();
/// 检查连续识别是否活跃 /// 检查连续识别是否活跃
bool isContinuousRecognitionActive(); bool isContinuousRecognitionActive();

50
lib/data/services/speech_impl/azure_asr_service.dart

@ -414,4 +414,54 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override
Future<void> setPause() async {
// TODO: implement setPause
try {
final bool result = await _channel.invokeMethod('setPause');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> setcontinue() async {
// TODO: implement setcontinue
try {
final bool result = await _channel.invokeMethod('setcontinue');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> disableBluetoothAudio() async {
// TODO: implement disableBluetoothAudio
try {
final bool result = await _channel.invokeMethod('disableBluetoothAudio');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> restoreOriginalAudioState() async {
// TODO: implement restoreOriginalAudioState
try {
final bool result =
await _channel.invokeMethod('restoreOriginalAudioState');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
} }

24
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -841,4 +841,28 @@ class VolcanoAsrApiService implements AsrService {
// TODO: implement renameFile // TODO: implement renameFile
throw UnimplementedError(); throw UnimplementedError();
} }
@override
Future<void> setPause() {
// TODO: implement setPause
throw UnimplementedError();
}
@override
Future<void> setcontinue() {
// TODO: implement setcontinue
throw UnimplementedError();
}
@override
Future<void> disableBluetoothAudio() {
// TODO: implement disableBluetoothAudio
throw UnimplementedError();
}
@override
Future<void> restoreOriginalAudioState() {
// TODO: implement restoreOriginalAudioState
throw UnimplementedError();
}
} }

24
lib/data/services/speech_impl/volcano_asr_service.dart

@ -376,4 +376,28 @@ class VolcanoAsrService extends GetxService implements AsrService {
// TODO: implement renameFile // TODO: implement renameFile
throw UnimplementedError(); throw UnimplementedError();
} }
@override
Future<void> setPause() {
// TODO: implement setPause
throw UnimplementedError();
}
@override
Future<void> setcontinue() {
// TODO: implement setcontinue
throw UnimplementedError();
}
@override
Future<void> disableBluetoothAudio() {
// TODO: implement disableBluetoothAudio
throw UnimplementedError();
}
@override
Future<void> restoreOriginalAudioState() {
// TODO: implement restoreOriginalAudioState
throw UnimplementedError();
}
} }

24
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -300,4 +300,28 @@ class XunfeiAsrService extends GetxService implements AsrService {
// TODO: implement renameFile // TODO: implement renameFile
throw UnimplementedError(); throw UnimplementedError();
} }
@override
Future<void> setPause() {
// TODO: implement setPause
throw UnimplementedError();
}
@override
Future<void> setcontinue() {
// TODO: implement setcontinue
throw UnimplementedError();
}
@override
Future<void> disableBluetoothAudio() {
// TODO: implement disableBluetoothAudio
throw UnimplementedError();
}
@override
Future<void> restoreOriginalAudioState() {
// TODO: implement restoreOriginalAudioState
throw UnimplementedError();
}
} }

105
lib/modules/home/views/home_view.dart

@ -209,44 +209,44 @@ class HomeView extends GetView<HomeController> {
return Container( return Container(
key: ValueKey('device_status_$isConnected'), key: ValueKey('device_status_$isConnected'),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1), color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
], ],
), ),
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
child: Column( child: Column(
children: [ children: [
// 状态标题行 // 状态标题行
_buildDeviceStatusHeader(isDarkMode), _buildDeviceStatusHeader(isDarkMode),
// 设备信息 // 设备信息
SizedBox(height: 12.h), SizedBox(height: 12.h),
Obx(() { Obx(() {
// 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断
// 使用响应式访问确保UI更新 // 使用响应式访问确保UI更新
return controller.isFullyConnectedRx.value return controller.isFullyConnectedRx.value
? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息
: _buildDisconnectedInfo(isDarkMode); //未连接提示 : _buildDisconnectedInfo(isDarkMode); //未连接提示
}), }),
// 连接按钮(仅在未连接时显示) // 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h), SizedBox(height: 12.h),
Obx(() { Obx(() {
// 使用响应式访问确保UI更新 // 使用响应式访问确保UI更新
if (!controller.isFullyConnectedRx.value) { if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton(); return _buildConnectButton();
} else { } else {
return const SizedBox(); return const SizedBox();
} }
}), }),
], ],
), ),
); );
}); });
} }
@ -328,7 +328,6 @@ class HomeView extends GetView<HomeController> {
// 连接错误部分 // 连接错误部分
// 已连接设备信息(新增设备名称 + 信号强度 + 电池信息) // 已连接设备信息(新增设备名称 + 信号强度 + 电池信息)
Widget _buildConnectedDeviceInfo(bool isDarkMode) { Widget _buildConnectedDeviceInfo(bool isDarkMode) {
return Column( return Column(
@ -417,20 +416,20 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// 分隔线 // // 分隔线
Container( // Container(
height: 30.h, // height: 30.h,
width: 1, // width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300], // color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
), // ),
// 充电盒 // // 充电盒
_buildCompactBatteryItem( // _buildCompactBatteryItem(
label: '充电盒', // label: '充电盒',
batteryLevel: caseBattery.value, // batteryLevel: caseBattery.value,
isCharging: caseCharging.value, // isCharging: caseCharging.value,
isDarkMode: isDarkMode, // isDarkMode: isDarkMode,
), // ),
], ],
), ),
); );

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

@ -36,7 +36,8 @@ class MeetingRecordController extends GetxController
final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call
final RxBool isMarked = false.obs; final RxBool isMarked = false.obs;
final RxBool showTextContent = false.obs; final RxBool showTextContent = false.obs;
final RxBool isVoiceRecognitionActive = false.obs; final RxBool isRecording = false.obs;
//final RxBool isVoiceRecognitionActive = false.obs;
final RxList<double> waveData = <double>[].obs; final RxList<double> waveData = <double>[].obs;
// Transcription content // Transcription content
@ -133,9 +134,12 @@ class MeetingRecordController extends GetxController
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
titleEditingController.dispose(); titleEditingController.dispose();
titleFocusNode.dispose(); titleFocusNode.dispose();
_bleManager.closeCodec(); if (audioType.value != 0) {
_asrService.stopContinuousRecognition(); _bleManager.closeCodec();
}
_asrService.stopRecord(true); _asrService.stopRecord(true);
_asrService.stopContinuousRecognition();
isRecording.value = false;
} }
/// Handles focus changes in title text field /// Handles focus changes in title text field
@ -222,7 +226,7 @@ class MeetingRecordController extends GetxController
// Start audio recording // Start audio recording
await _asrService.enableRecord("${dir.path}/$fullFileName.wav"); await _asrService.enableRecord("${dir.path}/$fullFileName.wav");
isRecording.value = true;
// Update state // Update state
fileName.value = fullFileName; fileName.value = fullFileName;
newName.value = fullFileName; newName.value = fullFileName;
@ -327,7 +331,10 @@ class MeetingRecordController extends GetxController
/// Saves recording and cleans up resources /// Saves recording and cleans up resources
Future<void> saveRecording() async { Future<void> saveRecording() async {
resetTimer(); resetTimer();
_bleManager.closeCodec(); if (audioType.value != 0) {
_bleManager.closeCodec();
}
if (!isRecording.value) return;
await _asrService.stopRecord(true); await _asrService.stopRecord(true);
// Rename file if title was edited // Rename file if title was edited
@ -339,12 +346,12 @@ class MeetingRecordController extends GetxController
final Newdir = Directory("${appDir.path}/DestAudio/"); final Newdir = Directory("${appDir.path}/DestAudio/");
if (!await Newdir.exists()) await Newdir.create(recursive: true); if (!await Newdir.exists()) await Newdir.create(recursive: true);
await _asrService.moveFile( await _asrService.moveFile(
"${dir.path}/${fileName.value}.wav", "${dir.path}${fileName.value}.wav",
"${Newdir.path}/${fileName.value}.wav", "${Newdir.path}${fileName.value}.wav",
); );
} else { } else {
await _asrService.renameFile( await _asrService.renameFile(
"${dir.path}/${fileName.value}.wav", "${dir.path}${fileName.value}.wav",
newName.value, newName.value,
); );
} }
@ -352,6 +359,7 @@ class MeetingRecordController extends GetxController
await _asrService.stopContinuousRecognition(); await _asrService.stopContinuousRecognition();
_resetToDefaultState(); _resetToDefaultState();
isRecording.value = false;
} }
/// Resets controller to initial state /// Resets controller to initial state

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

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

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

@ -1,3 +1,4 @@
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:deep_voice/core/utils/permission_util.dart'; import 'package:deep_voice/core/utils/permission_util.dart';
@ -37,7 +38,7 @@ class TranslationController extends GetxController {
// 音频输入源 // 音频输入源
bool _audioSourceType = false; bool _audioSourceType = false;
// 倒计时器 // 倒计时器
bool istts = false;
final RxString recordDurationText = '00:00'.obs; final RxString recordDurationText = '00:00'.obs;
Timer? _recordTimer; Timer? _recordTimer;
int _seconds = 0; int _seconds = 0;
@ -47,6 +48,8 @@ class TranslationController extends GetxController {
static const String _targetLanguageKey = 'translation_target_language'; static const String _targetLanguageKey = 'translation_target_language';
static const int _maxHistoryItems = 10; static const int _maxHistoryItems = 10;
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
RxInt lastActiveSpeaker = 0.obs; // 上一次状态
// 状态变量 // 状态变量
final isRecognizing = false.obs; final isRecognizing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
@ -62,12 +65,15 @@ class TranslationController extends GetxController {
final currentModeTitle = 'simultaneousTranslation'.obs; final currentModeTitle = 'simultaneousTranslation'.obs;
final currentMode = 'simultaneous'.obs; final currentMode = 'simultaneous'.obs;
// 翻译历史和当前项 // 翻译历史和当前项
String? fTFTranslationResult = '';
final translationHistory = <TranslationItem>[].obs; final translationHistory = <TranslationItem>[].obs;
String? currentSessionId; String? currentSessionId;
final appDocDir = getApplicationDocumentsDirectory(); final appDocDir = getApplicationDocumentsDirectory();
var appDir; var appDir;
var dir; var dir;
//int num = 0;
Future<void> changeTranslationMode(String mode) async { Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode; currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode); currentModeTitle.value = _getModeTitle(mode);
@ -89,6 +95,29 @@ class TranslationController extends GetxController {
} }
} }
Future<void> setActiveSpeaker(int id) async {
activeSpeaker.value = id;
// 监听识别结果
if (activeSpeaker.value != 0) {
istts = false;
_asrService.setcontinue();
lastActiveSpeaker.value = id;
} else {
_asrService.setPause();
if (fTFTranslationResult != '' && !istts) {
istts = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
await playTranslatedText(fTFTranslationResult!);
fTFTranslationResult = '';
}
}
}
// 切换TTS功能
void disableBluetoothAudio() => _asrService.disableBluetoothAudio();
// 切换TTS功能
void restoreOriginalAudioState() => _asrService.restoreOriginalAudioState();
// 兼容旧版视图的计算属性 // 兼容旧版视图的计算属性
Rx<TranslationItem?> get currentItem { Rx<TranslationItem?> get currentItem {
if (translationHistory.isNotEmpty && if (translationHistory.isNotEmpty &&
@ -210,19 +239,6 @@ class TranslationController extends GetxController {
return false; return false;
} }
} }
// /// 设置事件监听器
// void _setupEventListeners() {
// // 监听扫描结果
// _scanSubscription = jieli.scanResults.listen((devices) {
// scannedDevices.assignAll(devices);
// });
// // 监听唤醒信号
// _wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
// wakeupReceived.value = true;
// Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
// });
// }
// 初始化服务 // 初始化服务
Future<void> _initServices() async { Future<void> _initServices() async {
@ -340,21 +356,6 @@ class TranslationController extends GetxController {
} }
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// // 初始化新的翻译项
// final newItem = TranslationItem(
// sourceText: '',
// translatedText: '',
// sourceLanguageCode: sourceLanguageCode.value,
// targetLanguageCode: targetLanguageCode.value,
// timestamp: DateTime.now(),
// sessionId: currentSessionId!,
// isFirstInSession: true,
// isIntermediate: true,
// );
// // 添加到历史记录
// translationHistory.add(newItem);
// 开始连续语音识别 // 开始连续语音识别
if (currentMode.value == "audioVideo") { if (currentMode.value == "audioVideo") {
bleManager.openDecoder(); bleManager.openDecoder();
@ -375,6 +376,7 @@ class TranslationController extends GetxController {
} }
final recognitionStream = final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
_asrService.setcontinue();
isRecognizing.value = true; isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) { if (isRecording.value && isRecognizing.value) {
startRecording(); startRecording();
@ -461,8 +463,6 @@ class TranslationController extends GetxController {
translationHistory.last.sessionId != currentSessionId, translationHistory.last.sessionId != currentSessionId,
isIntermediate: true, isIntermediate: true,
); );
translationHistory.add(newItem);
// 添加新项后滚动到底部 // 添加新项后滚动到底部
_scrollToBottom(); _scrollToBottom();
} else { } else {
@ -562,32 +562,29 @@ class TranslationController extends GetxController {
detectedSourceLanguageCode; detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode = translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode; detectedTargetLanguageCode;
print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}"); translationHistory.refresh();
// 判断是否是面对面模式且语言一致 saveTranslationHistory();
final bool isFaceToFaceSameLang = // 更新后滚动到底部
detectedSourceLanguageCode == sourceLanguageCode.value && _scrollToBottom();
currentMode.value == "faceToFace"; if (isFinal && currentMode.value == 'faceToFace') {
print("音频输出文本2$translationResult");
// 设置音频输出设备 fTFTranslationResult =
var isSuccess = await _ttsService.setAudioOutputDevice( (fTFTranslationResult ?? '') + translationResult;
isFaceToFaceSameLang
? AudioOutputDevice.speaker.index if (activeSpeaker.value == 0 && !istts) {
: AudioOutputDevice.headphones.index, istts = true;
); await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
if (isSuccess) { await playTranslatedText(fTFTranslationResult!);
print('设置音频输出设备成功'); fTFTranslationResult = '';
translationHistory.refresh(); ;
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) {
// Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult);
} }
} else { }
print('设置音频输出设备失败'); // 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value &&
isFinal &&
currentMode.value != 'faceToFace') {
_ttsService.setAudioOutputDevice(activeSpeaker.value);
await playTranslatedText(translationResult);
} }
} else { } else {
Logger.info( Logger.info(

231
lib/modules/translation/views/translation_view.dart

@ -399,38 +399,209 @@ class TranslationView extends GetView<TranslationController> {
), ),
], ],
), ),
child: Row( child: Obx(() {
mainAxisAlignment: MainAxisAlignment.center, // 同时模式显示双按钮
children: [ if (controller.currentMode.value == 'faceToFace') {
// 主要控制按钮 - 开始/暂停语音识别 controller.startRecognition();
Obx(() {
final isRecognizing = controller.isRecognizing.value; return Row(
return GestureDetector( mainAxisAlignment: MainAxisAlignment.center,
onTap: () { children: [
if (isRecognizing) { // 耳机图标按钮 (说话者1)
controller.stopAll(); _buildHoldToTalkButton(
} else { isDarkMode: isDarkMode,
controller.startRecognition(); speakerId: 1,
} activeSpeaker: controller.activeSpeaker.value,
}, icon: Icons.headset, // 耳机图标
child: Container( label: "耳机用户",
width: 60.w, ),
height: 60.w, SizedBox(width: 40.w),
decoration: BoxDecoration( // 手机图标按钮 (说话者2)
color: isRecognizing _buildHoldToTalkButton(
? (isDarkMode ? Colors.red[700] : Colors.red) isDarkMode: isDarkMode,
: (isDarkMode ? AppColors.primary : Colors.blue), speakerId: 2,
shape: BoxShape.circle, activeSpeaker: controller.activeSpeaker.value,
), icon: Icons.phone_android, // 手机图标
child: Icon( label: "手机用户",
isRecognizing ? Icons.pause : Icons.play_arrow, ),
color: Colors.white, ],
size: 30.sp, );
}
// 普通模式显示单按钮
else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopAll();
} else {
controller.startRecognition();
}
},
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isRecognizing
? (isDarkMode ? Colors.red[700] : Colors.red)
: (isDarkMode ? AppColors.primary : Colors.blue),
shape: BoxShape.circle,
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
);
}),
],
);
}
}),
);
}
// // 按住说话按钮组件
// Widget _buildHoldToTalkButton({
// required bool isDarkMode,
// required int speakerId,
// required int activeSpeaker,
// required IconData icon,
// required String label,
// }) {
// bool isActive = activeSpeaker == speakerId;
// return Listener(
// onPointerDown: (_) {
// if (activeSpeaker == 0) {
// // 没有激活的按钮时才能激活
// if (speakerId == 1) {
// //耳机收音
// controller.restoreOriginalAudioState();
// //controller.setActiveSpeaker(speakerId);
// } else {
// controller.disableBluetoothAudio();
// }
// controller.setActiveSpeaker(speakerId);
// //controller.startRecognition();
// }
// },
// onPointerUp: (_) {
// if (isActive) {
// //controller.stopRecognition();
// controller.setActiveSpeaker(0);
// }
// },
// child: Container(
// width: 60.w,
// height: 60.w,
// decoration: BoxDecoration(
// color: isActive
// ? (isDarkMode ? Colors.green[700] : Colors.green) // 激活状态颜色
// : (isDarkMode ? AppColors.primary : Colors.blue),
// shape: BoxShape.circle,
// ),
// child: Icon(
// Icons.mic, // 麦克风图标
// color: isActive ? Colors.white : Colors.white.withOpacity(0.7),
// size: 30.sp,
// ),
// ),
// );
// }
// 按住说话按钮组件
Widget _buildHoldToTalkButton({
required bool isDarkMode,
required int speakerId,
required int activeSpeaker,
required IconData icon,
required String label,
}) {
bool isActive = activeSpeaker == speakerId;
bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId;
return Listener(
onPointerDown: (_) {
if (activeSpeaker == 0) {
// 没有激活的按钮时才能激活
if (speakerId == 1) {
// 耳机收音 - 恢复原始音频状态
controller.restoreOriginalAudioState();
} else {
// 手机收音 - 禁用蓝牙音频
controller.disableBluetoothAudio();
}
controller.setActiveSpeaker(speakerId);
}
},
onPointerUp: (_) {
if (isActive) {
controller.setActiveSpeaker(0);
}
},
onPointerCancel: (_) {
if (isActive) {
controller.setActiveSpeaker(0);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: isActive
? (isDarkMode ? Colors.green[700] : Colors.green)
: (isDisabled
? (isDarkMode ? Colors.grey[800] : Colors.grey[300])
: (isDarkMode ? AppColors.primary : Colors.blue)),
shape: BoxShape.circle,
boxShadow: isActive
? [
BoxShadow(
color: Colors.green.withOpacity(0.5),
blurRadius: 10,
spreadRadius: 3,
)
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
// 设备类型图标(叠加在麦克风上)
Icon(
icon,
color: isActive
? Colors.white
: Colors.white.withOpacity(isDisabled ? 0.3 : 0.9),
size: 24.sp,
), ),
],
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive
? Colors.white
: Colors.white.withOpacity(isDisabled ? 0.3 : 0.9),
), ),
); ),
}), ],
], ),
), ),
); );
} }

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

@ -1,23 +1,17 @@
package com.yunqiinnovation.azure_speech package com.yunqiinnovation.azure_speech
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.AudioFormat import android.media.AudioFormat
import android.media.AudioRecord import android.media.AudioRecord
import android.media.MediaRecorder import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl import android.media.audiofx.AutomaticGainControl
import android.os.Process
import android.util.Log import android.util.Log
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
import kotlinx.coroutines.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext
import android.media.AudioManager import android.media.AudioManager
import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat
@ -25,6 +19,9 @@ import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.yunqiinnovation.azure_speech.tools.RecordFile import com.yunqiinnovation.azure_speech.tools.RecordFile
import com.microsoft.cognitiveservices.speech.audio.AudioConfig
import com.microsoft.cognitiveservices.speech.audio.AudioInputStream
import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream
/** /**
@ -37,6 +34,9 @@ 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 +55,7 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理 // 音频处理
private var microphoneStream: MicrophoneStream? = null // private var microphoneStream: MicrophoneStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null private var externalAudioStream: ExternalAudioPullStream? = null
// 音频模式管理 // 音频模式管理
@ -65,6 +65,10 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件处理 // 录音文件处理
var recordfile: RecordFile? = null var recordfile: RecordFile? = null
private var isPaused = false
private var audioRecord: AudioRecord? = null
private var captureThread: Thread? = null
private var audioStream: PushAudioInputStream? = null
/** /**
* 音频来源类型 * 音频来源类型
@ -101,11 +105,16 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 1. 创建可控制的音频流
val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1)
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
// 5. 初始化麦克风数据
initAudioRecord()
// 保存配置 // 保存配置
this.subscriptionKey = subscriptionKey this.subscriptionKey = subscriptionKey
this.region = region this.region = region
@ -206,14 +215,17 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun setupMicrophoneStream() { private fun setupMicrophoneStream() {
try { try {
// 创建麦克风流 // 创建麦克风流
microphoneStream = MicrophoneStream() // microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream) // audioConfig = AudioConfig.fromStreamInput(microphoneStream)
audioConfig = AudioConfig.fromStreamInput(audioStream)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}") Log.e(tag, "设置麦克风流失败: ${e.message}")
microphoneStream = null //microphoneStream = null
} }
} }
@ -349,10 +361,13 @@ class AzureAsrHelper(private val context: Context) {
setupEventListeners(callback) setupEventListeners(callback)
// 启动音频处理 // 启动音频处理
startAudioProcessing() //startAudioProcessing()
// 开始连续识别 // 开始连续识别
recognizer?.startContinuousRecognitionAsync() recognizer?.startContinuousRecognitionAsync()
// 开始录音
audioRecord?.startRecording()
startCaptureThread() // 再启动数据读取线程
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
return true return true
@ -458,7 +473,7 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音 // 停止录音
stopMicrophoneCapture()
// 会话结束事件会设置isContinuousRecognitionActive = false // 会话结束事件会设置isContinuousRecognitionActive = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -497,6 +512,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
} }
releaseAudioResources()
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
@ -510,13 +526,17 @@ class AzureAsrHelper(private val context: Context) {
speechConfig?.close() speechConfig?.close()
speechConfig = null speechConfig = null
// 关闭音频流(根据实际实现可能需要)
audioStream?.close()
audioStream = null
// 释放音频配置 // 释放音频配置
audioConfig?.close() audioConfig?.close()
audioConfig = null audioConfig = null
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
microphoneStream = null //microphoneStream = null
externalAudioStream = null externalAudioStream = null
audioConfig = null audioConfig = null
recognizer = null recognizer = null
@ -525,7 +545,7 @@ class AzureAsrHelper(private val context: Context) {
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
microphoneStream = null //microphoneStream = null
externalAudioStream = null externalAudioStream = null
audioConfig = null audioConfig = null
recognizer = null recognizer = null
@ -555,15 +575,15 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理 * 停止音频处理
*/ */
private fun stopAudioProcessing() { private fun stopAudioProcessing() {
microphoneStream?.let { // microphoneStream?.let {
try { // try {
it.close() // it.close()
microphoneStream = null // microphoneStream = null
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}") // Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace() // e.printStackTrace()
} // }
} // }
externalAudioStream?.let { externalAudioStream?.let {
try { try {
@ -631,200 +651,418 @@ class AzureAsrHelper(private val context: Context) {
recordfile!!.closeFile(isSave) recordfile!!.closeFile(isSave)
} }
fun stopAudioPause() {
Log.d(tag, "关闭麦克风stopAudioPause: ")
// 清理旧的识别器
isPaused = true
}
fun setAudiocontinue() {
Log.d(tag, "继续麦克风stopAudioPause: ")
// 开始连续识别
isPaused = false
}
/** /**
* 麦克风流 - 拉流模式 * 禁用蓝牙音频功能,切换回正常音频模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
*/ */
private inner class MicrophoneStream : PullAudioInputStreamCallback() { fun disableBluetoothAudio() {
private var audioRecord: AudioRecord? = null try {
private var echoCanceler: AcousticEchoCanceler? = null // 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由
private var noiseSuppressor: NoiseSuppressor? = null audioManager?.isBluetoothScoOn = false
private var automaticGainControl: AutomaticGainControl? = null // 停止蓝牙SCO连接
audioManager?.stopBluetoothSco()
// 音频配置 // 切换回正常音频模式(非通话模式)
private val sampleRate = 16000 audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT } catch (e: Exception) {
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}")
// 优化:减少缓冲区大小以降低延迟
private val bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat
).let { if (it < 0) 2048 else it }
init {
initMicrophone()
} }
}
/** /**
* 初始化麦克风 * 恢复原始音频设备状态(通常是重新启用蓝牙)
*/ */
private fun initMicrophone() { fun restoreOriginalAudioState() {
try { try {
// 创建录音对象 // 恢复应用启动时的原始音频模式
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { audioManager?.mode = AudioManager.MODE_NORMAL
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
)
}
// 检查初始化状态 // 重新启用蓝牙SCO
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { audioManager?.isBluetoothScoOn = true
throw IllegalStateException("AudioRecord初始化失败") // 启动蓝牙SCO连接(通常在需要蓝牙通话时使用)
} audioManager?.startBluetoothSco()
// 应用音频效果 } catch (e: Exception) {
applyAudioEffects() Log.e("AudioConfig", "Failed to restore audio state: ${e.message}")
}
}
// 开始录音 /**
audioRecord?.startRecording() * 开始麦克风捕获
*/
private fun initAudioRecord() {
// 设置音频参数
val sampleRate = 16000 // 16kHz采样率(语音识别常用)
val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入
val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式
// 计算最小缓冲区大小(避免出现音频欠载)
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// 检查录音状态 // 创建 AudioRecord 对象
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { audioRecord = AudioRecord(
throw IllegalStateException("录音启动失败") MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
} sampleRate,
} catch (e: Exception) { channelConfig,
e.printStackTrace() audioFormat,
} minBufferSize * 2 // 使用两倍缓冲区防止溢出
} )
/**
* 应用音频效果 - 总是启用所有音频增强功能
*/
private fun applyAudioEffects() {
val sessionId = audioRecord?.audioSessionId ?: return
try { // 应用音频效果
// 设置音频模式为通信模式 applyAudioEffects()
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, "设备不支持声学回音消除")
}
// 启用噪声抑制 // 3. 单独封装线程启动逻辑
if (NoiseSuppressor.isAvailable()) { private fun startCaptureThread() {
noiseSuppressor = NoiseSuppressor.create(sessionId) captureThread = Thread {
noiseSuppressor?.enabled = true // 循环条件确保录音已开始
Log.i(tag, "噪声抑制已启用")
} else { val buffer = ByteArray(1024) // 4KB数据缓冲区
Log.w(tag, "设备不支持噪声抑制") while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
// 从麦克风读取数据
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
// 仅在未暂停时处理数据
if (!isPaused && bytesRead > 0) {
// 写入音频流(可能是网络传输或本地处理)
audioStream?.write(buffer)
// 保存到WAV文件(如果启用了录制功能)
recordfile?.saveAudioDataToWav(buffer)
} }
// 启用自动增益控制 // 暂停时短暂休眠以减少CPU占用
if (AutomaticGainControl.isAvailable()) { if (isPaused) {
automaticGainControl = AutomaticGainControl.create(sessionId) try {
automaticGainControl?.enabled = true Thread.sleep(50)
Log.i(tag, "自动增益控制已启用") } catch (e: InterruptedException) {
} else { break // 线程被中断时退出循环
Log.w(tag, "设备不支持自动增益控制") }
} }
Log.i(tag, "回音消除和音频增强功能已全部启用")
} catch (e: Exception) {
Log.e(tag, "设置音频效果失败: ${e.message}")
} }
}.apply { start() }
}
/**
* 停止麦克风捕获并释放所有相关资源
*/
private fun stopMicrophoneCapture() {
try {
// 中断并等待捕获线程结束
captureThread?.interrupt()
captureThread?.join(300) // 最多等待300ms
// 停止录音并释放AudioRecord资源
audioRecord?.stop() // 必须先停止再释放
} catch (e: Exception) {
Log.e("AudioConfig", "Error releasing resources: ${e.message}")
} finally {
// 确保恢复原始音频状态
restoreOriginalAudioState()
} }
}
/** /**
* 获取音频格式 * 应用音频效果 - 总是启用所有音频增强功能
*/ */
fun getFormat(): AudioStreamFormat = private fun applyAudioEffects() {
AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) val sessionId = audioRecord?.audioSessionId ?: return
/** try {
* Azure SDK 调用此方法获取音频数据 // 设置音频模式为通信模式
* 这是拉流模式的核心方法,由SDK调用以获取音频数据 audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
*/
/** // 启用声学回音消除
* 优化:简化音频数据读取(移除文件保存) if (AcousticEchoCanceler.isAvailable()) {
*/ echoCanceler = AcousticEchoCanceler.create(sessionId)
override fun read(buffer: ByteArray): Int { echoCanceler?.enabled = true
try { Log.i(tag, "声学回音消除已启用")
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 } else {
if (result < 0) { Log.w(tag, "设备不支持声学回音消除")
Log.e(tag, "读取音频数据失败: $result")
return 0
}
recordfile?.saveAudioDataToWav(buffer)
return result
} catch (e: Exception) {
Log.e(tag, "读取音频异常: ${e.message}")
return 0
} }
}
/** // 启用噪声抑制
* 优化:简化资源关闭 if (NoiseSuppressor.isAvailable()) {
*/ noiseSuppressor = NoiseSuppressor.create(sessionId)
override fun close() { noiseSuppressor?.enabled = true
releaseAudioResources() Log.i(tag, "噪声抑制已启用")
} } else {
Log.w(tag, "设备不支持噪声抑制")
}
/** // 启用自动增益控制
* 释放音频资源 if (AutomaticGainControl.isAvailable()) {
*/ automaticGainControl = AutomaticGainControl.create(sessionId)
private fun releaseAudioResources() { automaticGainControl?.enabled = true
try { Log.i(tag, "自动增益控制已启用")
// 停止录音 } else {
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { Log.w(tag, "设备不支持自动增益控制")
audioRecord?.stop() }
}
// 释放回音消除器 Log.i(tag, "回音消除和音频增强功能已全部启用")
echoCanceler?.let { } catch (e: Exception) {
it.enabled = false Log.e(tag, "设置音频效果失败: ${e.message}")
it.release() }
echoCanceler = null }
}
// 释放噪声抑制器 /**
noiseSuppressor?.let { * 释放音频资源
it.enabled = false */
it.release() private fun releaseAudioResources() {
noiseSuppressor = null try {
} // 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放自动增益控制 // 释放回音消除器
automaticGainControl?.let { echoCanceler?.let {
it.enabled = false it.enabled = false
it.release() it.release()
automaticGainControl = null echoCanceler = null
} }
// 恢复音频模式 // 释放噪声抑制器
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode noiseSuppressor?.let {
it.enabled = false
it.release()
noiseSuppressor = null
}
// 释放录音实例 // 释放自动增益控制
audioRecord?.release() automaticGainControl?.let {
audioRecord = null it.enabled = false
} catch (e: Exception) { it.release()
Log.e(tag, "释放音频资源失败: ${e.message}") automaticGainControl = null
e.printStackTrace()
} }
// 恢复音频模式
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()
// }
// }
// }
/** /**
* 外部音频拉流 * 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式 * 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式

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

@ -16,6 +16,7 @@ import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice import com.deep_voice.speech.tts.AudioOutputDevice
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin" private val tag = "AzureSpeechPlugin"
@ -254,6 +255,24 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
} }
"setPause" -> {
azureAsrHelper.stopAudioPause();
}
"setcontinue" -> {
// azureAsrHelper.stopAudioPause();
azureAsrHelper.setAudiocontinue();
}
"disableBluetoothAudio" -> {
azureAsrHelper.disableBluetoothAudio();
}
"restoreOriginalAudioState" -> {
// azureAsrHelper.stopAudioPause();
azureAsrHelper.restoreOriginalAudioState();
}
"startContinuousRecognition" -> { "startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好 // 确保事件通道已准备好
@ -501,20 +520,21 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureTtsHelper.release() azureTtsHelper.release()
result.success(true) result.success(true)
} }
"setAudioOutputDevice" -> { "setAudioOutputDevice" -> {
val type = call.argument<Int>("type") val type = call.argument<Int>("type")
if (type == 0) { if (type == 0) {
// 默认(如果有耳机选耳机,否则使用系统扬声器) // 默认(如果有耳机选耳机,否则使用系统扬声器)
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT)
} else if (type == 1) { } else if (type == 1) {
// 强制使用耳机 // 强制使用声器
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
} else if (type == 2) { } else if (type == 2) {
// 强制使用扬声器 // 强制使用耳机
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES)
} }
result.success(true) result.success(true)
} }

305
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -17,7 +17,7 @@ import java.io.InputStream
/** /**
* Azure TTS Helper * Azure TTS Helper
* *
* 基于微软Azure语音服务的TTS实现 * 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/ */
@ -27,33 +27,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
} }
// Azure语音服务配置 // Azure语音服务配置
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null private var synthesizer: SpeechSynthesizer? = null
private var isInitialized = false private var isInitialized = false
private var isSpeaking = false private var isSpeaking = false
// 当前配置 // 当前配置
private var currentVoice = DEFAULT_VOICE private var currentVoice = DEFAULT_VOICE
private var currentRate = "0%" private var currentRate = "0%"
private var currentPitch = "0%" private var currentPitch = "0%"
private var currentVolume = "100%" private var currentVolume = "100%"
// 事件监听器列表 // 事件监听器列表
private val eventListeners = mutableListOf<TtsEventListener>() private val eventListeners = mutableListOf<TtsEventListener>()
private val audioDataListeners = mutableListOf<AudioDataListener>() private val audioDataListeners = mutableListOf<AudioDataListener>()
// 自定义音频输出流 // 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null private var customAudioOutputStream: PushAudioOutputStream? = null
// 流式文本处理的缓冲区 // 流式文本处理的缓冲区
private val streamBuffer = StringBuilder() private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L private var lastSpeakTime = 0L
/** /**
* 初始化TTS引擎 * 初始化TTS引擎
* *
* @param ttsAppId 未使用,保留为空即可 * @param ttsAppId 未使用,保留为空即可
* @param ttsAppToken Azure语音服务订阅密钥 * @param ttsAppToken Azure语音服务订阅密钥
* @param ttsResource Azure语音服务区域 * @param ttsResource Azure语音服务区域
@ -61,33 +61,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
* @return 是否初始化成功 * @return 是否初始化成功
*/ */
override fun initialize( override fun initialize(
ttsAppId: String, ttsAppId: String,
ttsAppToken: String, ttsAppToken: String,
ttsResource: String, ttsResource: String,
language: String language: String
): Boolean { ): Boolean {
try { try {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource) speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
// 使用标准的音频格式(16KHz, 16bit, 单声道) // 使用标准的音频格式(16KHz, 16bit, 单声道)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm)
// 确保使用正确的语音输出 // 确保使用正确的语音输出
speechConfig?.setProperty("SPEECH-AudioOutputFormat", "riff-16khz-16bit-mono-pcm") speechConfig?.setProperty("SPEECH-AudioOutputFormat", "riff-16khz-16bit-mono-pcm")
// 优化:设置低延迟连接属性 // 优化:设置低延迟连接属性
speechConfig?.setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "300") speechConfig?.setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "300")
speechConfig?.setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") speechConfig?.setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300")
speechConfig?.setSpeechSynthesisVoiceName(currentVoice) speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
// 添加日志记录语音格式 // 添加日志记录语音格式
FileLogger.d(TAG, "语音设置: voice=$currentVoice, format=Riff16Khz16BitMonoPcm") FileLogger.d(TAG, "语音设置: voice=$currentVoice, format=Riff16Khz16BitMonoPcm")
// 创建音频配置 // 创建音频配置
val audioConfig = if (customAudioOutputStream != null) { val audioConfig = if (customAudioOutputStream != null) {
// 使用自定义音频输出流 // 使用自定义音频输出流
@ -96,29 +96,29 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 使用默认扬声器 // 使用默认扬声器
AudioConfig.fromDefaultSpeakerOutput() AudioConfig.fromDefaultSpeakerOutput()
} }
// 创建合成器 // 创建合成器
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 设置事件监听 // 设置事件监听
setupEventListeners() setupEventListeners()
isInitialized = true isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource") FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
// 优化:初始化完成后立即预热 // 优化:初始化完成后立即预热
warmupSynthesizer() warmupSynthesizer()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "TTS引擎初始化失败: ${e.message}") FileLogger.e(TAG, "TTS引擎初始化失败: ${e.message}")
return false return false
} }
} }
/** /**
* 设置自定义音频输出流 * 设置自定义音频输出流
* *
* @param outputStream 自定义音频输出流,如果为null则使用默认音频输出 * @param outputStream 自定义音频输出流,如果为null则使用默认音频输出
* @return 是否设置成功 * @return 是否设置成功
*/ */
@ -126,19 +126,19 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
try { try {
// 保存引用 // 保存引用
customAudioOutputStream = outputStream customAudioOutputStream = outputStream
// 如果已初始化,需要重新创建合成器以应用新的音频输出流 // 如果已初始化,需要重新创建合成器以应用新的音频输出流
if (isInitialized) { if (isInitialized) {
recreateSynthesizer() recreateSynthesizer()
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置自定义音频输出流失败: ${e.message}") FileLogger.e(TAG, "设置自定义音频输出流失败: ${e.message}")
return false return false
} }
} }
/** /**
* 添加TTS事件监听器 * 添加TTS事件监听器
*/ */
@ -147,14 +147,14 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
eventListeners.add(listener) eventListeners.add(listener)
} }
} }
/** /**
* 移除TTS事件监听器 * 移除TTS事件监听器
*/ */
override fun removeListener(listener: TtsEventListener) { override fun removeListener(listener: TtsEventListener) {
eventListeners.remove(listener) eventListeners.remove(listener)
} }
/** /**
* 添加音频数据监听器 * 添加音频数据监听器
*/ */
@ -163,17 +163,17 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
audioDataListeners.add(listener) audioDataListeners.add(listener)
} }
} }
/** /**
* 移除音频数据监听器 * 移除音频数据监听器
*/ */
override fun removeAudioDataListener(listener: AudioDataListener) { override fun removeAudioDataListener(listener: AudioDataListener) {
audioDataListeners.remove(listener) audioDataListeners.remove(listener)
} }
/** /**
* 设置是否使用内部播放器 * 设置是否使用内部播放器
* *
* @param useInternalPlayer true: 使用内部播放器自动播放音频 * @param useInternalPlayer true: 使用内部播放器自动播放音频
* false: 仅通过音频数据监听器输出数据,不播放 * false: 仅通过音频数据监听器输出数据,不播放
*/ */
@ -183,7 +183,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
if (!useInternalPlayer && customAudioOutputStream == null) { if (!useInternalPlayer && customAudioOutputStream == null) {
// 创建自定义音频输出流以便捕获音频数据 // 创建自定义音频输出流以便捕获音频数据
customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream() customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream()
// 如果已经初始化,需要重新创建 synthesizer // 如果已经初始化,需要重新创建 synthesizer
if (isInitialized && speechConfig != null) { if (isInitialized && speechConfig != null) {
synthesizer?.close() synthesizer?.close()
@ -194,7 +194,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} else if (useInternalPlayer && customAudioOutputStream != null) { } else if (useInternalPlayer && customAudioOutputStream != null) {
// 切换回使用内部播放器 // 切换回使用内部播放器
customAudioOutputStream = null customAudioOutputStream = null
// 如果已经初始化,需要重新创建 synthesizer // 如果已经初始化,需要重新创建 synthesizer
if (isInitialized && speechConfig != null) { if (isInitialized && speechConfig != null) {
synthesizer?.close() synthesizer?.close()
@ -204,16 +204,16 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} }
} }
} }
/** /**
* 设置音频输出设备 * 设置音频输出设备
* *
* @param device 音频输出设备类型 * @param device 音频输出设备类型
*/ */
override fun setAudioOutputDevice(device: AudioOutputDevice) { override fun setAudioOutputDevice(device: AudioOutputDevice) {
// Azure TTS使用系统默认的音频路由 // Azure TTS使用系统默认的音频路由
// 音频输出设备的控制需要通过Android的AudioManager实现 // 音频输出设备的控制需要通过Android的AudioManager实现
FileLogger.e(TAG, "音频输出设备类型device=: ${device}") FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
audioManager?.let { manager -> audioManager?.let { manager ->
when (device) { when (device) {
@ -221,7 +221,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 默认模式:系统自动选择 // 默认模式:系统自动选择
manager.mode = AudioManager.MODE_NORMAL manager.mode = AudioManager.MODE_NORMAL
manager.isSpeakerphoneOn = false manager.isSpeakerphoneOn = false
manager.startBluetoothSco()
} }
AudioOutputDevice.SPEAKER -> { AudioOutputDevice.SPEAKER -> {
// 强制使用扬声器 // 强制使用扬声器
/* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖) /* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖)
@ -230,17 +232,20 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
*/ */
manager.mode = AudioManager.MODE_IN_COMMUNICATION manager.mode = AudioManager.MODE_IN_COMMUNICATION
manager.isSpeakerphoneOn = true manager.isSpeakerphoneOn = true
manager.stopBluetoothSco()
} }
AudioOutputDevice.HEADPHONES -> { AudioOutputDevice.HEADPHONES -> {
// 强制使用耳机(如果已连接) // 强制使用耳机(如果已连接)
manager.mode = AudioManager.MODE_NORMAL manager.mode = AudioManager.MODE_NORMAL
manager.isSpeakerphoneOn = false manager.isSpeakerphoneOn = false
manager.startBluetoothSco()
// 注意:Android不能强制路由到耳机,只能在耳机已连接时使用 // 注意:Android不能强制路由到耳机,只能在耳机已连接时使用
} }
} }
} }
} }
/** /**
* 触发事件通知 * 触发事件通知
*/ */
@ -256,25 +261,25 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} }
} }
} }
/** /**
* 设置事件监听器 * 设置事件监听器
*/ */
private fun setupEventListeners() { private fun setupEventListeners() {
synthesizer?.apply { synthesizer?.apply {
// 合成开始事件 // 合成开始事件
SynthesisStarted?.addEventListener { _, eventArgs -> SynthesisStarted?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成开始: resultId=${eventArgs.result.resultId}") FileLogger.d(TAG, "语音合成开始: resultId=${eventArgs.result.resultId}")
notifyEvent(TtsEventType.SYNTHESIS_STARTED) notifyEvent(TtsEventType.SYNTHESIS_STARTED)
// Azure TTS 在使用默认音频输出时会立即开始播放 // Azure TTS 在使用默认音频输出时会立即开始播放
notifyEvent(TtsEventType.PLAYBACK_STARTED) notifyEvent(TtsEventType.PLAYBACK_STARTED)
} }
// 合成中事件(接收音频数据) // 合成中事件(接收音频数据)
Synthesizing?.addEventListener { _, eventArgs -> Synthesizing?.addEventListener { _, eventArgs ->
val audioData = eventArgs.result.audioData val audioData = eventArgs.result.audioData
FileLogger.d(TAG, "接收到音频数据: ${audioData.size} 字节") FileLogger.d(TAG, "接收到音频数据: ${audioData.size} 字节")
// 通知音频数据监听器 // 通知音频数据监听器
if (audioData.isNotEmpty()) { if (audioData.isNotEmpty()) {
val listeners = ArrayList(audioDataListeners) val listeners = ArrayList(audioDataListeners)
@ -287,38 +292,45 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} }
} }
} }
// 合成完成事件 // 合成完成事件
SynthesisCompleted?.addEventListener { _, eventArgs -> SynthesisCompleted?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节") FileLogger.d(
TAG,
"语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节"
)
isSpeaking = false isSpeaking = false
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
// Azure TTS 合成完成即播放完成 // Azure TTS 合成完成即播放完成
notifyEvent(TtsEventType.PLAYBACK_COMPLETED) notifyEvent(TtsEventType.PLAYBACK_COMPLETED)
} }
// 合成取消事件 // 合成取消事件
SynthesisCanceled?.addEventListener { _, eventArgs -> SynthesisCanceled?.addEventListener { _, eventArgs ->
val reason = eventArgs.result.reason val reason = eventArgs.result.reason
FileLogger.e(TAG, "语音合成取消: reason=$reason") FileLogger.e(TAG, "语音合成取消: reason=$reason")
if (reason == ResultReason.Canceled) { if (reason == ResultReason.Canceled) {
val cancellation = SpeechSynthesisCancellationDetails.fromResult(eventArgs.result) val cancellation =
FileLogger.e(TAG, "取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}") SpeechSynthesisCancellationDetails.fromResult(eventArgs.result)
FileLogger.e(
TAG,
"取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}"
)
} }
isSpeaking = false isSpeaking = false
notifyEvent(TtsEventType.SYNTHESIS_CANCELED) notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
} }
} }
} }
/** /**
* 设置语音角色 * 设置语音角色
*/ */
override fun setVoice(voiceName: String): Boolean { override fun setVoice(voiceName: String): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
currentVoice = voiceName currentVoice = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName) speechConfig?.setSpeechSynthesisVoiceName(voiceName)
@ -326,21 +338,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}") FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "VOICE_SET_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "设置语音失败: ${e.message}" "errorCode" to "VOICE_SET_FAILED",
)) "errorMessage" to "设置语音失败: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 重新创建合成器 * 重新创建合成器
*/ */
private fun recreateSynthesizer() { private fun recreateSynthesizer() {
try { try {
synthesizer?.close() synthesizer?.close()
// 创建音频配置 // 创建音频配置
val audioConfig = if (customAudioOutputStream != null) { val audioConfig = if (customAudioOutputStream != null) {
// 使用自定义音频输出流 // 使用自定义音频输出流
@ -349,19 +363,21 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 使用默认扬声器 // 使用默认扬声器
AudioConfig.fromDefaultSpeakerOutput() AudioConfig.fromDefaultSpeakerOutput()
} }
// 使用新的音频配置创建合成器 // 使用新的音频配置创建合成器
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
setupEventListeners() setupEventListeners()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "RECREATE_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "重新创建合成器失败: ${e.message}" "errorCode" to "RECREATE_FAILED",
)) "errorMessage" to "重新创建合成器失败: ${e.message}"
)
)
} }
} }
/** /**
* 预热TTS引擎(减少首次合成延迟) * 预热TTS引擎(减少首次合成延迟)
*/ */
@ -377,7 +393,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
</voice> </voice>
</speak> </speak>
""".trimIndent() """.trimIndent()
// 静音预热(音量设为0) // 静音预热(音量设为0)
synthesizer?.SpeakSsmlAsync(warmupSsml) synthesizer?.SpeakSsmlAsync(warmupSsml)
} catch (e: Exception) { } catch (e: Exception) {
@ -385,6 +401,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
FileLogger.d(TAG, "TTS预热失败: ${e.message}") FileLogger.d(TAG, "TTS预热失败: ${e.message}")
} }
} }
//清洗播报语音内容 //清洗播报语音内容
fun cleanTextForTTS(text: String): String { fun cleanTextForTTS(text: String): String {
return text return text
@ -394,12 +411,13 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
.replace(Regex("\\s+"), " ") // 多个空格合并 .replace(Regex("\\s+"), " ") // 多个空格合并
.trim() .trim()
} }
/** /**
* 设置语音参数 * 设置语音参数
*/ */
private fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean { private fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
currentRate = formatPercentage(rate) currentRate = formatPercentage(rate)
currentPitch = formatPercentage(pitch) currentPitch = formatPercentage(pitch)
@ -407,28 +425,30 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}") FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "PARAMS_SET_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "设置语音参数失败: ${e.message}" "errorCode" to "PARAMS_SET_FAILED",
)) "errorMessage" to "设置语音参数失败: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 格式化百分比值 * 格式化百分比值
*/ */
private fun formatPercentage(value: Int): String { private fun formatPercentage(value: Int): String {
return if (value >= 0) "+$value%" else "$value%" return if (value >= 0) "+$value%" else "$value%"
} }
/** /**
* 生成SSML * 生成SSML
*/ */
private fun generateSsml(rawText: String): String { private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表 // 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf( val symbolsToMute = listOf(
"#", "*", "#", "*",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
) )
@ -461,7 +481,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
</speak> </speak>
""".trimIndent() """.trimIndent()
} }
/** /**
* 生成优化的SSML(减少复杂度提升速度) * 生成优化的SSML(减少复杂度提升速度)
*/ */
@ -487,74 +507,81 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
</speak> </speak>
""".trimIndent() """.trimIndent()
} }
/** /**
* 单次播放文本(非流式) * 单次播放文本(非流式)
*/ */
override fun speakOnce(text: String): Boolean { override fun speakOnce(text: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化") FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "NOT_INITIALIZED", TtsEventType.ERROR, mapOf(
"errorMessage" to "TTS引擎未初始化" "errorCode" to "NOT_INITIALIZED",
)) "errorMessage" to "TTS引擎未初始化"
)
)
return false return false
} }
try { try {
// 优化:使用简化的SSML生成 // 优化:使用简化的SSML生成
val ssml = generateOptimizedSsml(text) val ssml = generateOptimizedSsml(text)
isSpeaking = true isSpeaking = true
FileLogger.d(TAG, "开始语音合成,文本长度: ${text.length}") FileLogger.d(TAG, "开始语音合成,文本长度: ${text.length}")
// 直接调用SpeakSsmlAsync,SDK内部已经是异步的 // 直接调用SpeakSsmlAsync,SDK内部已经是异步的
synthesizer?.SpeakSsmlAsync(ssml) synthesizer?.SpeakSsmlAsync(ssml)
// 返回true表示已开始合成请求 // 返回true表示已开始合成请求
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isSpeaking = false isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}") FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "SYNTHESIS_ERROR", TtsEventType.ERROR, mapOf(
"errorMessage" to "语音合成异常: ${e.message}" "errorCode" to "SYNTHESIS_ERROR",
)) "errorMessage" to "语音合成异常: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 流式合成文本 * 流式合成文本
*/ */
override fun speakStream(text: String): Boolean { override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) { if (!isInitialized || text.isBlank()) {
if (!isInitialized) { if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "NOT_INITIALIZED", TtsEventType.ERROR, mapOf(
"errorMessage" to "TTS引擎未初始化" "errorCode" to "NOT_INITIALIZED",
)) "errorMessage" to "TTS引擎未初始化"
)
)
} }
return false return false
} }
try { try {
// 添加新文本到缓冲区 // 添加新文本到缓冲区
streamBuffer.append(text) streamBuffer.append(text)
// 优化:大幅减少防抖时间从600ms到150ms // 优化:大幅减少防抖时间从600ms到150ms
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 150) { if (currentTime - lastSpeakTime < 150) {
return true return true
} }
lastSpeakTime = currentTime lastSpeakTime = currentTime
val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString()) val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString())
// 优化:使用字符集合替代列表,提升查找效率 // 优化:使用字符集合替代列表,提升查找效率
val punctuationSet = setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') val punctuationSet =
setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
// 优化:从后往前查找最后一个标点符号 // 优化:从后往前查找最后一个标点符号
var lastPunctuationIndex = -1 var lastPunctuationIndex = -1
for (i in currentText.length - 1 downTo 0) { for (i in currentText.length - 1 downTo 0) {
@ -563,73 +590,79 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
break break
} }
} }
// 如果找到标点符号,则播放到该标点符号 // 如果找到标点符号,则播放到该标点符号
if (lastPunctuationIndex >= 0) { if (lastPunctuationIndex >= 0) {
// 提取要播放的文本(包含标点符号) // 提取要播放的文本(包含标点符号)
val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1)
// 剩余的文本保存在缓冲区中 // 剩余的文本保存在缓冲区中
streamBuffer.delete(0, lastPunctuationIndex + 1) streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本 // 播放提取的文本
return speakOnce(textToSpeak) return speakOnce(textToSpeak)
} }
// 如果没有找到标点符号,则等待更多文本 // 如果没有找到标点符号,则等待更多文本
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}") FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "STREAM_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "流式语音合成失败: ${e.message}" "errorCode" to "STREAM_FAILED",
)) "errorMessage" to "流式语音合成失败: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 播放剩余的流式文本 * 播放剩余的流式文本
*/ */
override fun flushStream(): Boolean { override fun flushStream(): Boolean {
if (!isInitialized) { if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "NOT_INITIALIZED", TtsEventType.ERROR, mapOf(
"errorMessage" to "TTS引擎未初始化" "errorCode" to "NOT_INITIALIZED",
)) "errorMessage" to "TTS引擎未初始化"
)
)
return false return false
} }
try { try {
// 获取缓冲区中剩余的文本 // 获取缓冲区中剩余的文本
val remainingText = streamBuffer.toString() val remainingText = streamBuffer.toString()
// 清空缓冲区 // 清空缓冲区
streamBuffer.clear() streamBuffer.clear()
// 如果缓冲区为空,直接返回成功 // 如果缓冲区为空,直接返回成功
if (remainingText.isBlank()) { if (remainingText.isBlank()) {
return true return true
} }
// 播放剩余文本 // 播放剩余文本
return speakOnce(remainingText) return speakOnce(remainingText)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "FLUSH_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "刷新流式文本失败: ${e.message}" "errorCode" to "FLUSH_FAILED",
)) "errorMessage" to "刷新流式文本失败: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 停止当前语音合成 * 停止当前语音合成
*/ */
override fun stop(): Boolean { override fun stop(): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
synthesizer?.StopSpeakingAsync() synthesizer?.StopSpeakingAsync()
streamBuffer.clear() streamBuffer.clear()
@ -638,21 +671,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}") FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf( notifyEvent(
"errorCode" to "STOP_FAILED", TtsEventType.ERROR, mapOf(
"errorMessage" to "停止语音合成失败: ${e.message}" "errorCode" to "STOP_FAILED",
)) "errorMessage" to "停止语音合成失败: ${e.message}"
)
)
return false return false
} }
} }
/** /**
* 释放资源 * 释放资源
*/ */
override fun release() { override fun release() {
dispose() dispose()
} }
/** /**
* 释放资源(内部方法) * 释放资源(内部方法)
*/ */
@ -660,15 +695,15 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
try { try {
// 停止播放 // 停止播放
stop() stop()
// 关闭合成器 // 关闭合成器
synthesizer?.close() synthesizer?.close()
synthesizer = null synthesizer = null
// 关闭配置 // 关闭配置
speechConfig?.close() speechConfig?.close()
speechConfig = null speechConfig = null
// 关闭自定义音频输出流 // 关闭自定义音频输出流
try { try {
customAudioOutputStream?.close() customAudioOutputStream?.close()
@ -676,10 +711,10 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} catch (e: Exception) { } catch (e: Exception) {
// 忽略关闭异常 // 忽略关闭异常
} }
// 清空流缓冲区 // 清空流缓冲区
streamBuffer.clear() streamBuffer.clear()
// 重置状态 // 重置状态
isInitialized = false isInitialized = false
isSpeaking = false isSpeaking = false
@ -689,7 +724,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
isSpeaking = false isSpeaking = false
} }
} }
/** /**
* 是否正在播放 * 是否正在播放
*/ */

Loading…
Cancel
Save