|
|
@@ -1,18 +1,198 @@
|
|
|
+import 'dart:io';
|
|
|
+import 'dart:typed_data';
|
|
|
+
|
|
|
import 'package:electronic_assistant/base/base_controller.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/utils/mmkv_util.dart';
|
|
|
+import 'package:flutter/cupertino.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 {
|
|
|
- final FrameAnimationController frameAnimationController =
|
|
|
- FrameAnimationController();
|
|
|
+ static const String keyLastRecordId = "last_record_id";
|
|
|
|
|
|
- final RxString recordStatus = '准备开始录音'.obs;
|
|
|
+ final FrameAnimationController frameAnimationController =
|
|
|
+ FrameAnimationController(autoPlay: false);
|
|
|
+ final Rx<RecordStatus> 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ void _initLastRecordId() {
|
|
|
+ String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
|
|
|
+ if (lastRecordId == null || lastRecordId.isEmpty) {
|
|
|
+ this.lastRecordId = const Uuid().v4();
|
|
|
+ KVUtil.putString(keyLastRecordId, this.lastRecordId);
|
|
|
+ } else {
|
|
|
+ this.lastRecordId = lastRecordId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _initLastRecordStatus() async {
|
|
|
+ var currentRecordFile = await _getCurrentRecordFile();
|
|
|
+ var fileLength = currentRecordFile.lengthSync();
|
|
|
+ if (currentRecordFile.existsSync() && fileLength > 0) {
|
|
|
+ currentStatus.value = RecordStatus.paused;
|
|
|
+ currentDuration.value = await _getPcmDuration(
|
|
|
+ fileLength, recordConfig.sampleRate, 16, recordConfig.numChannels);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
void addShortcut() {}
|
|
|
+
|
|
|
+ void onBackClick(BuildContext context) {
|
|
|
+ if (currentStatus.value == RecordStatus.pending ||
|
|
|
+ currentStatus.value == RecordStatus.paused) {
|
|
|
+ Navigator.pop(context);
|
|
|
+ } else {
|
|
|
+ EAAlertDialog.show(
|
|
|
+ title: "是否保存当前录音?",
|
|
|
+ confirmText: "确定",
|
|
|
+ cancelText: "取消",
|
|
|
+ confirmOnTap: () {
|
|
|
+ _saveCurrentRecord();
|
|
|
+ EAAlertDialog.dismiss();
|
|
|
+ },
|
|
|
+ cancelOnTap: () {
|
|
|
+ EAAlertDialog.dismiss();
|
|
|
+ Navigator.pop(context);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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<void> _startOrContinueRecord() async {
|
|
|
+ bool hasPermission = await record.hasPermission();
|
|
|
+ if (!hasPermission) {
|
|
|
+ _onRecordPermissionDenied();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ File targetFile = await _getCurrentRecordFile();
|
|
|
+ Stream<Uint8List> recordStream = await record.startStream(recordConfig);
|
|
|
+ currentStatus.value = RecordStatus.recording;
|
|
|
+ recordStream.listen((data) async {
|
|
|
+ targetFile.writeAsBytesSync(data, mode: FileMode.append);
|
|
|
+ currentDuration.value = currentDuration.value +
|
|
|
+ await _getPcmDuration(data.length, recordConfig.sampleRate, 16,
|
|
|
+ recordConfig.numChannels);
|
|
|
+ }, onDone: () {
|
|
|
+ currentStatus.value = RecordStatus.paused;
|
|
|
+ }, onError: (error) {
|
|
|
+ currentStatus.value = RecordStatus.paused;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ _onRecordPermissionDenied() {}
|
|
|
+
|
|
|
+ Future<void> _stopRecord() {
|
|
|
+ return record.stop().then((_) => currentStatus.value = RecordStatus.paused);
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<File> _getCurrentRecordFile() async {
|
|
|
+ Directory documentDir = await getApplicationDocumentsDirectory();
|
|
|
+ File file = File("${documentDir.path}/.atmob/record/$lastRecordId");
|
|
|
+ if (!file.existsSync()) {
|
|
|
+ file.createSync(recursive: true);
|
|
|
+ }
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<double> _getPcmDuration(
|
|
|
+ int fileSize, int sampleRate, int bitDepth, int channels) async {
|
|
|
+ final bytesPerSecond = sampleRate * (bitDepth / 8) * channels;
|
|
|
+ final durationInSeconds = fileSize / bytesPerSecond;
|
|
|
+ return durationInSeconds;
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _deleteCurrentRecord() async {
|
|
|
+ await _stopRecord();
|
|
|
+ _getCurrentRecordFile().then((file) {
|
|
|
+ if (file.existsSync()) {
|
|
|
+ file.deleteSync();
|
|
|
+ }
|
|
|
+ }).then((_) {
|
|
|
+ currentDuration.value = 0;
|
|
|
+ currentStatus.value = RecordStatus.pending;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _saveCurrentRecord() async {
|
|
|
+ await _stopRecord();
|
|
|
+ talkRepository
|
|
|
+ .talkCreate(lastRecordId, currentDuration.value.toInt())
|
|
|
+ .then((talkInfo) async {
|
|
|
+ File pcmFile = await _getCurrentRecordFile();
|
|
|
+ if (pcmFile.existsSync()) {
|
|
|
+ File wavFile = await getRecordFile(talkInfo.id);
|
|
|
+ PcmWavConverter.convert(pcmFile, wavFile, recordConfig.sampleRate,
|
|
|
+ recordConfig.numChannels, 16);
|
|
|
+ pcmFile.delete();
|
|
|
+ Get.back();
|
|
|
+ TalkPage.start(talkInfo);
|
|
|
+ } else {
|
|
|
+ throw Exception("pcm file not found");
|
|
|
+ }
|
|
|
+ }).catchError((error) {
|
|
|
+ debugPrint(error);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ static Future<File> getRecordFile(String talkId) async {
|
|
|
+ Directory documentDir = await getApplicationDocumentsDirectory();
|
|
|
+ return File("${documentDir.path}/.atmob/record/$talkId.wav");
|
|
|
+ }
|
|
|
}
|