Browse Source

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

newdev_shunjiawei
lxm 1 year ago
parent
commit
4e95ae6b9f
  1. 33
      lib/core/utils/synchrodata.dart
  2. 6
      lib/data/services/speech_impl/azure_tts_service.dart
  3. 2
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  4. 4
      lib/data/services/tts_service.dart
  5. 62
      lib/modules/explore/views/explore_view.dart
  6. 101
      lib/modules/meeting/views/generate_bottom_sheet.dart
  7. 14
      lib/modules/meeting/views/meeting_details_view.dart
  8. 1075
      lib/modules/meeting/views/meeting_view.dart
  9. 92
      lib/modules/meeting/views/more_bottom_sheet.dart
  10. 85
      lib/modules/translation/controllers/translation_controller.dart
  11. 20
      lib/modules/translation/views/translation_list_view.dart
  12. 8
      lib/modules/translation/views/translation_view.dart
  13. 13
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  14. 14
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  15. 1
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  16. 110
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  17. 2
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

33
lib/core/utils/synchrodata.dart

@ -8,7 +8,11 @@ import '../../data/services/network/api.dart';
class Synchrodata { class Synchrodata {
static final GetStorage _storage = GetStorage(); static final GetStorage _storage = GetStorage();
static List _records = _storage.read("operation_records") ?? []; static List<Map<String, dynamic>> _records =
(_storage.read("operation_records") as List?)
?.map((e) => Map<String, dynamic>.from(e as Map))
.toList() ??
[];
static List _delids = _storage.read("operation_delids") ?? []; static List _delids = _storage.read("operation_delids") ?? [];
static bool _isSynchro = false; static bool _isSynchro = false;
@ -30,7 +34,7 @@ class Synchrodata {
switch (method) { switch (method) {
case 'insert': case 'insert':
_records.add({ _records.add({
...params, ...Map<String, dynamic>.from(params),
'id': id, 'id': id,
'number': params['id'], 'number': params['id'],
'uid': userId, 'uid': userId,
@ -38,19 +42,19 @@ class Synchrodata {
_storage.write("operation_records", _records); _storage.write("operation_records", _records);
break; break;
case 'update': case 'update':
Map? foundElement = _records.firstWhere( Map<String, dynamic>? foundElement = _records.firstWhere(
(element) => element['id'] == id, (element) => element['id'] == id,
orElse: () => null, orElse: () => {},
); );
if (foundElement != null) { if (foundElement.isNotEmpty) {
foundElement.addAll({ foundElement.addAll({
...params, ...Map<String, dynamic>.from(params),
'id': id, 'id': id,
'number': params['id'], 'number': params['id'],
}); });
} else { } else {
_records.add({ _records.add({
...params, ...Map<String, dynamic>.from(params),
'id': id, 'id': id,
'number': params['id'], 'number': params['id'],
'uid': userId, 'uid': userId,
@ -75,15 +79,18 @@ class Synchrodata {
// 获取需要修改的全部数据 // 获取需要修改的全部数据
List putRecords = []; List putRecords = [];
for (var element in _records) { for (var element in _records) {
final result = await SqfliteApi.getMeetingDetails(element['number']); Map result = {'details': {}};
final speakerList = List speakerList = [];
await SqfliteApi.getMeetingSpeaker(element['number']); if (!_delids.contains(element['id'])) {
result = await SqfliteApi.getMeetingDetails(element['number']);
speakerList = await SqfliteApi.getMeetingSpeaker(element['number']);
}
putRecords.add({ putRecords.add({
...result, ...Map<String, dynamic>.from(result),
...result['details'], ...Map<String, dynamic>.from(result['details']),
'rtype': result['type'], 'rtype': result['type'],
'original': jsonEncode(speakerList), 'original': jsonEncode(speakerList),
...element, ...Map<String, dynamic>.from(element),
}); });
} }
var result = await Api.putOperationRecord({ var result = await Api.putOperationRecord({

6
lib/data/services/speech_impl/azure_tts_service.dart

@ -415,10 +415,12 @@ class AzureTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> setAudioOutputDevice() async { Future<bool> setAudioOutputDevice(int type) async {
try { try {
// 开始合成,传递参数 // 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice'); final result = await _channel.invokeMethod('setAudioOutputDevice', {
'type': type,
});
return result; return result;
} catch (e) { } catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}'); Logger.error('音频输出设备类型失败: ${e.toString()}');

2
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -392,7 +392,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> setAudioOutputDevice() { Future<bool> setAudioOutputDevice(int type) {
// TODO: implement setAudioOutputDevice // TODO: implement setAudioOutputDevice
throw UnimplementedError(); throw UnimplementedError();
} }

4
lib/data/services/tts_service.dart

@ -33,7 +33,9 @@ abstract class TtsService {
/// 释放资源 /// 释放资源
Future<void> dispose(); Future<void> dispose();
Future<bool> setAudioOutputDevice();
/// 切换音频输出设备
Future<bool> setAudioOutputDevice(int type);
} }
/// TTS事件类型 /// TTS事件类型

62
lib/modules/explore/views/explore_view.dart

@ -126,37 +126,37 @@ class ExploreView extends GetView<ExploreController> {
], ],
), ),
// 搜索框 // // 搜索框
SizedBox(height: 16.h), // SizedBox(height: 16.h),
Container( // Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
color: // color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], // isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r), // borderRadius: BorderRadius.circular(12.r),
), // ),
child: TextField( // child: TextField(
controller: controller.searchController, // controller: controller.searchController,
decoration: InputDecoration( // decoration: InputDecoration(
hintText: 'searchAi'.tr, // 搜索AI功能... // hintText: 'searchAi'.tr, // 搜索AI功能...
hintStyle: TextStyle( // hintStyle: TextStyle(
fontSize: 14.sp, // fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400], // color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
), // ),
prefixIcon: Icon( // prefixIcon: Icon(
Icons.search, // Icons.search,
size: 20.sp, // size: 20.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400], // color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
), // ),
border: InputBorder.none, // border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h), // contentPadding: EdgeInsets.symmetric(vertical: 12.h),
), // ),
textInputAction: TextInputAction.search, // textInputAction: TextInputAction.search,
onSubmitted: (_) => controller.search(), // onSubmitted: (_) => controller.search(),
style: TextStyle( // style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, // color: isDarkMode ? Colors.white : Colors.black,
), // ),
), // ),
), // ),
], ],
), ),
); );

101
lib/modules/meeting/views/generate_bottom_sheet.dart

@ -9,7 +9,9 @@ import 'record_language_bottom_sheet.dart';
import 'template_view.dart'; import 'template_view.dart';
class GenerateBottomSheet extends StatefulWidget { class GenerateBottomSheet extends StatefulWidget {
const GenerateBottomSheet({super.key}); final bool isSummarizeAgain;
const GenerateBottomSheet({super.key, this.isSummarizeAgain = false});
@override @override
State<GenerateBottomSheet> createState() => _GenerateBottomSheetState(); State<GenerateBottomSheet> createState() => _GenerateBottomSheetState();
@ -214,56 +216,57 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
}, },
), ),
), ),
Container( if (!widget.isSummarizeAgain)
margin: EdgeInsets.symmetric(horizontal: 12.w), Container(
decoration: BoxDecoration( margin: EdgeInsets.symmetric(horizontal: 12.w),
color: Colors.white, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r), color: Colors.white,
), borderRadius: BorderRadius.circular(8.r),
child: Column( ),
children: [ child: Column(
_row( children: [
'区分说话人', _row(
widget: Transform.scale( '区分说话人',
scale: 0.8, widget: Transform.scale(
alignment: Alignment.centerRight, scale: 0.8,
child: Switch( alignment: Alignment.centerRight,
value: _isSpeaker, child: Switch(
onChanged: (bool value) { value: _isSpeaker,
setState(() { onChanged: (bool value) {
_isSpeaker = value; setState(() {
}); _isSpeaker = value;
}, });
},
),
), ),
), ),
), _row(
_row( '录音语言',
'录音语言', value: _languageText,
value: _languageText, isBorder: false,
isBorder: false, onTap: () {
onTap: () { Get.bottomSheet(
Get.bottomSheet( RecordLanguageBottomSheet(_language),
RecordLanguageBottomSheet(_language), isScrollControlled: true,
isScrollControlled: true, backgroundColor: Colors.grey[100],
backgroundColor: Colors.grey[100], shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(
borderRadius: BorderRadius.vertical( top: Radius.circular(12.r),
top: Radius.circular(12.r), ),
), ),
), ).then((value) {
).then((value) { if (value != null) {
if (value != null) { setState(() {
setState(() { _languageText = value['name'];
_languageText = value['name']; _language = value['value'];
_language = value['value']; });
}); }
} });
}); },
}, ),
), ],
], ),
), ),
),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
child: GestureDetector( child: GestureDetector(
@ -279,6 +282,10 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
_storage.write("meeting_last_template_Data", _templateData); _storage.write("meeting_last_template_Data", _templateData);
_storage.write("meeting_template_list", _templateList); _storage.write("meeting_template_list", _templateList);
Get.back(); Get.back();
if (widget.isSummarizeAgain) {
_controller.meetingDetails.value.tasktype = 2;
_controller.meetingDetails.refresh();
}
_controller.summaryCreate(); _controller.summaryCreate();
}, },
child: Container( child: Container(

14
lib/modules/meeting/views/meeting_details_view.dart

@ -10,6 +10,7 @@ import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_details_controller.dart'; import '../controllers/meeting_details_controller.dart';
import 'mind_map_tab.dart'; import 'mind_map_tab.dart';
import 'more_bottom_sheet.dart';
import 'overview_tab.dart'; import 'overview_tab.dart';
import 'share_bottom_sheet.dart'; import 'share_bottom_sheet.dart';
import 'speech_tab.dart'; import 'speech_tab.dart';
@ -189,7 +190,18 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
size: 16.sp, size: 16.sp,
), ),
), ),
onPressed: () {}, onPressed: () {
Get.bottomSheet(
const MoreBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
), ),
], ],
); );

1075
lib/modules/meeting/views/meeting_view.dart

File diff suppressed because it is too large

92
lib/modules/meeting/views/more_bottom_sheet.dart

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_controller.dart';
import '../controllers/meeting_details_controller.dart';
import 'generate_bottom_sheet.dart';
class MoreBottomSheet extends StatefulWidget {
const MoreBottomSheet({super.key});
@override
State<MoreBottomSheet> createState() => _MoreBottomSheetState();
}
class _MoreBottomSheetState extends State<MoreBottomSheet> {
final _controller = Get.find<MeetingController>();
final _detailsController = Get.find<MeetingDetailsController>();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10.w),
if (_detailsController.meetingDetails.value.tasktype == 3)
_shareItem(Icons.refresh, '重新总结', onTap: () {
Get.back();
Get.bottomSheet(
const GenerateBottomSheet(
isSummarizeAgain: true,
),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
}),
_shareItem(Icons.delete_outline, '删除', onTap: () async {
_controller.deleteMeeting(
_detailsController.meetingData.value.id,
_detailsController.meetingData.value.filepath,
);
Get.back();
Get.back();
}),
],
),
);
}
Widget _shareItem(
IconData icon,
String title, {
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(
icon,
color: Colors.black87,
),
10.horizontalSpace,
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
),
],
),
),
);
}
}

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

@ -17,6 +17,13 @@ import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举
enum AudioOutputDevice {
defaultDevice, // 默认(如有耳机优先耳机,否则扬声器)
headphones, // 强制耳机
speaker, // 强制扬声器
}
class TranslationController extends GetxController { class TranslationController extends GetxController {
// 服务 // 服务
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
@ -55,6 +62,7 @@ class TranslationController extends GetxController {
final appDocDir = getApplicationDocumentsDirectory(); final appDocDir = getApplicationDocumentsDirectory();
var appDir; var appDir;
var dir; var dir;
Future<void> changeTranslationMode(String mode) async { Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode; currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode); currentModeTitle.value = _getModeTitle(mode);
@ -104,7 +112,7 @@ class TranslationController extends GetxController {
//_setupEventListeners(); //_setupEventListeners();
// 加载上次保存的语言设置 // 加载上次保存的语言设置
_loadSavedLanguages(); _loadSavedLanguages();
_requestStoragePermission();
// 初始化语言代码 // 初始化语言代码
final sourceAsrCode = final sourceAsrCode =
_languageManager.getAsrCodeByChineseName(sourceLanguage.value); _languageManager.getAsrCodeByChineseName(sourceLanguage.value);
@ -175,25 +183,27 @@ class TranslationController extends GetxController {
} }
// 请求存储权限 // 请求存储权限
void _requestStoragePermission() async { Future<bool> _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!; // final BuildContext context = Get.overlayContext!;
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
);
PermissionUtil.instance
.requestPermission(
// context: context,
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
if (granted) { if (granted) {
Logger.d("Permission", "用户授予了存储权限"); Logger.d("Permission", "用户授予了存储权限");
return true;
} else { } else {
Logger.d("Permission", "用户未授予存储权限"); Logger.d("Permission", "用户未授予存储权限");
// 可以提示用户无法进行文件操作 return false;
} }
}); } catch (e) {
Logger.e("Permission", "请求存储权限时出错: $e");
return false;
}
} }
// /// 设置事件监听器 // /// 设置事件监听器
// void _setupEventListeners() { // void _setupEventListeners() {
@ -224,6 +234,11 @@ class TranslationController extends GetxController {
await _asrService.initialize( await _asrService.initialize(
supportedLanguages: asrSupportedLanguages, supportedLanguages: asrSupportedLanguages,
); );
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
appDir = await appDocDir; // 先 await 获取目录 appDir = await appDocDir; // 先 await 获取目录
dir = Directory("${appDir.path}/TranslatAudio"); dir = Directory("${appDir.path}/TranslatAudio");
if (!await dir.exists()) { if (!await dir.exists()) {
@ -266,10 +281,10 @@ class TranslationController extends GetxController {
Future<void> startRecognition() async { Future<void> startRecognition() async {
Logger.info('开始语音识别'); Logger.info('开始语音识别');
// 等待权限请求完成并获取结果 // 等待权限请求完成并获取结果
final bool granted = await _requestRecordPermission(); final bool record = await _requestRecordPermission();
// 如果权限被拒绝,直接返回 // 如果权限被拒绝,直接返回
if (!granted) { if (!record) {
Logger.d("Permission", "录音权限被拒绝,无法开始录音"); Logger.d("Permission", "录音权限被拒绝,无法开始录音");
return; return;
} }
@ -328,6 +343,11 @@ class TranslationController extends GetxController {
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
isRecognizing.value = true; isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) { if (isRecording.value && isRecognizing.value) {
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
final formattedTime = final formattedTime =
DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord( await _asrService.enableRecord(
@ -516,15 +536,32 @@ class TranslationController extends GetxController {
detectedSourceLanguageCode; detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode = translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode; detectedTargetLanguageCode;
translationHistory.refresh(); print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}");
saveTranslationHistory(); // 判断是否是面对面模式且语言一致
// 更新后滚动到底部 final bool isFaceToFaceSameLang =
_scrollToBottom(); detectedSourceLanguageCode == sourceLanguageCode.value &&
currentMode.value == "faceToFace";
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) { // 设置音频输出设备
// Logger.info('播放翻译文本: ${translationResult}'); var isSuccess = await _ttsService.setAudioOutputDevice(
await playTranslatedText(translationResult); 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);
}
} else {
print('设置音频输出设备失败');
} }
} else { } else {
Logger.info( Logger.info(

20
lib/modules/translation/views/translation_list_view.dart

@ -278,15 +278,16 @@ class TranslationListView extends GetView<TranslationListController> {
), ),
_popupMenuEntry( _popupMenuEntry(
Icon( Icon(
Icons.delete_outline, Icons.smart_button,
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red, color: isDarkMode ? Colors.white : Colors.black87,
), ),
'删除', '智能转写',
isDarkMode ? Colors.red[700]! : Colors.red, isDarkMode ? Colors.white : Colors.black87,
() { () {
file.delete(); Get.toNamed(Routes.meeting, arguments: {
controller.loadTranslatAudio(); // 刷新列表 'filePath': file.path,
});
}, },
), ),
_popupMenuEntry( _popupMenuEntry(
@ -295,12 +296,11 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red, color: isDarkMode ? Colors.red[700] : Colors.red,
), ),
'导入', '删除',
isDarkMode ? Colors.red[700]! : Colors.red, isDarkMode ? Colors.red[700]! : Colors.red,
() { () {
Get.toNamed(Routes.meeting, arguments: { file.delete();
'filePath': file.path, controller.loadTranslatAudio(); // 刷新列表
});
}, },
), ),
], ],

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

@ -110,7 +110,9 @@ class TranslationView extends GetView<TranslationController> {
// TTS toggle button // TTS toggle button
Obx(() => IconButton( Obx(() => IconButton(
icon: Icon( icon: Icon(
controller.isRecording.value ? Icons.mic : Icons.mic_off, controller.isRecording.value
? Icons.mic
: Icons.fiber_manual_record,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp, size: 24.sp,
), ),
@ -122,7 +124,7 @@ class TranslationView extends GetView<TranslationController> {
// 历史记录按钮 // 历史记录按钮
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.history, Icons.folder,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp, size: 24.sp,
), ),
@ -134,7 +136,7 @@ class TranslationView extends GetView<TranslationController> {
// 返回时不自动开始翻译,只需重新设置滚动监听器 // 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener(); controller.setupScrollListener();
}, },
tooltip: '查看历史记录', tooltip: '查看录音文件',
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(

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

@ -147,13 +147,14 @@ class AzureAsrHelper(private val context: Context) {
} }
// 录音文件类 // 录音文件类
recordfile = RecordFile; recordfile = RecordFile;
//进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请
// 创建识别器 // 创建识别器
val setupSuccess = setupRecognizer() // val setupSuccess = setupRecognizer()
if (setupSuccess) { // if (setupSuccess) {
// 优化:初始化完成后进行预热 // // 优化:初始化完成后进行预热
warmupRecognizer() // warmupRecognizer()
} // }
return setupSuccess return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "初始化失败: ${e.message}") Log.e(tag, "初始化失败: ${e.message}")
return false return false

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

@ -502,7 +502,19 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
result.success(true) result.success(true)
} }
"setAudioOutputDevice" -> { "setAudioOutputDevice" -> {
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) 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)
}
result.success(true) result.success(true)
} }

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

@ -213,6 +213,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun setAudioOutputDevice(device: AudioOutputDevice) { override fun setAudioOutputDevice(device: AudioOutputDevice) {
// Azure TTS使用系统默认的音频路由 // Azure TTS使用系统默认的音频路由
// 音频输出设备的控制需要通过Android的AudioManager实现 // 音频输出设备的控制需要通过Android的AudioManager实现
FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
audioManager?.let { manager -> audioManager?.let { manager ->
when (device) { when (device) {

110
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt

@ -15,12 +15,13 @@ import com.deep_voice.speech.tts.AudioDataListener
import com.deep_voice.speech.tts.AudioOutputDevice import com.deep_voice.speech.tts.AudioOutputDevice
/** /**
* 高性能音频播放器 - 16kHz采样率 * 语音通信音频播放器 - 16kHz采样率
* 设计要点: * 设计要点:
* • 永远使用 USAGE_MEDIA + MODE_NORMAL 媒体播放通路 * • 使用 USAGE_VOICE_COMMUNICATION + MODE_IN_COMMUNICATION 通信通路
* • 利用 AudioDeviceCallback 监听设备热插拔 * • 利用 AudioDeviceCallback 监听设备热插拔
* • API 31+ 使用 setPreferredDevice() 精准路由 * • API 31+ 使用 setPreferredDevice() 精准路由
* • 有耳机自动从耳机播放,无耳机自动切到扬声器 * • 有耳机自动从耳机播放,无耳机自动切到扬声器
* • 内置回声消除和噪音抑制功能
*/ */
class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() { class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() {
companion object { companion object {
@ -35,6 +36,35 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
private var isFirstData = true private var isFirstData = true
private var totalBytesWritten = 0 private var totalBytesWritten = 0
private var sessionActive = true private var sessionActive = true
private var audioFocusRequest: Any? = null // 兼容不同API版本
// 音频焦点变化监听器
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久失去焦点,停止播放
Log.d(TAG, "音频焦点丢失")
stop()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时失去焦点,暂停播放
Log.d(TAG, "音频焦点暂时丢失")
audioTrack?.pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 暂时失去焦点但可降低音量,这里选择暂停
Log.d(TAG, "音频焦点暂时丢失(可降低音量)")
audioTrack?.pause()
}
AudioManager.AUDIOFOCUS_GAIN -> {
// 重新获得焦点,恢复播放
Log.d(TAG, "音频焦点恢复")
if (sessionActive) {
audioTrack?.play()
}
}
}
}
// 回调 // 回调
private var onPlayStarted: (() -> Unit)? = null private var onPlayStarted: (() -> Unit)? = null
@ -62,6 +92,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
init { init {
// 注册设备热插拔监听 // 注册设备热插拔监听
audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper())) audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper()))
// 请求音频焦点
requestAudioFocus()
buildTrack(selectOutputDevice()) // 首次创建 buildTrack(selectOutputDevice()) // 首次创建
} }
@ -126,7 +158,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
audioTrack = AudioTrack.Builder() audioTrack = AudioTrack.Builder()
.setAudioAttributes( .setAudioAttributes(
AudioAttributes.Builder() AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) // 统一媒体通路 .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 语音通信通路
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build() .build()
) )
@ -159,8 +191,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
track.play() track.play()
} }
// 配置音频管理器 - 统一使用 MODE_NORMAL // 配置音频管理器 - 使用通信模式以启用回声消除和噪音抑制
audioManager.mode = AudioManager.MODE_NORMAL audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
// 切换扬声器开关仅在无耳机时打开 // 切换扬声器开关仅在无耳机时打开
audioManager.isSpeakerphoneOn = (device == null) audioManager.isSpeakerphoneOn = (device == null)
@ -168,6 +200,71 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz") Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz")
} }
/**
* 请求音频焦点
*/
private fun requestAudioFocus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android O及以上使用新的API
val focusRequest = android.media.AudioFocusRequest.Builder(
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build()
val result = audioManager.requestAudioFocus(focusRequest)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusRequest = focusRequest
Log.d(TAG, "音频焦点请求成功")
} else {
Log.w(TAG, "音频焦点请求失败")
}
} else {
// Android O以下使用旧的API
@Suppress("DEPRECATION")
val result = audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "音频焦点请求成功(旧API)")
} else {
Log.w(TAG, "音频焦点请求失败(旧API)")
}
}
} catch (e: Exception) {
Log.e(TAG, "请求音频焦点异常: ${e.message}")
}
}
/**
* 释放音频焦点
*/
private fun releaseAudioFocus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let {
audioManager.abandonAudioFocusRequest(it as android.media.AudioFocusRequest)
}
} else {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(audioFocusChangeListener)
}
Log.d(TAG, "音频焦点已释放")
} catch (e: Exception) {
Log.e(TAG, "释放音频焦点异常: ${e.message}")
}
}
/** /**
* 开始新的播放会话 * 开始新的播放会话
*/ */
@ -262,7 +359,10 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
stop() stop()
audioTrack?.release() audioTrack?.release()
audioTrack = null audioTrack = null
releaseAudioFocus()
audioManager.unregisterAudioDeviceCallback(this) audioManager.unregisterAudioDeviceCallback(this)
// 恢复音频模式
audioManager.mode = AudioManager.MODE_NORMAL
} }
/** /**

2
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

@ -116,7 +116,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
// TTS配置参数 // TTS配置参数
private var appId: String = "" private var appId: String = ""
private var accessToken: String = "" private var accessToken: String = ""
private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 private var speaker: String = "zh_female_yuanqinvyou_moon_bigtts" // 默认语音,使用Java示例的音色
private var format: String = "pcm" // pcm 格式 private var format: String = "pcm" // pcm 格式
private var sampleRate: Int = 16000 // 使用16kHz采样率 private var sampleRate: Int = 16000 // 使用16kHz采样率
private var enableTimestamp: Boolean = false private var enableTimestamp: Boolean = false

Loading…
Cancel
Save