import 'dart:io'; import 'dart:typed_data'; import 'package:electronic_assistant/base/base_controller.dart'; import 'package:electronic_assistant/data/consts/error_code.dart'; import 'package:electronic_assistant/data/repositories/talk_repository.dart'; import 'package:electronic_assistant/dialog/alert_dialog.dart'; import 'package:electronic_assistant/module/record/constants.dart'; import 'package:electronic_assistant/module/talk/view.dart'; import 'package:electronic_assistant/router/app_pages.dart'; import 'package:electronic_assistant/utils/http_handler.dart'; import 'package:electronic_assistant/utils/mmkv_util.dart'; import 'package:electronic_assistant/utils/toast_util.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:uuid/uuid.dart'; import '../../utils/pcm_wav_converter.dart'; import '../../widget/frame_animation_view.dart'; class RecordController extends BaseController { static const String keyLastRecordId = "last_record_id"; static const int minRecordDuration = 3; final FrameAnimationController frameAnimationController = FrameAnimationController(autoPlay: false); final Rx currentStatus = RecordStatus.pending.obs; final RxDouble currentDuration = 0.0.obs; final AudioRecorder _record = AudioRecorder(); final RecordConfig _recordConfig = const RecordConfig( encoder: AudioEncoder.pcm16bits, bitRate: 128000, sampleRate: 44100, numChannels: 2); late final String _lastRecordId; @override void onInit() { super.onInit(); _initLastRecordId(); _initLastRecordStatus(); } @override void onClose() { super.onClose(); _record.dispose(); } void _initLastRecordId() { String? lastRecordId = KVUtil.getString(keyLastRecordId, null); if (lastRecordId == null || lastRecordId.isEmpty) { _lastRecordId = const Uuid().v4(); KVUtil.putString(keyLastRecordId, _lastRecordId); } else { _lastRecordId = lastRecordId; } } Future _initLastRecordStatus() async { var currentRecordFile = await _getCurrentRecordFile(); var fileLength = currentRecordFile.lengthSync(); if (currentRecordFile.existsSync() && fileLength > 0) { _changeRecordStatus(RecordStatus.paused); currentDuration.value = _getPcmDuration( fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels); } } void addShortcut() {} void onBackClick() { if (currentStatus.value == RecordStatus.pending || currentStatus.value == RecordStatus.paused) { Get.back(); } else { EAAlertDialog.show( title: "是否保存当前录音?", confirmText: "确定", cancelText: "取消", confirmOnTap: () { _saveCurrentRecord(); EAAlertDialog.dismiss(); }, cancelOnTap: () { EAAlertDialog.dismiss(); _stopRecord().then((_) => Get.back()); }, ); } } void onActionClick() { RecordStatus nextStatus = currentStatus.value.nextStatus; if (nextStatus == RecordStatus.recording) { _startOrContinueRecord(); } else { _stopRecord(); } } void onCancelClick() { if (currentStatus.value == RecordStatus.pending) { return; } EAAlertDialog.show( title: "是否删除当前录音?", confirmText: "删除", cancelText: "取消", confirmOnTap: () { _deleteCurrentRecord(); EAAlertDialog.dismiss(); }, cancelOnTap: () { EAAlertDialog.dismiss(); }, ); } void onSaveClick() { if (currentStatus.value == RecordStatus.pending) { return; } _saveCurrentRecord(); } Future _startOrContinueRecord() async { bool hasPermission = await _record.hasPermission(); if (!hasPermission) { _onRecordPermissionDenied(); return; } File targetFile = await _getCurrentRecordFile(); Stream recordStream = await _record.startStream(_recordConfig); _changeRecordStatus(RecordStatus.recording); recordStream.listen((data) async { targetFile.writeAsBytesSync(data, mode: FileMode.append); currentDuration.value = currentDuration.value + _getPcmDuration(data.length, _recordConfig.sampleRate, 16, _recordConfig.numChannels); }, onDone: () { _changeRecordStatus(RecordStatus.paused); }, onError: (error) { _changeRecordStatus(RecordStatus.paused); }); } _onRecordPermissionDenied() {} Future _stopRecord() { return _record .pause() .then((_) => _changeRecordStatus(RecordStatus.paused)); } Future _getCurrentRecordFile() async { Directory documentDir = await getApplicationDocumentsDirectory(); File file = File("${documentDir.path}/.atmob/record/$_lastRecordId"); if (!file.existsSync()) { file.createSync(recursive: true); } return file; } double _getPcmDuration( int fileSize, int sampleRate, int bitDepth, int channels) { final bytesPerSecond = sampleRate * (bitDepth / 8) * channels; final durationInSeconds = fileSize / bytesPerSecond; return durationInSeconds; } Future _deleteCurrentRecord() async { await _stopRecord(); _getCurrentRecordFile().then((file) { if (file.existsSync()) { file.deleteSync(); } }).then((_) { currentDuration.value = 0; _changeRecordStatus(RecordStatus.pending); }); } Future _saveCurrentRecord() async { final currentDurationValue = currentDuration.value; if (currentDurationValue < minRecordDuration) { ToastUtil.showToast("录音时长不足$minRecordDuration秒"); return; } await _stopRecord(); talkRepository .talkCreate(_lastRecordId, currentDuration.value.toInt()) .then((talkInfo) async { //添加新的录音到最新记录 talkRepository.addNewTalkData(talkInfo); File pcmFile = await _getCurrentRecordFile(); if (pcmFile.existsSync()) { File wavFile = await getRecordFile(talkInfo.id); PcmWavConverter.convert(pcmFile, wavFile, _recordConfig.sampleRate, _recordConfig.numChannels, 16); pcmFile.delete(); KVUtil.putString(keyLastRecordId, ""); Get.back(); TalkPage.start(talkInfo); } else { throw Exception("pcm file not found"); } }).catchError((error) { if (error is ServerErrorException) { ToastUtil.showToast("${error.message}"); if (error.code == ErrorCode.errorCodeNoLogin) { Get.toNamed(RoutePath.login)?.then((loginSuccess) { loginSuccess != null && loginSuccess ? _saveCurrentRecord() : null; }); } } else { ToastUtil.showToast("保存失败, 请重试"); } }); } void _changeRecordStatus(RecordStatus status) { currentStatus.value = status; status == RecordStatus.recording ? frameAnimationController.play() : null; } /// 获取录音文件地址 static Future getRecordFile(String talkId) async { Directory documentDir = await getApplicationDocumentsDirectory(); return File("${documentDir.path}/.atmob/record/$talkId.wav"); } /// 判断是否有未上传的录音 static Future hasUnUploadRecord() async { String? lastRecordId = KVUtil.getString(keyLastRecordId, null); if (lastRecordId == null || lastRecordId.isEmpty) { return false; } Directory documentDir = await getApplicationDocumentsDirectory(); File file = File("${documentDir.path}/.atmob/record/$lastRecordId"); return await file.exists() && await file.length() > 0; } }