Browse Source

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

newdev_shunjiawei
fdp 1 year ago
parent
commit
86fd3efe06
  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. 42
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  13. 125
      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<void> setPause();
Future<void> setcontinue();
Future<void> restoreOriginalAudioState();
Future<void> disableBluetoothAudio();
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive();

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

@ -414,4 +414,54 @@ class AzureAsrService extends GetxService implements AsrService {
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
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
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
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(
key: ValueKey('device_status_$isConnected'),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 状态标题行
_buildDeviceStatusHeader(isDarkMode),
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断
// 使用响应式访问确保UI更新
return controller.isFullyConnectedRx.value
? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息
: _buildDisconnectedInfo(isDarkMode); //未连接提示
}),
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
// 使用响应式访问确保UI更新
if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton();
} else {
return const SizedBox();
}
}),
],
),
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 状态标题行
_buildDeviceStatusHeader(isDarkMode),
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断
// 使用响应式访问确保UI更新
return controller.isFullyConnectedRx.value
? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息
: _buildDisconnectedInfo(isDarkMode); //未连接提示
}),
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
// 使用响应式访问确保UI更新
if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton();
} else {
return const SizedBox();
}
}),
],
),
);
});
}
@ -328,7 +328,6 @@ class HomeView extends GetView<HomeController> {
// 连接错误部分
// 已连接设备信息(新增设备名称 + 信号强度 + 电池信息)
Widget _buildConnectedDeviceInfo(bool isDarkMode) {
return Column(
@ -417,20 +416,20 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode,
),
// 分隔线
Container(
height: 30.h,
width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
),
// 充电盒
_buildCompactBatteryItem(
label: '充电盒',
batteryLevel: caseBattery.value,
isCharging: caseCharging.value,
isDarkMode: isDarkMode,
),
// // 分隔线
// Container(
// height: 30.h,
// width: 1,
// color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
// ),
// // 充电盒
// _buildCompactBatteryItem(
// label: '充电盒',
// batteryLevel: caseBattery.value,
// isCharging: caseCharging.value,
// 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 RxBool isMarked = 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;
// Transcription content
@ -133,9 +134,12 @@ class MeetingRecordController extends GetxController
_recognitionSubscription?.cancel();
titleEditingController.dispose();
titleFocusNode.dispose();
_bleManager.closeCodec();
_asrService.stopContinuousRecognition();
if (audioType.value != 0) {
_bleManager.closeCodec();
}
_asrService.stopRecord(true);
_asrService.stopContinuousRecognition();
isRecording.value = false;
}
/// Handles focus changes in title text field
@ -222,7 +226,7 @@ class MeetingRecordController extends GetxController
// Start audio recording
await _asrService.enableRecord("${dir.path}/$fullFileName.wav");
isRecording.value = true;
// Update state
fileName.value = fullFileName;
newName.value = fullFileName;
@ -327,7 +331,10 @@ class MeetingRecordController extends GetxController
/// Saves recording and cleans up resources
Future<void> saveRecording() async {
resetTimer();
_bleManager.closeCodec();
if (audioType.value != 0) {
_bleManager.closeCodec();
}
if (!isRecording.value) return;
await _asrService.stopRecord(true);
// Rename file if title was edited
@ -339,12 +346,12 @@ class MeetingRecordController extends GetxController
final Newdir = Directory("${appDir.path}/DestAudio/");
if (!await Newdir.exists()) await Newdir.create(recursive: true);
await _asrService.moveFile(
"${dir.path}/${fileName.value}.wav",
"${Newdir.path}/${fileName.value}.wav",
"${dir.path}${fileName.value}.wav",
"${Newdir.path}${fileName.value}.wav",
);
} else {
await _asrService.renameFile(
"${dir.path}/${fileName.value}.wav",
"${dir.path}${fileName.value}.wav",
newName.value,
);
}
@ -352,6 +359,7 @@ class MeetingRecordController extends GetxController
await _asrService.stopContinuousRecognition();
_resetToDefaultState();
isRecording.value = false;
}
/// Resets controller to initial state

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

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

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

@ -1,3 +1,4 @@
import 'dart:ffi';
import 'dart:io';
import 'package:deep_voice/core/utils/permission_util.dart';
@ -37,7 +38,7 @@ class TranslationController extends GetxController {
// 音频输入源
bool _audioSourceType = false;
// 倒计时器
bool istts = false;
final RxString recordDurationText = '00:00'.obs;
Timer? _recordTimer;
int _seconds = 0;
@ -47,6 +48,8 @@ class TranslationController extends GetxController {
static const String _targetLanguageKey = 'translation_target_language';
static const int _maxHistoryItems = 10;
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
RxInt lastActiveSpeaker = 0.obs; // 上一次状态
// 状态变量
final isRecognizing = false.obs;
final isTranslating = false.obs;
@ -62,12 +65,15 @@ class TranslationController extends GetxController {
final currentModeTitle = 'simultaneousTranslation'.obs;
final currentMode = 'simultaneous'.obs;
// 翻译历史和当前项
String? fTFTranslationResult = '';
final translationHistory = <TranslationItem>[].obs;
String? currentSessionId;
final appDocDir = getApplicationDocumentsDirectory();
var appDir;
var dir;
//int num = 0;
Future<void> changeTranslationMode(String mode) async {
currentMode.value = 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 {
if (translationHistory.isNotEmpty &&
@ -210,19 +239,6 @@ class TranslationController extends GetxController {
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 {
@ -340,21 +356,6 @@ class TranslationController extends GetxController {
}
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") {
bleManager.openDecoder();
@ -375,6 +376,7 @@ class TranslationController extends GetxController {
}
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
_asrService.setcontinue();
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
startRecording();
@ -461,8 +463,6 @@ class TranslationController extends GetxController {
translationHistory.last.sessionId != currentSessionId,
isIntermediate: true,
);
translationHistory.add(newItem);
// 添加新项后滚动到底部
_scrollToBottom();
} else {
@ -562,32 +562,29 @@ class TranslationController extends GetxController {
detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode;
print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}");
// 判断是否是面对面模式且语言一致
final bool isFaceToFaceSameLang =
detectedSourceLanguageCode == sourceLanguageCode.value &&
currentMode.value == "faceToFace";
// 设置音频输出设备
var isSuccess = await _ttsService.setAudioOutputDevice(
isFaceToFaceSameLang
? AudioOutputDevice.speaker.index
: AudioOutputDevice.headphones.index,
);
if (isSuccess) {
print('设置音频输出设备成功');
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) {
// Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult);
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
if (isFinal && currentMode.value == 'faceToFace') {
print("音频输出文本2$translationResult");
fTFTranslationResult =
(fTFTranslationResult ?? '') + translationResult;
if (activeSpeaker.value == 0 && !istts) {
istts = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
await playTranslatedText(fTFTranslationResult!);
fTFTranslationResult = '';
;
}
} else {
print('设置音频输出设备失败');
}
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value &&
isFinal &&
currentMode.value != 'faceToFace') {
_ttsService.setAudioOutputDevice(activeSpeaker.value);
await playTranslatedText(translationResult);
}
} else {
Logger.info(

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

@ -399,38 +399,209 @@ class TranslationView extends GetView<TranslationController> {
),
],
),
child: 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,
child: Obx(() {
// 同时模式显示双按钮
if (controller.currentMode.value == 'faceToFace') {
controller.startRecognition();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 耳机图标按钮 (说话者1)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 1,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.headset, // 耳机图标
label: "耳机用户",
),
SizedBox(width: 40.w),
// 手机图标按钮 (说话者2)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 2,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.phone_android, // 手机图标
label: "手机用户",
),
],
);
}
// 普通模式显示单按钮
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
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import android.util.Log
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
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 com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback
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.TimeUnit
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 var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
private var speechConfig: SpeechConfig? = null
private var recognizer: SpeechRecognizer? = null
private var audioConfig: AudioConfig? = null
@ -55,7 +55,7 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
private var microphoneStream: MicrophoneStream? = null
// private var microphoneStream: MicrophoneStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null
// 音频模式管理
@ -65,6 +65,10 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件处理
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()
// 1. 创建可控制的音频流
val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1)
audioStream = AudioInputStream.createPushStream(format)
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
// 5. 初始化麦克风数据
initAudioRecord()
// 保存配置
this.subscriptionKey = subscriptionKey
this.region = region
@ -206,14 +215,17 @@ class AzureAsrHelper(private val context: Context) {
*/
private fun setupMicrophoneStream() {
try {
// 创建麦克风流
microphoneStream = MicrophoneStream()
// microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream)
// audioConfig = AudioConfig.fromStreamInput(microphoneStream)
audioConfig = AudioConfig.fromStreamInput(audioStream)
} catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}")
microphoneStream = null
//microphoneStream = null
}
}
@ -349,10 +361,13 @@ class AzureAsrHelper(private val context: Context) {
setupEventListeners(callback)
// 启动音频处理
startAudioProcessing()
//startAudioProcessing()
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()
// 开始录音
audioRecord?.startRecording()
startCaptureThread() // 再启动数据读取线程
isContinuousRecognitionActive = true
return true
@ -458,7 +473,7 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理
stopAudioProcessing()
// 停止录音
stopMicrophoneCapture()
// 会话结束事件会设置isContinuousRecognitionActive = false
return true
} catch (e: Exception) {
@ -497,6 +512,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
}
releaseAudioResources()
// 停止音频处理
stopAudioProcessing()
@ -510,13 +526,17 @@ class AzureAsrHelper(private val context: Context) {
speechConfig?.close()
speechConfig = null
// 关闭音频流(根据实际实现可能需要)
audioStream?.close()
audioStream = null
// 释放音频配置
audioConfig?.close()
audioConfig = null
// 确保状态被重置
isContinuousRecognitionActive = false
microphoneStream = null
//microphoneStream = null
externalAudioStream = null
audioConfig = null
recognizer = null
@ -525,7 +545,7 @@ class AzureAsrHelper(private val context: Context) {
// 确保状态被重置
isContinuousRecognitionActive = false
microphoneStream = null
//microphoneStream = null
externalAudioStream = null
audioConfig = null
recognizer = null
@ -555,15 +575,15 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理
*/
private fun stopAudioProcessing() {
microphoneStream?.let {
try {
it.close()
microphoneStream = null
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
}
// microphoneStream?.let {
// try {
// it.close()
// microphoneStream = null
// } catch (e: Exception) {
// Log.e(tag, "关闭麦克风流失败: ${e.message}")
// e.printStackTrace()
// }
// }
externalAudioStream?.let {
try {
@ -631,200 +651,418 @@ class AzureAsrHelper(private val context: Context) {
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() {
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()
fun disableBluetoothAudio() {
try {
// 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由
audioManager?.isBluetoothScoOn = false
// 停止蓝牙SCO连接
audioManager?.stopBluetoothSco()
// 切换回正常音频模式(非通话模式)
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
} catch (e: Exception) {
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}")
}
}
/**
* 初始化麦克风
*/
private fun initMicrophone() {
try {
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.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
)
}
/**
* 恢复原始音频设备状态(通常是重新启用蓝牙)
*/
fun restoreOriginalAudioState() {
try {
// 恢复应用启动时的原始音频模式
audioManager?.mode = AudioManager.MODE_NORMAL
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
// 重新启用蓝牙SCO
audioManager?.isBluetoothScoOn = true
// 启动蓝牙SCO连接(通常在需要蓝牙通话时使用)
audioManager?.startBluetoothSco()
// 应用音频效果
applyAudioEffects()
} catch (e: Exception) {
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
)
// 检查录音状态
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
throw IllegalStateException("录音启动失败")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// 创建 AudioRecord 对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2 // 使用两倍缓冲区防止溢出
)
/**
* 应用音频效果 - 总是启用所有音频增强功能
*/
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, "设备不支持声学回音消除")
}
// 应用音频效果
applyAudioEffects()
}
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(sessionId)
noiseSuppressor?.enabled = true
Log.i(tag, "噪声抑制已启用")
} else {
Log.w(tag, "设备不支持噪声抑制")
// 3. 单独封装线程启动逻辑
private fun startCaptureThread() {
captureThread = Thread {
// 循环条件确保录音已开始
val buffer = ByteArray(1024) // 4KB数据缓冲区
while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
// 从麦克风读取数据
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
// 仅在未暂停时处理数据
if (!isPaused && bytesRead > 0) {
// 写入音频流(可能是网络传输或本地处理)
audioStream?.write(buffer)
// 保存到WAV文件(如果启用了录制功能)
recordfile?.saveAudioDataToWav(buffer)
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
automaticGainControl = AutomaticGainControl.create(sessionId)
automaticGainControl?.enabled = true
Log.i(tag, "自动增益控制已启用")
} else {
Log.w(tag, "设备不支持自动增益控制")
// 暂停时短暂休眠以减少CPU占用
if (isPaused) {
try {
Thread.sleep(50)
} catch (e: InterruptedException) {
break // 线程被中断时退出循环
}
}
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 =
AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
/**
* 应用音频效果 - 总是启用所有音频增强功能
*/
private fun applyAudioEffects() {
val sessionId = audioRecord?.audioSessionId ?: return
/**
* 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
try {
// 设置音频模式为通信模式
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
// 启用声学回音消除
if (AcousticEchoCanceler.isAvailable()) {
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true
Log.i(tag, "声学回音消除已启用")
} else {
Log.w(tag, "设备不支持声学回音消除")
}
}
/**
* 优化:简化资源关闭
*/
override fun close() {
releaseAudioResources()
}
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(sessionId)
noiseSuppressor?.enabled = true
Log.i(tag, "噪声抑制已启用")
} else {
Log.w(tag, "设备不支持噪声抑制")
}
/**
* 释放音频资源
*/
private fun releaseAudioResources() {
try {
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
automaticGainControl = AutomaticGainControl.create(sessionId)
automaticGainControl?.enabled = true
Log.i(tag, "自动增益控制已启用")
} else {
Log.w(tag, "设备不支持自动增益控制")
}
// 释放回音消除器
echoCanceler?.let {
it.enabled = false
it.release()
echoCanceler = null
}
Log.i(tag, "回音消除和音频增强功能已全部启用")
} catch (e: Exception) {
Log.e(tag, "设置音频效果失败: ${e.message}")
}
}
// 释放噪声抑制器
noiseSuppressor?.let {
it.enabled = false
it.release()
noiseSuppressor = null
}
/**
* 释放音频资源
*/
private fun releaseAudioResources() {
try {
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放自动增益控制
automaticGainControl?.let {
it.enabled = false
it.release()
automaticGainControl = null
}
// 释放回音消除器
echoCanceler?.let {
it.enabled = false
it.release()
echoCanceler = null
}
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// 释放噪声抑制器
noiseSuppressor?.let {
it.enabled = false
it.release()
noiseSuppressor = null
}
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
// 释放自动增益控制
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()
// }
// }
// }
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式

42
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.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
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" -> {
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好
@ -501,18 +520,19 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureTtsHelper.release()
result.success(true)
}
"setAudioOutputDevice" -> {
val type = call.argument<Int>("type")
if (type == 0) {
// 默认(如果有耳机选耳机,否则使用系统扬声器)
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT)
} else if (type == 1) {
// 强制使用耳机
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES)
} else if (type == 2) {
// 强制使用扬声器
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
}
val type = call.argument<Int>("type")
if (type == 0) {
// 默认(如果有耳机选耳机,否则使用系统扬声器)
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT)
} else if (type == 1) {
// 强制使用声器
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
} else if (type == 2) {
// 强制使用耳机
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES)
}
result.success(true)

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

@ -213,7 +213,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun setAudioOutputDevice(device: AudioOutputDevice) {
// Azure TTS使用系统默认的音频路由
// 音频输出设备的控制需要通过Android的AudioManager实现
FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
audioManager?.let { manager ->
when (device) {
@ -221,7 +221,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 默认模式:系统自动选择
manager.mode = AudioManager.MODE_NORMAL
manager.isSpeakerphoneOn = false
manager.startBluetoothSco()
}
AudioOutputDevice.SPEAKER -> {
// 强制使用扬声器
/* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖)
@ -230,11 +232,14 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
*/
manager.mode = AudioManager.MODE_IN_COMMUNICATION
manager.isSpeakerphoneOn = true
manager.stopBluetoothSco()
}
AudioOutputDevice.HEADPHONES -> {
// 强制使用耳机(如果已连接)
manager.mode = AudioManager.MODE_NORMAL
manager.isSpeakerphoneOn = false
manager.startBluetoothSco()
// 注意:Android不能强制路由到耳机,只能在耳机已连接时使用
}
}
@ -290,7 +295,10 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 合成完成事件
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
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
// Azure TTS 合成完成即播放完成
@ -303,8 +311,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
FileLogger.e(TAG, "语音合成取消: reason=$reason")
if (reason == ResultReason.Canceled) {
val cancellation = SpeechSynthesisCancellationDetails.fromResult(eventArgs.result)
FileLogger.e(TAG, "取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}")
val cancellation =
SpeechSynthesisCancellationDetails.fromResult(eventArgs.result)
FileLogger.e(
TAG,
"取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}"
)
}
isSpeaking = false
@ -326,10 +338,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
)
)
return false
}
}
@ -355,10 +369,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
setupEventListeners()
} catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
)
)
}
}
@ -385,6 +401,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
FileLogger.d(TAG, "TTS预热失败: ${e.message}")
}
}
//清洗播报语音内容
fun cleanTextForTTS(text: String): String {
return text
@ -394,6 +411,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
.replace(Regex("\\s+"), " ") // 多个空格合并
.trim()
}
/**
* 设置语音参数
*/
@ -407,10 +425,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
)
)
return false
}
}
@ -494,10 +514,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun speakOnce(text: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
)
)
return false
}
@ -517,10 +539,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} catch (e: Exception) {
isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
)
)
return false
}
}
@ -531,10 +555,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
)
)
}
return false
}
@ -553,7 +579,8 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString())
// 优化:使用字符集合替代列表,提升查找效率
val punctuationSet = setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
val punctuationSet =
setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
// 优化:从后往前查找最后一个标点符号
var lastPunctuationIndex = -1
@ -580,10 +607,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
)
)
return false
}
}
@ -593,10 +622,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
*/
override fun flushStream(): Boolean {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
)
)
return false
}
@ -616,10 +647,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return speakOnce(remainingText)
} catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
)
)
return false
}
}
@ -638,10 +671,12 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
))
notifyEvent(
TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
)
)
return false
}
}

Loading…
Cancel
Save