Browse Source

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

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
18b86b000a
  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. 215
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  12. 618
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  13. 46
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  14. 305
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  15. 120
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  16. 44
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt
  17. 73
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.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),
),
);
}),
],
),
],
),
),
);
}

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

@ -1,9 +1,11 @@
package com.yunqiinnovation.agent_service
import android.content.Context
import android.content.Intent
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.chat_api.ChatApiService
import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper
@ -23,6 +25,8 @@ import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.bytedance_speech.BytedanceTTS
import java.util.UUID
/**
* 代理服务事件监听接口
*/
@ -56,7 +60,7 @@ object AgentService : CoroutineScope {
private var azureAsrHelper: AzureAsrHelper? = null
// 使用BytedanceTTS作为唯一的TTS实现
private var ttsService: BytedanceTTS? = null
private var ttsService: AzureTtsHelper? = null
// ChatAPI服务 - 使用新的ChatApiService
private lateinit var chatApiService: ChatApiService
@ -97,7 +101,7 @@ object AgentService : CoroutineScope {
// AI流生成相关
private var currentAiJob: Job? = null
private var currsessionId:String = ""
// 空闲检测相关
private var idleCheckJob: Job? = null
private val maxIdleSeconds = 10 // 最大空闲秒数
@ -244,14 +248,14 @@ object AgentService : CoroutineScope {
val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN"
// 创建并初始化BytedanceTTS
val bytedanceTts = BytedanceTTS(context)
ttsService = bytedanceTts
val azureTtsHelper = AzureTtsHelper(context)
ttsService = azureTtsHelper
// 初始化Bytedance TTS
val success = bytedanceTts.initialize(
ttsAppId = ttsAppId,
ttsAppToken = ttsAppToken,
ttsResource = "", // Bytedance TTS不需要resource参数
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = config["azureSpeechKey"]?.toString() ?: "", // Azure需要subscription key
ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息
language = ttsLanguage
)
@ -278,13 +282,23 @@ object AgentService : CoroutineScope {
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
TtsEventType.PLAYBACK_STARTED -> {
TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容
restartIdleCheck()
sendEvent("playback_started", mapOf("status" to "playback_started"))
// 发送AI说话开始广播
val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
}
TtsEventType.PLAYBACK_COMPLETED -> {
TtsEventType.PLAYBACK_COMPLETED -> { //结束AI播报内容
restartIdleCheck()
sendEvent("playback_completed", mapOf("status" to "playback_completed"))
// 发送AI说话开始广播
val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_END").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
}
TtsEventType.ERROR -> {
_isTtsSpeaking.set(false)
@ -666,6 +680,7 @@ object AgentService : CoroutineScope {
// 记录新的AI任务
currentAiJob = launch {
try {
currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出
_isAiStreaming.set(true)
audioPlayer?.playAudio(R.raw.await, true,0.3f)
@ -692,8 +707,8 @@ object AgentService : CoroutineScope {
)
val _systemPrompt = Utils.fillTemplate(systemPrompt,location)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
// Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
// Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
// 再添加历史消息
for (i in 0 until historyMessages.length()) {
@ -730,61 +745,84 @@ object AgentService : CoroutineScope {
}
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream(
currsessionId,
messagesWithSystemPrompt,
object : StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse && broadcast) {
ttsService?.speakStream(token)
if (token.length > 0){
audioPlayer?.stopAudio()
override fun onToken(sessionid:String,token: String) {
try {
if (sessionid != currsessionId){
return
}
}
if (broadcast){
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
responseBuilder.append(token)
if (speakResponse && broadcast) {
ttsService?.speakStream(token)
if (token.length > 0) {
audioPlayer?.stopAudio()
}
}
if (broadcast) {
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
}
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onToken 异常", e)
}
}
override fun onComplete() {
// 视情况决定是否朗读回复
if (speakResponse && broadcast) {
ttsService?.flushStream()
}
val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"text" to response,
"userInput" to displayText
)
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
if (response.isNotEmpty()) {
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
override fun onComplete(sessionid:String) {
try {
// 视情况决定是否朗读回复
if (speakResponse && broadcast && sessionid == currsessionId){
ttsService?.flushStream()
}
val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"text" to response,
"userInput" to displayText
)
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
if (response.isNotEmpty()) {
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
}
// 保存聊天记录
saveChatMessage(
displayText,
response,
aiMetadata,
userMetadata.toString()
)
// 标记AI流式输出已完成
_isAiStreaming.set(false)
currentAiJob = null
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onComplete 异常", e)
}
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
// 标记AI流式输出已完成
_isAiStreaming.set(false)
currentAiJob = null
}
override fun onError(e: Exception) {
Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
))
// 标记AI流式输出已完成
_isAiStreaming.set(false)
currentAiJob = null
override fun onError(sessionid:String,e: Exception) {
try {
Log.e(TAG, "AI处理出错", e)
sendEvent(
"error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
)
)
// 标记AI流式输出已完成
_isAiStreaming.set(false)
currentAiJob = null
}catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onError 异常", e)
}
}
override fun onFunctionCall(call: JSONObject) {
override fun onFunctionCall(sessionid:String,call: JSONObject) {
try {
// audioPlayer?.playAudio(R.raw.calling, true)
val name = call.getString("name")
@ -796,28 +834,34 @@ object AgentService : CoroutineScope {
stopRecognition()
}
} catch (e: Exception) {
Log.e(TAG, "处理函数调用事件失败", e)
Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e)
}
}
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name,functionCallResult);
aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean;
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
))
override fun onFunctionCallResult(sessionid:String,functionCall: JSONObject, functionCallResult: JSONObject) {
try {
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean;
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
)
)
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onFunctionCallResult 异常", e)
}
}
}
)
} catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR"
Log.e(TAG, "AI处理出错", e)
Log.e(TAG, "liwei--------------- AI处理出错", e)
sendEvent("error", mapOf(
"code" to errorType,
"message" to e.message.toString()
@ -1119,6 +1163,7 @@ object AgentService : CoroutineScope {
*/
class AudioPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
private var isInitialized = false
/**
* 播放音频资源
@ -1155,21 +1200,47 @@ object AgentService : CoroutineScope {
release()
}
}
fun stopAudio() {
mediaPlayer?.stop()
try {
mediaPlayer?.apply {
when {
isPlaying -> {
stop()
Log.d(TAG, "音频已停止")
}
else -> {
Log.d(TAG, "音频未在播放状态,无需停止")
}
}
}
} catch (e: IllegalStateException) {
Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e)
// 重置 MediaPlayer
release()
} catch (e: Exception) {
Log.e(TAG, "停止音频播放异常: ${e.message}", e)
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
if (isPlaying) {
stop()
}
reset()
release()
}
mediaPlayer = null
isInitialized = false
} catch (e: Exception) {
// 忽略异常
Log.e(TAG, "释放音频资源异常: ${e.message}", e)
mediaPlayer = null
isInitialized = false
}
}
}

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可拉取的形式

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.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,20 +520,21 @@ 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)
}

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

120
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -39,27 +39,27 @@ interface StreamCallback {
/**
* 接收到新的token
*/
fun onToken(token: String)
fun onToken(sessionid: String,token: String)
/**
* 流式响应完成
*/
fun onComplete()
fun onComplete(sessionid: String)
/**
* 出现错误
*/
fun onError(error: Exception)
fun onError(sessionid: String,error: Exception)
/**
* 函数调用 - 兼容JSONObject格式
*/
fun onFunctionCall(functionCall: org.json.JSONObject)
fun onFunctionCall(sessionid: String,functionCall: org.json.JSONObject)
/**
* 函数调用结果 - 兼容JSONObject格式
*/
fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject)
fun onFunctionCallResult(sessionid: String,functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject)
}
/**
@ -142,7 +142,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
private var currentMessages: List<Map<String, Any>> = emptyList()
private var toolCalls: MutableMap<Int, ToolCallInfo> = Collections.synchronizedMap(mutableMapOf())
private var currSessionId = ""
private var currSessionCallback: StreamCallback? = null
// 为每个会话维护独立的回调函数
private val sessionCallbacks: MutableMap<String, StreamCallback> = Collections.synchronizedMap(mutableMapOf())
@ -156,17 +157,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
*/
private fun abortCurrentSession() {
if (currSessionId.isNotEmpty()) {
val oldSessionId = currSessionId
// 1. 取消当前的协程任务
currentStreamJob?.cancel()
currentStreamJob = null
// 2. 通知旧会话被中止
// currSessionCallback?.onError(ChatApiException("Session aborted by new request"))
currSessionCallback?.onComplete() // 直接完成当前会话
getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话
// 3. 清理状态
currSessionId = ""
currSessionCallback = null
sessionCallbacks.remove(oldSessionId)
toolCalls.clear()
}
}
@ -175,12 +177,20 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 清理当前会话
*/
private fun clearCurrentSession() {
val oldSessionId = currSessionId
currSessionId = ""
currSessionCallback = null
sessionCallbacks.remove(oldSessionId)
currentStreamJob = null
toolCalls.clear()
}
/**
* 获取指定会话的回调函数
*/
private fun getSessionCallback(sessionId: String): StreamCallback? {
return if (sessionId == currSessionId) sessionCallbacks[sessionId] else null
}
/**
* 初始化ChatAPI服务
*/
@ -312,17 +322,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/**
* 发送消息(流式输出)- 兼容JSONArray格式
*/
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) {
fun sendMessageStream(sessionid: String,messages: org.json.JSONArray, callback: StreamCallback) {
// 1. 立即中止旧会话
abortCurrentSession()
// 2. 创建新会话
currSessionId = UUID.randomUUID().toString()
currSessionCallback = callback
// val newSessionId = UUID.randomUUID().toString()
currSessionId = sessionid
sessionCallbacks[sessionid] = callback
// 3. 转换消息并开始处理
val messageList = convertJsonArrayToList(messages)
sendMessageStream(currSessionId, messageList)
sendMessageStream(sessionid, messageList)
}
/**
@ -336,7 +347,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
currSessionCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
try {
getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
} catch (e: Exception) {
Log.e(TAG, "onError回调异常: ${e.message}", e)
}
return
}
@ -345,7 +360,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
// Log.d(TAG, "liwei-------------------------开始AI 对话 0")
currentStreamJob = launch {
try {
// 转换消息格式
@ -355,22 +370,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
throw e
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 1")
// 直接获取工具列表
val tools = getOpenAiTools()
// Log.d(TAG, "liwei-------------------------开始AI 对话 2")
// 构建请求
if (currentModel.isEmpty()) {
Log.e("ChatApiService", "模型名称为空")
throw IllegalArgumentException("模型名称不能为空")
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 3")
if (chatMessages.isEmpty()) {
Log.e("ChatApiService", "消息列表为空")
throw IllegalArgumentException("消息列表不能为空")
}
// Log.e("ChatApiService", "AI解析工具列表: ${tools}")
// Log.d(TAG, "liwei-------------------------开始AI 对话 4")
val chatsFlow = try {
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
@ -379,42 +394,49 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
temperature = 0.7,
tools = if (tools.isNotEmpty()) tools else null
)
// Log.d(TAG, "liwei-------------------------开始AI 对话 5")
if (openAI == null) {
Log.e("ChatApiService", "openAI对象为null")
throw IllegalStateException("OpenAI客户端未初始化")
}
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
val flow = openAI!!.chatCompletions(chatCompletionRequest)
// Log.d(TAG, "liwei-------------------------开始AI 对话 6")
flow
} catch (e: Exception) {
Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e)
throw e
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 7")
chatsFlow.collect { result ->
// 每次处理前检查是否仍是当前会话
if (sessionid != currSessionId) {
throw CancellationException("Session cancelled")
// throw CancellationException("Session cancelled")
return@collect
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-1")
val choice = result.choices.firstOrNull()
if (choice == null) {
Log.w("ChatApiService", "[Session: $sessionid] choice为null")
return@collect
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-2")
val delta = choice.delta
if (delta == null) {
Log.w("ChatApiService", "[Session: $sessionid] delta为null")
return@collect
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-3")
// 处理普通文本内容
delta.content?.let { content ->
currSessionCallback?.onToken(content)
try {
getSessionCallback(sessionid)?.onToken(sessionid,content)
} catch (e: Exception) {
Log.e(TAG, "onToken回调异常: ${e.message}", e)
// 不重新抛出异常,继续处理后续内容
}
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-4")
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
try {
@ -461,23 +483,32 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
}
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 8")
// 完成处理
if (sessionid == currSessionId) {
// Log.d(TAG, "liwei-------------------------开始AI 对话 9")
// 检查是否有工具调用需要处理
val hasToolCalls = processToolCalls(sessionid)
if (!hasToolCalls) {
currSessionCallback?.onComplete()
try {
getSessionCallback(sessionid)?.onComplete(sessionid)
} catch (e: Exception) {
Log.e(TAG, "onComplete回调异常: ${e.message}", e)
}
clearCurrentSession()
}
}
// Log.d(TAG, "liwei-------------------------开始AI 对话 10")
} catch (e: CancellationException) {
// 会话被取消,不需要处理
Log.d("ChatApiService", "Session $sessionid cancelled")
Log.d(TAG, "Session $sessionid cancelled")
} catch (e: Exception) {
if (sessionid == currSessionId) {
currSessionCallback?.onError(ChatApiException("流式请求失败: ${e.message}"))
try {
getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("流式请求失败: ${e.message}"))
} catch (ex: Exception) {
Log.e(TAG, "onError回调异常: ${ex.message}", ex)
}
clearCurrentSession()
}
}
@ -517,7 +548,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
"id" to firstToolCall.id
)
// 通知上层工具调用事件
currSessionCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
try {
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall))
} catch (e: Exception) {
Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e)
}
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理
val toolCallDeferred = async {
try {
@ -564,10 +599,15 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
if (sessionid == currSessionId) {
// 处理结果
currSessionCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
try {
getSessionCallback(sessionid)?.onFunctionCallResult(
sessionid,
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
} catch (e: Exception) {
Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
}
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
sessionid= sessionid,
@ -843,7 +883,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
coroutineContext[Job]?.cancelChildren()
// 清理其他资源
currSessionCallback = null
sessionCallbacks.clear()
currSessionId = ""
currentMessages = emptyList()
toolCalls.clear()
@ -1305,4 +1345,4 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return "Function '$functionName' called with arguments: $arguments"
}
}
}
}

44
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

@ -50,6 +50,7 @@ class MCPSubClient(
* 连接到MCP服务器
*/
suspend fun connect(): Boolean = connectionMutex.withLock {
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 1")
if (isConnected) return true
Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl")
return try {
@ -60,7 +61,7 @@ class MCPSubClient(
version = "1.0.0"
)
)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 2")
// 根据URL类型选择传输方式
val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
@ -83,15 +84,26 @@ class MCPSubClient(
return false
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 3")
transport = newTransport
// 连接到服务器
client.connect(newTransport)
try {
// 连接到服务器
withTimeout(10000) { // 10秒超时
client.connect(newTransport)
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 4")
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接超时")
return false
} catch (e: Exception) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接异常: ${e.message}", e)
return false
}
// 获取可用工具列表
try {
val toolsResult = client.listTools()
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 5")
if (toolsResult != null) {
availableTools.clear()
val filtered = toolsResult.tools.filter { tool ->
@ -99,17 +111,19 @@ class MCPSubClient(
}
Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}")
availableTools.addAll(filtered)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 6")
}
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
// 即使获取工具失败,连接也可能是成功的
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 7")
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 8")
// 启动心跳检测
// startHeartbeat()
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
@ -162,11 +176,23 @@ class MCPSubClient(
// 先检查连接状态
if (!checkConnection()) {
Log.e(TAG, "无法连接到服务器,工具调用失败")
return null
return mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call failed: mcp: service not connected"
)),
"isError" to true
)
}
if (!containsTool(name)) {
Log.w(TAG, "此客户端不包含工具: $name")
return null
return mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call failed: no found tool"
)),
"isError" to true
)
}
return try {
// 创建工具调用请求 - 将Map转换为JsonObject

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

@ -39,6 +39,9 @@ class MusicService : MediaBrowserServiceCompat() {
private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0
private var currentVolume: Float = 1.0f // 当前音量
private var originalVolume: Float = 1.0f // 原始音量,用于恢复
private var isVolumeReduced: Boolean = false // 是否已降低音量
fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus
@ -179,6 +182,8 @@ class MusicService : MediaBrowserServiceCompat() {
addAction("com.yunqiinnovation.music_service.MEDIA_CLOSE")
addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS")
addAction("com.yunqiinnovation.music_service.MEDIA_NEXT")
addAction("com.yunqiinnovation.music_service.AI_SAY_START")
addAction("com.yunqiinnovation.music_service.AI_SAY_END")
}
// Android 14+ 需要指定 flag
@ -205,7 +210,7 @@ class MusicService : MediaBrowserServiceCompat() {
val songMap = intent.getSerializableExtra("song") as? Map<String, Any>
songMap?.let { song ->
val songId = song["id"] ?: return@let
val existingIndex = playlist.indexOfFirst { it["id"] == songId }
if (existingIndex == -1) {
playlist.add(song)
@ -213,7 +218,7 @@ class MusicService : MediaBrowserServiceCompat() {
} else {
currentIndex = existingIndex
}
play(song)
}
}
@ -238,6 +243,10 @@ class MusicService : MediaBrowserServiceCompat() {
seekTo(position)
}
}
"setVolume" -> {
val volume = intent.getFloatExtra("volume", 1.0f)
setVolume(volume)
}
}
return START_STICKY
}
@ -267,6 +276,14 @@ class MusicService : MediaBrowserServiceCompat() {
Log.i(TAG, "收到 MEDIA_NEXT 广播")
playNext()
}
"com.yunqiinnovation.music_service.AI_SAY_START" -> {
Log.i(TAG, "收到 AI_SAY_START 广播 降低播放音量")
reduceVolume()
}
"com.yunqiinnovation.music_service.AI_SAY_END" -> {
Log.i(TAG, "收到 AI_SAY_START 广播 降低播放音量")
restoreVolume()
}
// 你也可以扩展支持更多指令
}
}
@ -427,19 +444,7 @@ class MusicService : MediaBrowserServiceCompat() {
return PendingIntent.getBroadcast(this, action.toInt(), intent, flags)
}
// fun mediaButtonPendingIntent(action: Long): PendingIntent {
// val intent = Intent(this, MusicService::class.java).apply {
// this.action = Intent.ACTION_MEDIA_BUTTON
// putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode()))
// }
// val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
// } else {
// PendingIntent.FLAG_UPDATE_CURRENT
// }
// return PendingIntent.getService(this, action.toInt(), intent, flags)
// }
// 转换 PlaybackStateCompat.ACTION 到 KeyEvent
fun Long.toKeyCode(): Int = when (this) {
PlaybackStateCompat.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY
PlaybackStateCompat.ACTION_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE
@ -447,6 +452,43 @@ class MusicService : MediaBrowserServiceCompat() {
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
else -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
}
/**
* 设置播放音量
* @param volume 音量值,范围 0.0f - 1.0f
*/
private fun setVolume(volume: Float) {
val clampedVolume = volume.coerceIn(0.0f, 1.0f)
currentVolume = clampedVolume
mediaPlayer?.setVolume(clampedVolume, clampedVolume)
Log.i(TAG, "设置音量: $clampedVolume")
}
/**
* 降低音量(AI说话时)
*/
private fun reduceVolume() {
if (!isVolumeReduced) {
originalVolume = currentVolume
val reducedVolume = (currentVolume * 0.3f).coerceIn(0.0f, 1.0f) // 降低到30%
setVolume(reducedVolume)
isVolumeReduced = true
Log.i(TAG, "AI说话开始,音量从 $originalVolume 降低到 $reducedVolume")
}
}
/**
* 恢复原始音量(AI说话结束时)
*/
private fun restoreVolume() {
if (isVolumeReduced) {
setVolume(originalVolume)
isVolumeReduced = false
Log.i(TAG, "AI说话结束,音量恢复到 $originalVolume")
}
}
/**
* 设置播放状态
*/
@ -601,3 +643,4 @@ class MusicService : MediaBrowserServiceCompat() {
override fun onBind(intent: Intent?): IBinder? = null
}

Loading…
Cancel
Save