diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 82cfabbc2..4ecee122a 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -24,7 +24,12 @@ class MeetingController extends GetxController { bool isLoading = false; int page = 1; int addQuantity = 0; + List selected = []; + final appDocDir = getApplicationSupportDirectory(); + // final directory = Directory( + // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); + List files = []; TextEditingController titleController = TextEditingController(); // 录音标题 RxList dataList = [].obs; @@ -42,7 +47,7 @@ class MeetingController extends GetxController { void onInit() { super.onInit(); listController.addListener(_onScroll); - _requestStoragePermission(); + //_requestStoragePermission(); //Permission.storage.request(); _headers = { 'Authorization': 'Bearer; $_token', @@ -107,8 +112,64 @@ class MeetingController extends GetxController { }); } + void loadTranslatAudio() async { + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/TranslatAudio"); + print("appDocDir=${dir?.path}"); + 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(); + } + + void addTranslatAudio() async { + EasyLoading.show(status: '正在批量导入...'); + for (final filePath in selected) { + try { + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + final fileStat = await File(filePath).stat(); + final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳 + var audiourl = await UploadOss.upload( + filepath: filePath, rootDir: 'TranslatAudio'); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + final fileName = filePath.split('/').last; + // int creationTime = DateTime.now().millisecondsSinceEpoch; + Map data = { + 'title': fileName, + 'type': 'TRANSLAT', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': audiourl, + 'tasktype': 0, + 'creationtime': creationTime, + }; + print("data=$data"); + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + dataList.insert(0, { + 'id': meetingId, + 'taskid': '', + ...data, + }); + addQuantity++; + } + } catch (e) { + Logger.error('导入文件失败: $filePath, error: $e'); + // 可选:继续导入下一个文件 + } + } + EasyLoading.dismiss(); + } + void addAudio() async { - EasyLoading.show(); + EasyLoading.show(status: '正在导入...'); FilePickerResult? result = await FilePicker.platform.pickFiles( withData: true, ); @@ -118,7 +179,7 @@ class MeetingController extends GetxController { String fileName = file.name; await _audioPlayer.setAudioSource(AudioSource.file(filePath)); var audiourl = - await UploadOss.upload(filepath: filePath, rootDir: 'audio'); + await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); int seconds = _audioPlayer.duration?.inSeconds ?? 0; int creationTime = DateTime.now().millisecondsSinceEpoch; Map data = { @@ -145,7 +206,7 @@ class MeetingController extends GetxController { Future _saveFile(PlatformFile file) async { final directory = await getApplicationDocumentsDirectory(); - final String filePath = '${directory.path}/${file.name}'; + final String filePath = '${directory.path}/ExternalAudio/${file.name}'; final File localFile = File(filePath); await localFile.writeAsBytes(file.bytes!); return filePath; diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index c3f0b43be..67e4297b6 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -183,23 +183,7 @@ class MeetingView extends GetView { // 新增:本地音视频文件选择弹窗 void _showLocalAudioVideoPicker(BuildContext context, bool isDarkMode) async { // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 - - final directory = Directory( - '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); - final List files = directory - .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(); - - List selected = []; - + controller.loadTranslatAudio(); showModalBottomSheet( context: context, isScrollControlled: true, @@ -236,19 +220,20 @@ class MeetingView extends GetView { SizedBox( height: 300.w, child: ListView.builder( - itemCount: files.length, + itemCount: controller.files.length, itemBuilder: (context, idx) { - final file = files[idx]; + final file = controller.files[idx]; final fileName = file.path.split('/').last; - final isChecked = selected.contains(file.path); + final isChecked = + controller.selected.contains(file.path); return CheckboxListTile( value: isChecked, onChanged: (checked) { setState(() { if (checked == true) { - selected.add(file.path); + controller.selected.add(file.path); } else { - selected.remove(file.path); + controller.selected.remove(file.path); } }); }, @@ -281,12 +266,12 @@ class MeetingView extends GetView { style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, ), - onPressed: selected.isEmpty + onPressed: controller.selected.isEmpty ? null : () { Navigator.of(context).pop(); // TODO: 调用导入逻辑 - // controller.importFiles(selected); + controller.addTranslatAudio(); }, child: Text('导入选中', style: TextStyle(color: Colors.white)), diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index dbbc4fa1a..d04d4f5c5 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -277,12 +277,13 @@ class OtaUpgradeController extends GetxController { type.value = DOWNLOADING_UP_FILES; try { - final Directory? downloadsDir = await getDownloadsDirectory(); + final Directory? downloadsDir = await getApplicationSupportDirectory(); if (downloadsDir == null) { throw FileSystemException('无法访问下载目录'); } - final targetDir = Directory('${downloadsDir.path}/$directoryName'); + final targetDir = + Directory('${downloadsDir.path}/Download/$directoryName'); if (!await targetDir.exists()) { await targetDir.create(recursive: true); } diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index 011ff285e..bea4404c0 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -244,12 +244,14 @@ class ProfileController extends GetxController { if (response.statusCode == 200) { // 保存到应用文档目录 - final appDir = await getApplicationDocumentsDirectory(); - final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final appDir = await getApplicationSupportDirectory(); + final fileName = + '/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; final localPath = '${appDir.path}/$fileName'; // 写入文件 final file = File(localPath); + await file.parent.create(recursive: true); await file.writeAsBytes(response.data); // 更新本地头像路径 diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index b892932ce..17757e376 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -121,8 +121,16 @@ class SettingsController extends GetxController { 'name': '日程权限', 'icon': Icons.calendar_today, 'permissions': [ - {'type': Permission.calendarFullAccess, 'name': '日历写入', 'description': '用于创建日历事件和提醒'}, - {'type': Permission.calendarFullAccess, 'name': '日历完全访问', 'description': '用于读取和修改日历事件'} + { + 'type': Permission.calendarFullAccess, + 'name': '日历写入', + 'description': '用于创建日历事件和提醒' + }, + { + 'type': Permission.calendarFullAccess, + 'name': '日历完全访问', + 'description': '用于读取和修改日历事件' + } ] }, { @@ -137,46 +145,58 @@ class SettingsController extends GetxController { 'name': '位置权限', 'icon': Icons.location_on, 'permissions': [ - {'type': Permission.location, 'name': '位置', 'description': '用于获取当前位置信息'}, - {'type': Permission.locationAlways, 'name': '后台位置', 'description': '用于后台获取位置信息'} + { + 'type': Permission.location, + 'name': '位置', + 'description': '用于获取当前位置信息' + }, + { + 'type': Permission.locationAlways, + 'name': '后台位置', + 'description': '用于后台获取位置信息' + } ] }, { 'name': '媒体权限', 'icon': Icons.mic, 'permissions': [ - {'type': Permission.microphone, 'name': '麦克风', 'description': '用于语音识别和录音'}, + { + 'type': Permission.microphone, + 'name': '麦克风', + 'description': '用于语音识别和录音' + }, {'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'} ] } ]; - + // 检查单个权限状态 Future checkPermission(Permission permission) async { return await permission.status; } - + // 请求单个权限 Future requestPermission(Permission permission) async { return await permission.request(); } - + // 打开应用设置 void openSettings() { openAppSettings(); } - + // 检查所有权限状态并返回结果 Future> checkAllPermissions() async { Map statuses = {}; - + for (var group in permissionGroups) { for (var perm in group['permissions']) { Permission permission = perm['type']; statuses[permission] = await permission.status; } } - + return statuses; } @@ -263,12 +283,15 @@ class SettingsController extends GetxController { if (response.statusCode == 200) { // 保存到应用文档目录 - final appDir = await getApplicationDocumentsDirectory(); - final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final appDir = await getApplicationSupportDirectory(); + final fileName = + '/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; final localPath = '${appDir.path}/$fileName'; - // 写入文件 +// 确保目录存在 final file = File(localPath); + await file.parent.create(recursive: true); + await file.writeAsBytes(response.data); // 更新本地头像路径 diff --git a/lib/modules/timbre/controllers/timbre_controller.dart b/lib/modules/timbre/controllers/timbre_controller.dart index 62d2cec06..d976105fa 100644 --- a/lib/modules/timbre/controllers/timbre_controller.dart +++ b/lib/modules/timbre/controllers/timbre_controller.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; @@ -77,8 +78,12 @@ class TimbreController extends GetxController { await _getTraintext(); await _addTask(); final directory = await getApplicationDocumentsDirectory(); + final recordDir = Directory('${directory.path}/RecordSound'); + if (!await recordDir.exists()) { + await recordDir.create(recursive: true); + } final path = - '${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; + '${directory.path}/RecordSound/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; await _record.start( const RecordConfig( encoder: AudioEncoder.wav, 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 3fed166b0..da6d58be0 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 @@ -30,6 +30,10 @@ 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语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 * @@ -128,15 +132,15 @@ class AzureAsrHelper(private val context: Context) { // 设置指定的识别语言 speechRecognitionLanguage = currentLanguage } - + setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") setProperty("Speech_SegmentationSilenceTimeoutMs", "300") - + // 优化:添加低延迟连接配置 setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200") setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") setProperty("Speech_PushStreamFormat", "PCM") - + // 设置分段策略为时间模式 setProperty("Speech_SegmentationStrategy", "Time") } @@ -189,13 +193,13 @@ class AzureAsrHelper(private val context: Context) { val needEchoCancellation = !isHeadsetConnected if (needEchoCancellation) { - Log.i(tag, "使用拉流方式进行回音消除") + Log.i(tag, "使用拉流方式进行回音消除") // 使用拉流方式进行回音消除 setupMicrophoneStream() } else { - Log.i(tag, "使用拉流方式进行回音消除2") + Log.i(tag, "使用拉流方式进行回音消除2") audioConfig = AudioConfig.fromDefaultMicrophoneInput() - setupMicrophoneStream() + setupMicrophoneStream() } } @@ -237,7 +241,7 @@ class AzureAsrHelper(private val context: Context) { microphoneStream = null } } - + /** * 预热识别器(减少首次识别延迟) */ @@ -247,7 +251,7 @@ class AzureAsrHelper(private val context: Context) { val warmupData = ByteArray(320) // 10ms 16kHz 单声道 // 模拟静音数据 warmupData.fill(0) - + // 如果是外部音频流,进行预热 if (audioSourceType == AudioSourceType.EXTERNAL) { externalAudioStream?.pushAudio(warmupData) @@ -259,7 +263,7 @@ class AzureAsrHelper(private val context: Context) { } } - + /** * 设置外部音频流 - 使用拉流方式 */ @@ -412,7 +416,8 @@ class AzureAsrHelper(private val context: Context) { EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { val detectedLanguage = if (isAutoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: supportedLanguages[0] + AutoDetectSourceLanguageResult.fromResult(event.result)?.language + ?: supportedLanguages[0] } else { currentLanguage } @@ -536,7 +541,7 @@ class AzureAsrHelper(private val context: Context) { // 释放音频配置 audioConfig?.close() audioConfig = null - + isRecording = false // 确保状态被重置 isContinuousRecognitionActive = false microphoneStream = null @@ -546,6 +551,7 @@ class AzureAsrHelper(private val context: Context) { speechConfig = null } catch (e: Exception) { // 确保状态被重置 + isRecording = false isContinuousRecognitionActive = false microphoneStream = null externalAudioStream = null @@ -603,9 +609,11 @@ class AzureAsrHelper(private val context: Context) { */ fun enableRecord() { Log.i(tag, "开启录音:") - isRecording = true + recordfile!!.closeFile() - if (isContinuousRecognitionActive) { + isRecording = true + if (isContinuousRecognitionActive) { + recordfile!!.creatingFiles() } @@ -628,15 +636,17 @@ class AzureAsrHelper(private val context: Context) { private inner class MicrophoneStream : PullAudioInputStreamCallback() { private var audioRecord: AudioRecord? = null private var echoCanceler: AcousticEchoCanceler? = 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() } @@ -694,7 +704,7 @@ class AzureAsrHelper(private val context: Context) { */ private fun applyAudioEffects() { val sessionId = audioRecord?.audioSessionId ?: return - + // 优化:只在真正需要时启用回音消除 val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (audioManager.isSpeakerphoneOn) { @@ -726,7 +736,7 @@ class AzureAsrHelper(private val context: Context) { Log.e(tag, "读取音频数据失败: $result") return 0 } - recordfile?.saveAudioDataToWav(buffer) + recordfile?.saveAudioDataToWav(buffer) return result } catch (e: Exception) { Log.e(tag, "读取音频异常: ${e.message}") @@ -792,12 +802,12 @@ class AzureAsrHelper(private val context: Context) { override fun read(buffer: ByteArray): Int { try { val chunk = queue.take() - + // 检查是否是结束标志(空数组) if (chunk.isEmpty()) { return 0 } - + val toCopy = minOf(chunk.size, buffer.size) System.arraycopy(chunk, 0, buffer, 0, toCopy) return toCopy @@ -890,6 +900,11 @@ class AzureAsrHelper(private val context: Context) { 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 @@ -904,9 +919,12 @@ class AzureAsrHelper(private val context: Context) { } // 创建新的音频文件 val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss") - val timestamp = dateFormat.format(Date()) - val filePath = - context.getExternalFilesDir(null)?.absolutePath + "/${fileName}_${timestamp}.wav" + 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() @@ -918,31 +936,38 @@ class AzureAsrHelper(private val context: Context) { // 追加音频数据到文件 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 文件 + * 保存音频数据到 WAV 文件(异步) */ internal fun saveAudioDataToWav(buffer: ByteArray) { - if (fos == null || currentAudioFile == null) { - return - } - // 复制数据到新数组 - val copy = ByteArray(buffer.size) - System.arraycopy(buffer, 0, copy, 0, buffer.size) - - // 添加到缓冲区 - dataBuffer.add(copy) + if (fos == null || currentAudioFile == null) return + // 放入队列,由写线程写入 + writeQueue.offer(buffer.copyOf()) totalBytesWritten += buffer.size - - // 写入文件 - try { - fos?.write(copy, 0, buffer.size) - } catch (e: Exception) { - Log.e(tag, "写入音频数据失败: ${e.message}") - } } + // 更新文件头生成(修正RIFF长度计算) private fun generateWavHeader(dataLength: Int): ByteArray { val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 @@ -967,35 +992,36 @@ class AzureAsrHelper(private val context: Context) { ) } - internal fun closeFile() { - try { - if (fos == null || currentAudioFile == null) { - return - } - // 关闭文件流 - 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)) + 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}") + } } - Log.d(tag, "音频文件保存完成: ${file.absolutePath}") + + } catch (e: Exception) { + Log.e(tag, "更新WAV文件头失败: ${e.message}") + } finally { + fos = null + currentAudioFile = null } } - - } catch (e: Exception) { - Log.e(tag, "更新WAV文件头失败: ${e.message}") - } finally { - fos = null - currentAudioFile = null - } -} } } \ No newline at end of file