Browse Source

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

weicu
tanlongsheng 1 year ago
parent
commit
8d5bbe2027
  1. 73
      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

73
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<String> selected = [];
final appDocDir = getApplicationSupportDirectory();
// final directory = Directory(
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List<FileSystemEntity> 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<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 {
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<String, Object?> data = {
@ -145,7 +206,11 @@ class MeetingController extends GetxController {
Future<String> _saveFile(PlatformFile file) async {
final directory = await getApplicationDocumentsDirectory();
final String filePath = '${directory.path}/${file.name}';
final directoryDir = Directory('${directory.path}/ExternalAudio');
if (!await directoryDir.exists()) {
await directoryDir.create(recursive: true);
}
final String filePath = '${directory.path}/ExternalAudio/${file.name}';
final File localFile = File(filePath);
await localFile.writeAsBytes(file.bytes!);
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 {
// 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径
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 = [];
controller.loadTranslatAudio();
showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -236,19 +220,20 @@ class MeetingView extends GetView<MeetingController> {
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<MeetingController> {
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)),

5
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);
}

6
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);
// 更新本地头像路径

51
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<PermissionStatus> checkPermission(Permission permission) async {
return await permission.status;
}
// 请求单个权限
Future<PermissionStatus> requestPermission(Permission permission) async {
return await permission.request();
}
// 打开应用设置
void openSettings() {
openAppSettings();
}
// 检查所有权限状态并返回结果
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
Map<Permission, PermissionStatus> 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);
// 更新本地头像路径

7
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,

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.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<SpeechRecognitionEventArgs> { _, 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<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
// 用于存储音频数据的缓冲区
private val dataBuffer = mutableListOf<ByteArray>()
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
}
}
}
}
Loading…
Cancel
Save