Browse Source

优化录音音频卡,翻译音频导入

newdev_shunjiawei
fdp 1 year ago
parent
commit
08fa48a712
  1. 69
      lib/modules/meeting/controllers/meeting_controller.dart
  2. 33
      lib/modules/meeting/views/meeting_view.dart
  3. 5
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  4. 6
      lib/modules/profile/controllers/profile_controller.dart
  5. 51
      lib/modules/settings/controllers/settings_controller.dart
  6. 7
      lib/modules/timbre/controllers/timbre_controller.dart
  7. 156
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

69
lib/modules/meeting/controllers/meeting_controller.dart

@ -24,7 +24,12 @@ class MeetingController extends GetxController {
bool isLoading = false; bool isLoading = false;
int page = 1; int page = 1;
int addQuantity = 0; int addQuantity = 0;
List<String> selected = [];
final appDocDir = getApplicationSupportDirectory();
// final directory = Directory(
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List<FileSystemEntity> files = [];
TextEditingController titleController = TextEditingController(); // 录音标题 TextEditingController titleController = TextEditingController(); // 录音标题
RxList dataList = [].obs; RxList dataList = [].obs;
@ -42,7 +47,7 @@ class MeetingController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
listController.addListener(_onScroll); listController.addListener(_onScroll);
_requestStoragePermission(); //_requestStoragePermission();
//Permission.storage.request(); //Permission.storage.request();
_headers = { _headers = {
'Authorization': 'Bearer; $_token', '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<String, Object?> 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 { void addAudio() async {
EasyLoading.show(); EasyLoading.show(status: '正在导入...');
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true, withData: true,
); );
@ -118,7 +179,7 @@ class MeetingController extends GetxController {
String fileName = file.name; String fileName = file.name;
await _audioPlayer.setAudioSource(AudioSource.file(filePath)); await _audioPlayer.setAudioSource(AudioSource.file(filePath));
var audiourl = var audiourl =
await UploadOss.upload(filepath: filePath, rootDir: 'audio'); await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio');
int seconds = _audioPlayer.duration?.inSeconds ?? 0; int seconds = _audioPlayer.duration?.inSeconds ?? 0;
int creationTime = DateTime.now().millisecondsSinceEpoch; int creationTime = DateTime.now().millisecondsSinceEpoch;
Map<String, Object?> data = { Map<String, Object?> data = {
@ -145,7 +206,7 @@ class MeetingController extends GetxController {
Future<String> _saveFile(PlatformFile file) async { Future<String> _saveFile(PlatformFile file) async {
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationDocumentsDirectory();
final String filePath = '${directory.path}/${file.name}'; final String filePath = '${directory.path}/ExternalAudio/${file.name}';
final File localFile = File(filePath); final File localFile = File(filePath);
await localFile.writeAsBytes(file.bytes!); await localFile.writeAsBytes(file.bytes!);
return filePath; return filePath;

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

@ -183,23 +183,7 @@ class MeetingView extends GetView<MeetingController> {
// 新增:本地音视频文件选择弹窗 // 新增:本地音视频文件选择弹窗
void _showLocalAudioVideoPicker(BuildContext context, bool isDarkMode) async { void _showLocalAudioVideoPicker(BuildContext context, bool isDarkMode) async {
// 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径
controller.loadTranslatAudio();
final directory = Directory(
'/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
final List<FileSystemEntity> 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<String> selected = [];
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -236,19 +220,20 @@ class MeetingView extends GetView<MeetingController> {
SizedBox( SizedBox(
height: 300.w, height: 300.w,
child: ListView.builder( child: ListView.builder(
itemCount: files.length, itemCount: controller.files.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final file = files[idx]; final file = controller.files[idx];
final fileName = file.path.split('/').last; final fileName = file.path.split('/').last;
final isChecked = selected.contains(file.path); final isChecked =
controller.selected.contains(file.path);
return CheckboxListTile( return CheckboxListTile(
value: isChecked, value: isChecked,
onChanged: (checked) { onChanged: (checked) {
setState(() { setState(() {
if (checked == true) { if (checked == true) {
selected.add(file.path); controller.selected.add(file.path);
} else { } else {
selected.remove(file.path); controller.selected.remove(file.path);
} }
}); });
}, },
@ -281,12 +266,12 @@ class MeetingView extends GetView<MeetingController> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
), ),
onPressed: selected.isEmpty onPressed: controller.selected.isEmpty
? null ? null
: () { : () {
Navigator.of(context).pop(); Navigator.of(context).pop();
// TODO: 调用导入逻辑 // TODO: 调用导入逻辑
// controller.importFiles(selected); controller.addTranslatAudio();
}, },
child: child:
Text('导入选中', style: TextStyle(color: Colors.white)), Text('导入选中', style: TextStyle(color: Colors.white)),

5
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -277,12 +277,13 @@ class OtaUpgradeController extends GetxController {
type.value = DOWNLOADING_UP_FILES; type.value = DOWNLOADING_UP_FILES;
try { try {
final Directory? downloadsDir = await getDownloadsDirectory(); final Directory? downloadsDir = await getApplicationSupportDirectory();
if (downloadsDir == null) { if (downloadsDir == null) {
throw FileSystemException('无法访问下载目录'); throw FileSystemException('无法访问下载目录');
} }
final targetDir = Directory('${downloadsDir.path}/$directoryName'); final targetDir =
Directory('${downloadsDir.path}/Download/$directoryName');
if (!await targetDir.exists()) { if (!await targetDir.exists()) {
await targetDir.create(recursive: true); await targetDir.create(recursive: true);
} }

6
lib/modules/profile/controllers/profile_controller.dart

@ -244,12 +244,14 @@ class ProfileController extends GetxController {
if (response.statusCode == 200) { if (response.statusCode == 200) {
// 保存到应用文档目录 // 保存到应用文档目录
final appDir = await getApplicationDocumentsDirectory(); final appDir = await getApplicationSupportDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; final fileName =
'/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final localPath = '${appDir.path}/$fileName'; final localPath = '${appDir.path}/$fileName';
// 写入文件 // 写入文件
final file = File(localPath); final file = File(localPath);
await file.parent.create(recursive: true);
await file.writeAsBytes(response.data); await file.writeAsBytes(response.data);
// 更新本地头像路径 // 更新本地头像路径

51
lib/modules/settings/controllers/settings_controller.dart

@ -121,8 +121,16 @@ class SettingsController extends GetxController {
'name': '日程权限', 'name': '日程权限',
'icon': Icons.calendar_today, 'icon': Icons.calendar_today,
'permissions': [ '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': '位置权限', 'name': '位置权限',
'icon': Icons.location_on, 'icon': Icons.location_on,
'permissions': [ 'permissions': [
{'type': Permission.location, 'name': '位置', 'description': '用于获取当前位置信息'}, {
{'type': Permission.locationAlways, 'name': '后台位置', 'description': '用于后台获取位置信息'} 'type': Permission.location,
'name': '位置',
'description': '用于获取当前位置信息'
},
{
'type': Permission.locationAlways,
'name': '后台位置',
'description': '用于后台获取位置信息'
}
] ]
}, },
{ {
'name': '媒体权限', 'name': '媒体权限',
'icon': Icons.mic, 'icon': Icons.mic,
'permissions': [ 'permissions': [
{'type': Permission.microphone, 'name': '麦克风', 'description': '用于语音识别和录音'}, {
'type': Permission.microphone,
'name': '麦克风',
'description': '用于语音识别和录音'
},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'} {'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
] ]
} }
]; ];
// 检查单个权限状态 // 检查单个权限状态
Future<PermissionStatus> checkPermission(Permission permission) async { Future<PermissionStatus> checkPermission(Permission permission) async {
return await permission.status; return await permission.status;
} }
// 请求单个权限 // 请求单个权限
Future<PermissionStatus> requestPermission(Permission permission) async { Future<PermissionStatus> requestPermission(Permission permission) async {
return await permission.request(); return await permission.request();
} }
// 打开应用设置 // 打开应用设置
void openSettings() { void openSettings() {
openAppSettings(); openAppSettings();
} }
// 检查所有权限状态并返回结果 // 检查所有权限状态并返回结果
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async { Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
Map<Permission, PermissionStatus> statuses = {}; Map<Permission, PermissionStatus> statuses = {};
for (var group in permissionGroups) { for (var group in permissionGroups) {
for (var perm in group['permissions']) { for (var perm in group['permissions']) {
Permission permission = perm['type']; Permission permission = perm['type'];
statuses[permission] = await permission.status; statuses[permission] = await permission.status;
} }
} }
return statuses; return statuses;
} }
@ -263,12 +283,15 @@ class SettingsController extends GetxController {
if (response.statusCode == 200) { if (response.statusCode == 200) {
// 保存到应用文档目录 // 保存到应用文档目录
final appDir = await getApplicationDocumentsDirectory(); final appDir = await getApplicationSupportDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; final fileName =
'/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final localPath = '${appDir.path}/$fileName'; final localPath = '${appDir.path}/$fileName';
// 写入文件 // 确保目录存在
final file = File(localPath); final file = File(localPath);
await file.parent.create(recursive: true);
await file.writeAsBytes(response.data); await file.writeAsBytes(response.data);
// 更新本地头像路径 // 更新本地头像路径

7
lib/modules/timbre/controllers/timbre_controller.dart

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -77,8 +78,12 @@ class TimbreController extends GetxController {
await _getTraintext(); await _getTraintext();
await _addTask(); await _addTask();
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationDocumentsDirectory();
final recordDir = Directory('${directory.path}/RecordSound');
if (!await recordDir.exists()) {
await recordDir.create(recursive: true);
}
final path = final path =
'${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; '${directory.path}/RecordSound/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
await _record.start( await _record.start(
const RecordConfig( const RecordConfig(
encoder: AudioEncoder.wav, encoder: AudioEncoder.wav,

156
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.io.RandomAccessFile
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import android.os.Handler
import android.os.HandlerThread
/** /**
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 * Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别
* *
@ -128,15 +132,15 @@ class AzureAsrHelper(private val context: Context) {
// 设置指定的识别语言 // 设置指定的识别语言
speechRecognitionLanguage = currentLanguage speechRecognitionLanguage = currentLanguage
} }
setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300")
setProperty("Speech_SegmentationSilenceTimeoutMs", "300") setProperty("Speech_SegmentationSilenceTimeoutMs", "300")
// 优化:添加低延迟连接配置 // 优化:添加低延迟连接配置
setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200") setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200")
setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE")
setProperty("Speech_PushStreamFormat", "PCM") setProperty("Speech_PushStreamFormat", "PCM")
// 设置分段策略为时间模式 // 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time") setProperty("Speech_SegmentationStrategy", "Time")
} }
@ -189,13 +193,13 @@ class AzureAsrHelper(private val context: Context) {
val needEchoCancellation = !isHeadsetConnected val needEchoCancellation = !isHeadsetConnected
if (needEchoCancellation) { if (needEchoCancellation) {
Log.i(tag, "使用拉流方式进行回音消除") Log.i(tag, "使用拉流方式进行回音消除")
// 使用拉流方式进行回音消除 // 使用拉流方式进行回音消除
setupMicrophoneStream() setupMicrophoneStream()
} else { } else {
Log.i(tag, "使用拉流方式进行回音消除2") Log.i(tag, "使用拉流方式进行回音消除2")
audioConfig = AudioConfig.fromDefaultMicrophoneInput() audioConfig = AudioConfig.fromDefaultMicrophoneInput()
setupMicrophoneStream() setupMicrophoneStream()
} }
} }
@ -237,7 +241,7 @@ class AzureAsrHelper(private val context: Context) {
microphoneStream = null microphoneStream = null
} }
} }
/** /**
* 预热识别器(减少首次识别延迟) * 预热识别器(减少首次识别延迟)
*/ */
@ -247,7 +251,7 @@ class AzureAsrHelper(private val context: Context) {
val warmupData = ByteArray(320) // 10ms 16kHz 单声道 val warmupData = ByteArray(320) // 10ms 16kHz 单声道
// 模拟静音数据 // 模拟静音数据
warmupData.fill(0) warmupData.fill(0)
// 如果是外部音频流,进行预热 // 如果是外部音频流,进行预热
if (audioSourceType == AudioSourceType.EXTERNAL) { if (audioSourceType == AudioSourceType.EXTERNAL) {
externalAudioStream?.pushAudio(warmupData) externalAudioStream?.pushAudio(warmupData)
@ -259,7 +263,7 @@ class AzureAsrHelper(private val context: Context) {
} }
} }
/** /**
* 设置外部音频流 - 使用拉流方式 * 设置外部音频流 - 使用拉流方式
*/ */
@ -412,7 +416,8 @@ class AzureAsrHelper(private val context: Context) {
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) {
val detectedLanguage = if (isAutoDetectLanguage) { val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: supportedLanguages[0] AutoDetectSourceLanguageResult.fromResult(event.result)?.language
?: supportedLanguages[0]
} else { } else {
currentLanguage currentLanguage
} }
@ -536,7 +541,7 @@ class AzureAsrHelper(private val context: Context) {
// 释放音频配置 // 释放音频配置
audioConfig?.close() audioConfig?.close()
audioConfig = null audioConfig = null
isRecording = false
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
microphoneStream = null microphoneStream = null
@ -546,6 +551,7 @@ class AzureAsrHelper(private val context: Context) {
speechConfig = null speechConfig = null
} catch (e: Exception) { } catch (e: Exception) {
// 确保状态被重置 // 确保状态被重置
isRecording = false
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
microphoneStream = null microphoneStream = null
externalAudioStream = null externalAudioStream = null
@ -603,9 +609,11 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun enableRecord() { fun enableRecord() {
Log.i(tag, "开启录音:") Log.i(tag, "开启录音:")
isRecording = true
recordfile!!.closeFile() recordfile!!.closeFile()
if (isContinuousRecognitionActive) { isRecording = true
if (isContinuousRecognitionActive) {
recordfile!!.creatingFiles() recordfile!!.creatingFiles()
} }
@ -628,15 +636,17 @@ class AzureAsrHelper(private val context: Context) {
private inner class MicrophoneStream : PullAudioInputStreamCallback() { private inner class MicrophoneStream : PullAudioInputStreamCallback() {
private var audioRecord: AudioRecord? = null private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null private var echoCanceler: AcousticEchoCanceler? = null
// 音频配置 // 音频配置
private val sampleRate = 16000 private val sampleRate = 16000
private val channelConfig = AudioFormat.CHANNEL_IN_MONO private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 优化:减少缓冲区大小以降低延迟 // 优化:减少缓冲区大小以降低延迟
private val bufferSize = AudioRecord.getMinBufferSize( private val bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat sampleRate, channelConfig, audioFormat
).let { if (it < 0) 2048 else it } ).let { if (it < 0) 2048 else it }
init { init {
initMicrophone() initMicrophone()
} }
@ -694,7 +704,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun applyAudioEffects() { private fun applyAudioEffects() {
val sessionId = audioRecord?.audioSessionId ?: return val sessionId = audioRecord?.audioSessionId ?: return
// 优化:只在真正需要时启用回音消除 // 优化:只在真正需要时启用回音消除
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.isSpeakerphoneOn) { if (audioManager.isSpeakerphoneOn) {
@ -726,7 +736,7 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "读取音频数据失败: $result") Log.e(tag, "读取音频数据失败: $result")
return 0 return 0
} }
recordfile?.saveAudioDataToWav(buffer) recordfile?.saveAudioDataToWav(buffer)
return result return result
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "读取音频异常: ${e.message}") Log.e(tag, "读取音频异常: ${e.message}")
@ -792,12 +802,12 @@ class AzureAsrHelper(private val context: Context) {
override fun read(buffer: ByteArray): Int { override fun read(buffer: ByteArray): Int {
try { try {
val chunk = queue.take() val chunk = queue.take()
// 检查是否是结束标志(空数组) // 检查是否是结束标志(空数组)
if (chunk.isEmpty()) { if (chunk.isEmpty()) {
return 0 return 0
} }
val toCopy = minOf(chunk.size, buffer.size) val toCopy = minOf(chunk.size, buffer.size)
System.arraycopy(chunk, 0, buffer, 0, toCopy) System.arraycopy(chunk, 0, buffer, 0, toCopy)
return toCopy return toCopy
@ -890,6 +900,11 @@ class AzureAsrHelper(private val context: Context) {
private var currentAudioFile: File? = null private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null private var fos: FileOutputStream? = null
// 新增:用于异步写入的队列和线程
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
// 用于存储音频数据的缓冲区 // 用于存储音频数据的缓冲区
private val dataBuffer = mutableListOf<ByteArray>() private val dataBuffer = mutableListOf<ByteArray>()
private var totalBytesWritten = 0 private var totalBytesWritten = 0
@ -904,9 +919,12 @@ class AzureAsrHelper(private val context: Context) {
} }
// 创建新的音频文件 // 创建新的音频文件
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss") val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss")
val timestamp = dateFormat.format(Date()) val timestamp = dateFormat.format(Date())
val filePath = val parentDir = File(context.filesDir, "TranslatAudio")
context.getExternalFilesDir(null)?.absolutePath + "/${fileName}_${timestamp}.wav" if (!parentDir.exists()) {
parentDir.mkdirs()
}
val filePath = File(parentDir, "${fileName}_${timestamp}.wav").absolutePath
currentAudioFile = File(filePath) currentAudioFile = File(filePath)
currentAudioFile?.createNewFile() currentAudioFile?.createNewFile()
@ -918,31 +936,38 @@ class AzureAsrHelper(private val context: Context) {
// 追加音频数据到文件 // 追加音频数据到文件
fos = FileOutputStream(currentAudioFile, true) 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) { internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) { if (fos == null || currentAudioFile == null) return
return // 放入队列,由写线程写入
} writeQueue.offer(buffer.copyOf())
// 复制数据到新数组
val copy = ByteArray(buffer.size)
System.arraycopy(buffer, 0, copy, 0, buffer.size)
// 添加到缓冲区
dataBuffer.add(copy)
totalBytesWritten += buffer.size totalBytesWritten += buffer.size
// 写入文件
try {
fos?.write(copy, 0, buffer.size)
} catch (e: Exception) {
Log.e(tag, "写入音频数据失败: ${e.message}")
}
} }
// 更新文件头生成(修正RIFF长度计算) // 更新文件头生成(修正RIFF长度计算)
private fun generateWavHeader(dataLength: Int): ByteArray { private fun generateWavHeader(dataLength: Int): ByteArray {
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
@ -967,35 +992,36 @@ class AzureAsrHelper(private val context: Context) {
) )
} }
internal fun closeFile() { internal fun closeFile() {
try { try {
if (fos == null || currentAudioFile == null) { if (fos == null || currentAudioFile == null) {
return return
} }
// 关闭文件流 isWriting.set(false)
fos?.close() writeThread?.join(500) // 等待写线程结束
fos?.close()
// 更新WAV文件头
currentAudioFile?.let { file -> // 更新WAV文件头
// 如果文件小于44字节,直接删除 currentAudioFile?.let { file ->
if (file.length() <= 44) { // 如果文件小于44字节,直接删除
file.delete() if (file.length() <= 44) {
Log.d(tag, "音频文件过小已删除: ${file.absolutePath}") file.delete()
} else { Log.d(tag, "音频文件过小已删除: ${file.absolutePath}")
RandomAccessFile(file, "rw").use { raf -> } else {
raf.seek(0) RandomAccessFile(file, "rw").use { raf ->
raf.write(generateWavHeader(totalBytesWritten)) 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
}
}
} }
} }
Loading…
Cancel
Save