From a369c647bd3bf97b3904e600fe38ea5eb01b80cf Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 17 Jun 2025 10:25:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BD=95=E9=9F=B3=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/asr_service.dart | 10 +- .../speech_impl/azure_asr_service.dart | 42 ++- .../speech_impl/volcano_asr_api_service.dart | 16 +- .../speech_impl/volcano_asr_service.dart | 16 +- .../speech_impl/xunfei_asr_service.dart | 16 +- .../controllers/meeting_controller.dart | 131 +++++++-- .../meeting_record_controller.dart | 92 +++++-- .../meeting/views/meeting_record_view.dart | 199 +++++++++++++- lib/modules/meeting/views/meeting_view.dart | 18 +- .../controllers/translation_controller.dart | 42 ++- .../azure_speech/AzureAsrHelper.kt | 205 +++----------- .../azure_speech/AzureSpeechPlugin.kt | 41 ++- .../azure_speech/tools/RecordFile.kt | 251 ++++++++++++++++++ 13 files changed, 833 insertions(+), 246 deletions(-) create mode 100644 local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 6d09e8744..a9a9e6b52 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -30,13 +30,19 @@ abstract class AsrService { bool isContinuousRecognitionActive(); /// 开始录音 - Future enableRecord(String fileName); + Future enableRecord(String filePath); /// 暂停录音 Future pauseRecord(); + // /// 移动文件到新路径 + Future moveFile(String sourcePath, String destPath); + + // /// 重命名指定路径的音频文件 + Future renameFile(String filePath, String newName); + /// 结束录音 - Future stopRecord(); + Future stopRecord(bool isSave); /// 释放资源 Future dispose(); diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 317836638..39ac44aa9 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -344,10 +344,10 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String fileName) async { + Future enableRecord(String filePath) async { try { final bool result = await _channel.invokeMethod('enableRecord', { - 'fileName': fileName, + 'filePath': filePath, }); return result; @@ -358,9 +358,11 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future stopRecord() async { + Future stopRecord(bool isSave) async { try { - final bool result = await _channel.invokeMethod('stopRecord'); + final bool result = await _channel.invokeMethod('stopRecord', { + 'isSave': isSave, + }); return result; } catch (e) { @@ -380,4 +382,36 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } + + @override + Future moveFile(String sourcePath, String destPath) async { + // TODO: implement moveFile + try { + final bool result = await _channel.invokeMethod('moveFile', { + 'sourcePath': sourcePath, + 'destPath': destPath, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future renameFile(String filePath, String newName) async { + // TODO: implement renameFile + try { + final bool result = await _channel.invokeMethod('renameFile', { + 'filePath': filePath, + 'newName': newName, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } } diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 50cb85f12..a15a4ae0b 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -813,13 +813,13 @@ class VolcanoAsrApiService implements AsrService { } @override - Future enableRecord(String fileName) { + Future enableRecord(String filePath) { // TODO: implement enableRecord throw UnimplementedError(); } @override - Future stopRecord() { + Future stopRecord(bool isSave) { // TODO: implement stopRecord throw UnimplementedError(); } @@ -829,4 +829,16 @@ class VolcanoAsrApiService implements AsrService { // TODO: implement pauseRecord throw UnimplementedError(); } + + @override + Future moveFile(String sourcePath, String destPath) { + // TODO: implement moveFile + throw UnimplementedError(); + } + + @override + Future renameFile(String filePath, String newName) { + // TODO: implement renameFile + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 923ede8f0..8571f9c4c 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -348,13 +348,13 @@ class VolcanoAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String fileName) { + Future enableRecord(String filePath) { // TODO: implement enableRecord throw UnimplementedError(); } @override - Future stopRecord() { + Future stopRecord(bool isSave) { // TODO: implement stopRecord throw UnimplementedError(); } @@ -364,4 +364,16 @@ class VolcanoAsrService extends GetxService implements AsrService { // TODO: implement pauseRecord throw UnimplementedError(); } + + @override + Future moveFile(String sourcePath, String destPath) { + // TODO: implement moveFile + throw UnimplementedError(); + } + + @override + Future renameFile(String filePath, String newName) { + // TODO: implement renameFile + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 4cd8ac171..f4d77ae50 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -272,13 +272,13 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String fileName) { + Future enableRecord(String filePath) { // TODO: implement enableRecord throw UnimplementedError(); } @override - Future stopRecord() { + Future stopRecord(bool isSave) { // TODO: implement stopRecord throw UnimplementedError(); } @@ -288,4 +288,16 @@ class XunfeiAsrService extends GetxService implements AsrService { // TODO: implement pauseRecord throw UnimplementedError(); } + + @override + Future moveFile(String sourcePath, String destPath) { + // TODO: implement moveFile + throw UnimplementedError(); + } + + @override + Future renameFile(String filePath, String newName) { + // TODO: implement renameFile + throw UnimplementedError(); + } } diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index f6d1d7ee9..5fc7b6ebc 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -28,7 +28,7 @@ class MeetingController extends GetxController { int addQuantity = 0; List selected = []; - final appDocDir = getApplicationSupportDirectory(); + final appDocDir = getApplicationDocumentsDirectory(); // final directory = Directory( // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); List files = []; @@ -56,6 +56,7 @@ class MeetingController extends GetxController { 'Content-Type': 'application/json', }; _getDataList(); + addMeetingAudio(); } @override @@ -127,22 +128,34 @@ class MeetingController extends GetxController { } void loadTranslatAudio() async { - final appDir = await appDocDir; // 先 await 获取目录 - final dir = Directory("${appDir.path}/TranslatAudio"); - - print("appDocDir=${dir?.path}"); + try { + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/TranslatAudio"); + + print("appDocDir=${dir?.path}"); + // Create directory if it doesn't exist + if (!await dir.exists()) { + await dir.create(recursive: true); // Create directory tree if needed + files = []; // No files exist in new directory + return; + } - files = dir - .listSync() - .where((f) => - f is File && - (f.path.endsWith('.mp3') || - f.path.endsWith('.wav') || - f.path.endsWith('.aac') || - f.path.endsWith('.m4a') || - f.path.endsWith('.mp4') || - f.path.endsWith('.mov'))) - .toList(); + // Directory exis + files = dir + .listSync() + .where((f) => + f is File && + (f.path.endsWith('.mp3') || + f.path.endsWith('.wav') || + f.path.endsWith('.aac') || + f.path.endsWith('.m4a') || + f.path.endsWith('.mp4') || + f.path.endsWith('.mov'))) + .toList(); + } catch (e) { + print("Error loading files: $e"); + files = []; // Return empty list on error + } } // 导入音频文件 @@ -166,6 +179,92 @@ class MeetingController extends GetxController { } } + void addMeetingAudio() async { + EasyLoading.show(status: '正在导入本地录音...'); + try { + final appDir = await appDocDir; + final sourceDir = Directory("${appDir.path}/MeetingAudio/"); + + // 检查源目录是否存在 + if (!await sourceDir.exists()) { + EasyLoading.dismiss(); + return; + } + + // 创建目标目录(如果不存在) + final destDir = Directory("${appDir.path}/LocalAudio/"); + if (!await destDir.exists()) { + await destDir.create(recursive: true); + } + + // 获取目录下所有文件 + final files = + await sourceDir.list().where((entity) => entity is File).toList(); + + int addQuantity = 0; + for (final fileEntity in files) { + try { + final file = File(fileEntity.path); + final filePath = file.path; + + // 设置音频源获取时长 + final _audioPlayer = AudioPlayer(); + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + + // 获取文件修改时间 + final fileStat = await file.stat(); + final creationTime = fileStat.changed.millisecondsSinceEpoch; + + // 上传OSS + final audiourl = + await UploadOss.upload(filepath: filePath, rootDir: 'LocalAudio'); + + // 准备新路径 + final fileName = filePath.split('/').last; + final newPath = "${destDir.path}/$fileName"; + + // 构建数据 + Map data = { + 'title': fileName, + 'type': 'LOCAL', + 'seconds': seconds, + 'filepath': newPath, // 存储新路径 + 'audiourl': audiourl, + 'tasktype': 0, + 'creationtime': creationTime, + }; + print("导入数据: $data"); + + // 插入数据库 + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + // 更新UI数据 + dataList.insert(0, { + 'id': meetingId, + 'taskid': '', + ...data, + }); + + // 移动文件 + await file.copy(newPath); + await file.delete(); + addQuantity++; + } + } catch (e) { + print("文件处理失败: ${fileEntity.path}, 错误: $e"); + } finally { + await _audioPlayer.dispose(); // 释放播放器资源 + } + } + if (addQuantity > 0) EasyLoading.showSuccess('成功导入本地 $addQuantity 条录音'); + } catch (e) { + EasyLoading.showError('导入失败: $e'); + } finally { + EasyLoading.dismiss(); + } + } + void addTranslatAudio() async { EasyLoading.show(status: '正在批量导入...'); for (final filePath in selected) { diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 1ad18656f..b1bb68481 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -1,9 +1,12 @@ // meeting_record_controller.dart import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/ble_manager.dart'; @@ -46,24 +49,66 @@ class MeetingRecordController extends GetxController int _elapsedMilliseconds = 0; // 累计毫秒数 int? _lastStartTime; // 上次开始计时的时间戳 + final newName = "".obs; + final fileName = "初始文件名".obs; + final isEditingTitle = false.obs; + late TextEditingController titleEditingController; + late FocusNode titleFocusNode; + final appDocDir = getApplicationDocumentsDirectory(); @override void onInit() { super.onInit(); - _asrService.initialize(); - tabController = TabController(length: 2, vsync: this); + tabController = TabController(length: 2, vsync: this); // Add this line + titleEditingController = TextEditingController(text: fileName.value); + titleFocusNode = FocusNode(); + + // 监听焦点变化 + titleFocusNode.addListener(() { + if (!titleFocusNode.hasFocus && isEditingTitle.value) { + saveTitleChange(); + } + }); + final Map args = Get.arguments ?? {}; + audioTypes.value = args['audioTypes']; + // 蓝牙处理 + if (audioTypes.value == 0) { + fileName.value = "现场录音"; + } else if (audioTypes.value == 1) { + fileName.value = "音、视频录音"; + } else if (audioTypes.value == 2) { + fileName.value = "通话录音"; + } + newName.value = fileName.value; generateWaveData(); - // 移除初始化的 startTimer() 调用 } @override void onClose() { - _timer?.cancel(); - _asrService.dispose(); - - tabController.dispose(); + titleEditingController.dispose(); + titleFocusNode.dispose(); super.onClose(); } + void startTitleEditing() { + isEditingTitle.value = true; + titleEditingController.text = fileName.value; + newName.value = fileName.value; + // 延迟请求焦点确保TextField已渲染 + Future.delayed(Duration(milliseconds: 100), () { + titleFocusNode.requestFocus(); + }); + } + + void saveTitleChange() { + if (isEditingTitle.value) { + newName.value = titleEditingController.text; + isEditingTitle.value = false; + + // 这里添加实际的文件重命名逻辑 + // 例如: renameFile(fileName.value); + } + } + /// 启动/继续计时器 void startTimer() { if (timerStatus.value != TimerStatus.running) { @@ -195,16 +240,29 @@ class MeetingRecordController extends GetxController _recognitionSubscription?.cancel(); _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); - await _asrService.enableRecord("录音"); + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/MeetingAudio/"); + if (!await dir.exists()) { + await dir.create(recursive: true); // Add recursive creation + } + print("appDocDir=${dir?.path}"); +// 使用可读性更好的时间格式 + final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); + + await _asrService + .enableRecord("${dir?.path}/${fileName.value}_$formattedTime.wav"); + fileName.value = "${fileName.value}_$formattedTime"; + newName.value = fileName.value; + titleEditingController.text = fileName.value; startTimer(); // 蓝牙处理 - if (audioTypes == 0) { + if (audioTypes.value == 0) { _bleManager.openEncoder(); - } else if (audioTypes == 1) { + } else if (audioTypes.value == 1) { _bleManager.openDecoder(); - } else if (audioTypes == 2) { + } else if (audioTypes.value == 2) { _bleManager.openA2DPDecoder(); } } @@ -225,8 +283,6 @@ class MeetingRecordController extends GetxController _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); startTimer(); - - //await _asrService.resumeRecognition(); // 假设ASR服务有继续方法 } /// 切换是否标记为重点 @@ -247,8 +303,14 @@ class MeetingRecordController extends GetxController Future saveRecording() async { resetTimer(); // 重置计时器 _bleManager.closeCodec(); - await _asrService.stopRecord(); + await _asrService.stopRecord(true); + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/MeetingAudio/"); + if (!await dir.exists()) { + await dir.create(recursive: true); // Add recursive creation + } + await _asrService.renameFile( + "${dir.path}/${fileName.value}.wav", newName.value); await _asrService.stopContinuousRecognition(); - Get.back(); } } diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index f29de80c6..990d99f44 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -3,26 +3,79 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:lottie/lottie.dart'; import '../controllers/meeting_record_controller.dart'; +import '../../../core/theme/app_colors.dart'; class MeetingRecordView extends GetView { const MeetingRecordView({super.key}); @override Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + // 在控制器中添加编辑状态管理 return Scaffold( appBar: AppBar( leading: const BackButton(), - title: const Text('新录音 29', style: TextStyle(fontSize: 18)), // 更新为29 + title: Obx(() { + if (controller.isEditingTitle.value) { + return SizedBox( + height: 40, + child: TextField( + controller: controller.titleEditingController, + focusNode: controller.titleFocusNode, + autofocus: true, + style: TextStyle( + fontSize: 18, + color: isDarkMode ? Colors.white : Colors.black, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + hintText: '输入文件名', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + textInputAction: TextInputAction.done, + onSubmitted: (value) { + controller.saveTitleChange(); + }, + ), + ); + } else { + return GestureDetector( + onTap: () { + controller.startTitleEditing(); + }, + child: Text( + controller.newName.value, + style: const TextStyle(fontSize: 18), + ), + ); + } + }), elevation: 0, - backgroundColor: Colors.white, - foregroundColor: Colors.black, + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + foregroundColor: isDarkMode ? Colors.white : Colors.black, + actions: [ + Obx(() { + if (controller.isEditingTitle.value) { + return IconButton( + icon: const Icon(Icons.check), + onPressed: controller.saveTitleChange, + ); + } + return const SizedBox.shrink(); + }), + ], ), body: Column( children: [ TabBar( controller: controller.tabController, indicatorColor: Colors.red, - labelColor: Colors.black, + labelColor: isDarkMode ? Colors.white : Colors.black, unselectedLabelColor: Colors.grey, tabs: const [ Tab(text: '音频'), @@ -33,8 +86,8 @@ class MeetingRecordView extends GetView { child: TabBarView( controller: controller.tabController, children: [ - _buildAudioTab(context), - _buildTextTab(context), + _buildAudioTab(context, isDarkMode), + _buildTextTab(context, isDarkMode), ], ), ), @@ -43,7 +96,7 @@ class MeetingRecordView extends GetView { ); } - Widget _buildAudioTab(BuildContext context) { + Widget _buildAudioTab(BuildContext context, bool isDarkMode) { return Obx(() => Column( children: [ const SizedBox(height: 24), @@ -77,13 +130,13 @@ class MeetingRecordView extends GetView { style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold), ), const Spacer(), - _buildBottomBar(context), + _buildBottomBar(context, isDarkMode), const SizedBox(height: 32), ], )); } - Widget _buildTextTab(BuildContext context) { + Widget _buildTextTab(BuildContext context, bool isDarkMode) { return Obx(() => Column( children: [ const SizedBox(height: 48), @@ -139,16 +192,18 @@ class MeetingRecordView extends GetView { ), ), const Spacer(), - _buildBottomBar(context), + _buildBottomBar(context, isDarkMode), const SizedBox(height: 32), ], )); } - Widget _buildBottomBar(BuildContext context) { + // 底部操作栏 + Widget _buildBottomBar(BuildContext context, bool isDarkMode) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + // 收藏按钮 GestureDetector( onTap: controller.toggleMarked, child: Obx(() => CircleAvatar( @@ -162,6 +217,7 @@ class MeetingRecordView extends GetView { ), )), ), + // 录音按钮 GestureDetector( onTap: controller.toggleRecording, child: Obx(() => CircleAvatar( @@ -176,8 +232,9 @@ class MeetingRecordView extends GetView { ), )), ), + // 完成按钮 GestureDetector( - onTap: controller.saveRecording, + onTap: () => _bottomSheet(context, isDarkMode), // 改为匿名函数 child: const CircleAvatar( radius: 32, backgroundColor: Colors.white, @@ -191,6 +248,124 @@ class MeetingRecordView extends GetView { ], ); } + +// 底部编辑弹窗 + void _bottomSheet(BuildContext context, bool isDarkMode) { + Get.bottomSheet( + Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 标题 + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Text( + 'editMeetingTitle'.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + + // 输入框 + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white10 : Colors.white, + borderRadius: BorderRadius.circular(12.w), + ), + child: TextField( + controller: controller.titleEditingController, + autofocus: true, // 自动弹出键盘 + maxLength: 50, // 添加字符限制 + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: 'maxCharacters'.tr, + hintStyle: TextStyle( + color: isDarkMode ? Colors.white60 : Colors.black45, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.all(16.w), + counterText: '', // 隐藏默认计数器 + ), + ), + ), + + // 操作按钮 + Padding( + padding: EdgeInsets.only(top: 30.w), + child: Row( + children: [ + // 取消按钮 + Expanded( + child: TextButton( + onPressed: () { + Get.back; + }, + style: TextButton.styleFrom( + foregroundColor: + isDarkMode ? AppColors.primary : Colors.blue, + padding: EdgeInsets.symmetric(vertical: 12.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.w), + side: BorderSide( + color: + isDarkMode ? AppColors.primary : Colors.blue, + width: 2.w, + ), + ), + ), + child: Text('cancel'.tr), + ), + ), + + SizedBox(width: 20.w), + + // 确认按钮 + Expanded( + child: TextButton( + onPressed: () async { + // 先执行保存标题修改 + controller.saveTitleChange(); + // 然后执行保存录音 + await controller.saveRecording(); + // 最后关闭弹窗 + Get.back(); + }, + style: TextButton.styleFrom( + backgroundColor: + isDarkMode ? AppColors.primary : Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 12.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.w), + ), + ), + child: Text('confirm'.tr), + ), + ), + ], + ), + ), + ], + ), + ), + ), + isScrollControlled: true, // 防止键盘遮挡 + backgroundColor: Colors.transparent, // 移除默认背景 + ); + } } class WaveformPainter extends CustomPainter { diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index b377232f7..d7891456c 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -3,13 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; -import 'package:path_provider/path_provider.dart'; import '../../../core/theme/app_colors.dart'; import '../../../routes/app_routes.dart'; import '../controllers/meeting_controller.dart'; import '../model/meeting_model.dart'; -import 'meeting_record_view.dart'; class MeetingView extends GetView { const MeetingView({super.key}); @@ -563,10 +561,12 @@ class MeetingView extends GetView { isDarkMode, icon: Image.asset('assets/images/explore.png', width: 32.w, height: 32.w), - title: '音/视频录音', + title: '现场录音', onTap: () { Navigator.of(context).pop(); - Get.toNamed(Routes.meetingRecord); + Get.toNamed(Routes.meetingRecord, arguments: { + 'audioTypes': 0, + }); // TODO: 实现从文件导入逻辑 }, ), @@ -576,10 +576,12 @@ class MeetingView extends GetView { isDarkMode, icon: Image.asset('assets/images/explore.png', width: 32.w, height: 32.w), - title: '现场录音', + title: '音、视频录音', onTap: () { Navigator.of(context).pop(); - Get.toNamed(Routes.meetingRecord); + Get.toNamed(Routes.meetingRecord, arguments: { + 'audioTypes': 1, + }); // TODO: 实现从相册导入逻辑 }, @@ -604,7 +606,9 @@ class MeetingView extends GetView { title: '通话录音', onTap: () { Navigator.of(context).pop(); - Get.toNamed(Routes.meetingRecord); + Get.toNamed(Routes.meetingRecord, arguments: { + 'audioTypes': 2, + }); // TODO: 实现从其他APP导入逻辑 }, ), diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 5be5a8990..b7e42e44c 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:async'; import 'dart:convert'; import 'package:get_storage/get_storage.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; @@ -46,14 +50,13 @@ class TranslationController extends GetxController { // 翻译历史和当前项 final translationHistory = [].obs; String? currentSessionId; - + final appDocDir = getApplicationDocumentsDirectory(); + var appDir; + var dir; Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); - // 这里添加具体模式切换逻辑 - if (isRecording.value) { - await _asrService.enableRecord(currentModeTitle.value.tr); - } + stopAll(); } @@ -176,7 +179,11 @@ class TranslationController extends GetxController { await _asrService.initialize( supportedLanguages: asrSupportedLanguages, ); - + appDir = await appDocDir; // 先 await 获取目录 + dir = Directory("${appDir.path}/TranslatAudio"); + if (!await dir.exists()) { + await dir.create(recursive: true); // Add recursive creation + } Logger.info('所有服务初始化完成'); } catch (e) { Logger.error('初始化服务失败: ${e.toString()}'); @@ -199,11 +206,15 @@ class TranslationController extends GetxController { // 切换录音功能 Future toggleisRecord() async { isRecording.toggle(); - if (isRecording.value) { - await _asrService.enableRecord(currentModeTitle.value.tr); - } else { - await _asrService.stopRecord(); - } + if (isRecording.value && isRecognizing.value) { + // 使用可读性更好的时间格式 + final formattedTime = + DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); + await _asrService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + } else if (!isRecording.value) { + await _asrService.stopRecord(true); + } else {} } // 开始语音识别 @@ -262,7 +273,12 @@ class TranslationController extends GetxController { final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType); isRecognizing.value = true; - + if (isRecording.value && isRecognizing.value) { + final formattedTime = + DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); + await _asrService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + } // 监听识别结果 _recognitionSubscription?.cancel(); _recognitionSubscription = @@ -500,7 +516,7 @@ class TranslationController extends GetxController { if (isRecognizing.value) { await stopRecognition(); } - + await _asrService.stopRecord(true); _translationDebounceTimer?.cancel(); await _ttsService.stop(); } catch (e) { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index b2680e0c3..24e9f6e82 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -24,15 +24,8 @@ import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit +import com.yunqiinnovation.azure_speech.tools.RecordFile -import java.io.File -import java.io.FileOutputStream -import java.io.RandomAccessFile -import java.text.SimpleDateFormat -import java.util.Date - -import android.os.Handler -import android.os.HandlerThread /** * Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 @@ -70,8 +63,8 @@ class AzureAsrHelper(private val context: Context) { private var originalAudioMode = AudioManager.MODE_NORMAL // 录音文件处理 - var recordfile: RecordingFile? = null - private var isRecording = false + var recordfile: RecordFile? = null + /** * 音频来源类型 @@ -153,7 +146,7 @@ class AzureAsrHelper(private val context: Context) { setProperty("Speech_SegmentationStrategy", "Time") } // 录音文件类 - recordfile = RecordingFile() + recordfile = RecordFile; // 创建识别器 val setupSuccess = setupRecognizer() if (setupSuccess) { @@ -360,10 +353,7 @@ class AzureAsrHelper(private val context: Context) { // 开始连续识别 recognizer?.startContinuousRecognitionAsync() isContinuousRecognitionActive = true - Log.e(tag, "开始连续语音识别 是否录音${isRecording}") - if (isRecording) { - enableRecord() - } + return true } catch (e: Exception) { stopAudioProcessing() @@ -510,7 +500,7 @@ class AzureAsrHelper(private val context: Context) { // 停止音频处理 stopAudioProcessing() // 停止录音 - recordfile!!.closeFile() + recordfile!!.closeFile(true) // 释放recognizer recognizer?.close() recognizer = null @@ -522,7 +512,7 @@ class AzureAsrHelper(private val context: Context) { // 释放音频配置 audioConfig?.close() audioConfig = null - isRecording = false + // 确保状态被重置 isContinuousRecognitionActive = false microphoneStream = null @@ -532,7 +522,7 @@ class AzureAsrHelper(private val context: Context) { speechConfig = null } catch (e: Exception) { // 确保状态被重置 - isRecording = false + isContinuousRecognitionActive = false microphoneStream = null externalAudioStream = null @@ -588,41 +578,55 @@ class AzureAsrHelper(private val context: Context) { /** * 开启录音 */ - fun enableRecord() { + fun enableRecord(filePath: String) { Log.i(tag, "开启录音:") -if( recordfile!!.isPause) -{ - recordfile!!.isPause=false -} -else{ - recordfile!!.closeFile() - isRecording = true - if (isContinuousRecognitionActive) { - recordfile!!.creatingFiles() - } -} + recordfile!!.closeFile(true) + + recordfile!!.creatingFiles(filePath) } + /** + * 移动文件到新路径 + */ + fun moveFile(sourcePath: String, destPath: String): Boolean { + Log.i(tag, "移动文件到新路径:") + + recordfile!!.moveFile(sourcePath,destPath) + return true + } + + + /** + * 重命名指定路径的音频文件 + */ + fun renameFile(filePath: String, newName: String): Boolean { + Log.i(tag, "重命名指定路径的音频文件:") + + + recordfile!!.renameFile(filePath,newName) + return true + } + /** * 停止录音 */ fun pauseRecord() { Log.i(tag, "停止连续录音:") - isRecording = false + recordfile!!.isPause=true } /** - * 停止录音 + * 关闭录音 */ - fun stopRecord() { - Log.i(tag, "停止连续录音:") - isRecording = false + fun stopRecord(isSave:Boolean) { + Log.i(tag, "关闭录音:") + recordfile!!.isPause=false - recordfile!!.closeFile() + recordfile!!.closeFile(isSave) } /** @@ -941,133 +945,4 @@ else{ } - inner class RecordingFile { - private var currentAudioFile: File? = null - private var fos: FileOutputStream? = null - - // 新增:用于异步写入的队列和线程 - private val writeQueue = LinkedBlockingQueue() - private val isWriting = AtomicBoolean(false) - private var writeThread: Thread? = null - - // 用于存储音频数据的缓冲区 - private val dataBuffer = mutableListOf() - private var totalBytesWritten = 0 - - // 音频配置 - private val sampleRate = 16000 - -var isPause=false - var fileName = "" - internal fun creatingFiles() { - if (fos != null || currentAudioFile != null) { - return - } - // 创建新的音频文件 - val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss") - val timestamp = dateFormat.format(Date()) - val parentDir = File(context.filesDir, "TranslatAudio") - if (!parentDir.exists()) { - parentDir.mkdirs() - } - val filePath = File(parentDir, "${fileName}_${timestamp}.wav").absolutePath - currentAudioFile = File(filePath) - currentAudioFile?.createNewFile() - - // 仅写入初始文件头 - fos = FileOutputStream(currentAudioFile).apply { - write(generateWavHeader(0)) // 初始0长度 - close() - } - - // 追加音频数据到文件 - fos = FileOutputStream(currentAudioFile, true) - startWriteThread() // 新增:启动写入线程 - } - - - // 新增:启动写入线程 - private fun startWriteThread() { - if (isWriting.get()) return - isWriting.set(true) - writeThread = Thread { - try { - while (isWriting.get() || writeQueue.isNotEmpty()) { - val data = writeQueue.poll() ?: continue - fos?.write(data) - } - } catch (e: Exception) { - Log.e(tag, "异步写入音频数据失败: ${e.message}") - } - } - writeThread?.start() - } - - /** - * 保存音频数据到 WAV 文件(异步) - */ - internal fun saveAudioDataToWav(buffer: ByteArray) { - if (fos == null || currentAudioFile == null) return - // 放入队列,由写线程写入 - writeQueue.offer(buffer.copyOf()) - totalBytesWritten += buffer.size - } - - - // 更新文件头生成(修正RIFF长度计算) - private fun generateWavHeader(dataLength: Int): ByteArray { - val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 - val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 - - return byteArrayOf( - 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), - (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), - ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), - 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), - 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), - 16, 0, 0, 0, // PCM头长度 - 1, 0, // PCM格式 - 1, 0, // 单声道 - (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 - (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 - 2, 0, // 块对齐 (通道数 * 样本位数/8) - 16, 0, // 样本位数 - 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), - (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), - ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() - ) - } - - internal fun closeFile() { - try { - if (fos == null || currentAudioFile == null) { - return - } - isWriting.set(false) - writeThread?.join(500) // 等待写线程结束 - fos?.close() - - // 更新WAV文件头 - currentAudioFile?.let { file -> - // 如果文件小于44字节,直接删除 - if (file.length() <= 44) { - file.delete() - Log.d(tag, "音频文件过小已删除: ${file.absolutePath}") - } else { - RandomAccessFile(file, "rw").use { raf -> - raf.seek(0) - raf.write(generateWavHeader(totalBytesWritten)) - } - Log.d(tag, "音频文件保存完成: ${file.absolutePath}") - } - } - - } catch (e: Exception) { - Log.e(tag, "更新WAV文件头失败: ${e.message}") - } finally { - fos = null - currentAudioFile = null - } - } - } } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 855d0b843..38346fd28 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -354,12 +354,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { result.success(true) } - "enableRecord" -> { - val fileName = call.argument("fileName")?: "" + "renameFile" -> { + + val filePath = call.argument("filePath")?: "" + val newName = call.argument("newName")?: "" try { - FileLogger.d(tag, "音频文件名称为: ${fileName}") // - azureAsrHelper.recordfile?.fileName= fileName!! - azureAsrHelper.enableRecord() + FileLogger.d(tag, "音频文件名称为: ${filePath}") // + azureAsrHelper.renameFile(filePath,newName) + result.success(true) + } catch (e: Exception) { + result.error("ENABLERECORD_ERROR", e.message, null) + } + } + "moveFile" -> { + val sourcePath = call.argument("sourcePath")?: "" + val destPath = call.argument("destPath")?: "" + try { + FileLogger.d(tag, "音频文件名称为: ${sourcePath}") // + + azureAsrHelper.moveFile(sourcePath,destPath) + result.success(true) + } catch (e: Exception) { + result.error("ENABLERECORD_ERROR", e.message, null) + } + } + "enableRecord" -> { + val filePath = call.argument("filePath")?: "" + try { + FileLogger.d(tag, "音频文件名称为: ${filePath}") // + + azureAsrHelper.enableRecord(filePath) result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -370,8 +394,13 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { result.success(true) } "stopRecord" -> { - azureAsrHelper.stopRecord() + val isSave = call.argument("isSave")?:false + try { + azureAsrHelper.stopRecord(isSave) result.success(true) + } catch (e: Exception) { + result.error("ENABLERECORD_ERROR", e.message, null) + } } else -> { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt new file mode 100644 index 000000000..e23c6cb34 --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt @@ -0,0 +1,251 @@ +package com.yunqiinnovation.azure_speech.tools +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile + +import android.util.Log +import java.io.FileInputStream +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean + + object RecordFile { + private var currentAudioFile: File? = null + private var fos: FileOutputStream? = null + + // 新增:用于异步写入的队列和线程 + private val writeQueue = LinkedBlockingQueue() + private val isWriting = AtomicBoolean(false) + private var writeThread: Thread? = null + + // 用于存储音频数据的缓冲区 + private val dataBuffer = mutableListOf() + private var totalBytesWritten = 0 + + // 音频配置 + private val sampleRate = 16000 + + // 新增:用于存储最后成功保存的文件 + private var lastSavedFile: File? = null + private var lastFileTimestamp: String = "" + var isPause = false + var fileName = "" + + + // 修改:添加 filePath 参数 + internal fun creatingFiles(filePath: String) { + if (fos != null || currentAudioFile != null) { + return + } + + val file = File(filePath) + val parentDir = file.parentFile + if (!parentDir.exists()) { + parentDir.mkdirs() + } + + currentAudioFile = file + currentAudioFile?.createNewFile() + + fos = FileOutputStream(currentAudioFile).apply { + write(generateWavHeader(0)) + close() + } + + fos = FileOutputStream(currentAudioFile, true) + startWriteThread() + } + + + // 新增:启动写入线程 + private fun startWriteThread() { + if (isWriting.get()) return + isWriting.set(true) + writeThread = Thread { + try { + while (isWriting.get() || writeQueue.isNotEmpty()) { + val data = writeQueue.poll() ?: continue + fos?.write(data) + } + } catch (e: Exception) { + Log.e("tag", "异步写入音频数据失败: ${e.message}") + } + } + writeThread?.start() + } + + /** + * 保存音频数据到 WAV 文件(异步) + */ + internal fun saveAudioDataToWav(buffer: ByteArray) { + if (fos == null || currentAudioFile == null) return + // 放入队列,由写线程写入 + writeQueue.offer(buffer.copyOf()) + totalBytesWritten += buffer.size + } + + + // 更新文件头生成(修正RIFF长度计算) + private fun generateWavHeader(dataLength: Int): ByteArray { + val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 + val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 + + return byteArrayOf( + 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), + (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), + ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), + 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), + 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), + 16, 0, 0, 0, // PCM头长度 + 1, 0, // PCM格式 + 1, 0, // 单声道 + (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 + (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 + 2, 0, // 块对齐 (通道数 * 样本位数/8) + 16, 0, // 样本位数 + 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), + (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), + ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() + ) + } + + /** + * 重命名指定路径的音频文件 + * @param filePath 要重命名的文件完整路径 + * @param newName 新的文件名(不含扩展名) + * @return true 重命名成功,false 失败 + */ + internal fun renameFile(filePath: String, newName: String): Boolean { + val sourceFile = File(filePath) + + // 验证源文件存在 + if (!sourceFile.exists()) { + Log.w("tag", "重命名失败: 文件不存在 $filePath") + return false + } + + // 防止重命名正在录制的文件 + if (sourceFile == currentAudioFile) { + Log.w("tag", "重命名失败: 不能重命名正在录制的文件") + return false + } + + val parentDir = sourceFile.parentFile + val newFile = File(parentDir, "$newName.wav") // 保持.wav扩展名 + + // 处理目标文件已存在的情况 + if (newFile.exists()) { + Log.w("tag", "重命名失败: 目标文件已存在 ${newFile.absolutePath}") + return false + } + + // 执行重命名 + return try { + val success = sourceFile.renameTo(newFile) + if (success) { + Log.d("tag", "文件重命名成功: ${sourceFile.name} -> ${newFile.name}") + // 更新最后保存的文件引用(如果是同一个文件) + if (lastSavedFile == sourceFile) { + lastSavedFile = newFile + } + } else { + Log.w("tag", "文件重命名操作失败") + } + success + } catch (e: Exception) { + Log.e("tag", "重命名异常: ${e.message}") + false + } + } +/** + * 移动文件到新路径 + * @param sourcePath 原始文件完整路径 + * @param destPath 目标文件完整路径 + * @return true 移动成功,false 移动失败 + */ + internal fun moveFile(sourcePath: String, destPath: String): Boolean { + val sourceFile = File(sourcePath) + val destFile = File(destPath) + + if (!sourceFile.exists()) { + Log.w("tag", "移动失败: 源文件不存在 ${sourceFile.absolutePath}") + return false + } + + // 确保目标目录存在 + val destDir = destFile.parentFile + if (!destDir.exists() && !destDir.mkdirs()) { + Log.w("tag", "移动失败: 无法创建目标目录 ${destDir.absolutePath}") + return false + } + + // 删除已存在的目标文件 + if (destFile.exists() && !destFile.delete()) { + Log.w("tag", "移动失败: 无法覆盖已存在文件 ${destFile.absolutePath}") + return false + } + + return try { + // 使用renameTo尝试移动(同分区最快) + if (sourceFile.renameTo(destFile)) { + true + } else { + // 跨分区移动:复制+删除 + FileInputStream(sourceFile).use { input -> + FileOutputStream(destFile).use { output -> + input.copyTo(output) + } + } + if (!sourceFile.delete()) { + Log.w("tag", "移动警告: 源文件删除失败 ${sourceFile.absolutePath}") + } + true + }.also { + if (it) { + Log.d("tag", "文件移动成功: ${sourcePath} -> ${destPath}") + // 更新最后保存的文件引用 + if (lastSavedFile?.absolutePath == sourcePath) { + lastSavedFile = destFile + } + } + } + } catch (e: Exception) { + Log.e("tag", "文件移动异常: ${e.message}") + false + } + } + internal fun closeFile( isSave:Boolean): Boolean { + try { + if (fos == null || currentAudioFile == null) { + return false + } + isWriting.set(false) + writeThread?.join(500) + fos?.close() + + currentAudioFile?.let { file -> + if (file.length() <= 44||isSave==false) { + file.delete() + Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") + return false + } else { + RandomAccessFile(file, "rw").use { raf -> + raf.seek(0) + raf.write(generateWavHeader(totalBytesWritten)) + } + Log.d("tag", "音频文件保存完成: ${file.absolutePath}") + + // 保存成功文件引用 + lastSavedFile = file + return true + } + } + } catch (e: Exception) { + Log.e("tag", "更新WAV文件头失败: ${e.message}") + return false + } finally { + fos = null + currentAudioFile = null + } + return false + } + } \ No newline at end of file