controller.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import 'dart:io';
  2. import 'dart:typed_data';
  3. import 'package:electronic_assistant/base/base_controller.dart';
  4. import 'package:electronic_assistant/data/repositories/talk_repository.dart';
  5. import 'package:electronic_assistant/dialog/alert_dialog.dart';
  6. import 'package:electronic_assistant/module/record/constants.dart';
  7. import 'package:electronic_assistant/module/talk/view.dart';
  8. import 'package:electronic_assistant/utils/http_handler.dart';
  9. import 'package:electronic_assistant/utils/mmkv_util.dart';
  10. import 'package:electronic_assistant/utils/toast_util.dart';
  11. import 'package:get/get.dart';
  12. import 'package:path_provider/path_provider.dart';
  13. import 'package:record/record.dart';
  14. import 'package:uuid/uuid.dart';
  15. import '../../utils/pcm_wav_converter.dart';
  16. import '../../widget/frame_animation_view.dart';
  17. class RecordController extends BaseController {
  18. static const String keyLastRecordId = "last_record_id";
  19. static const int minRecordDuration = 3;
  20. final FrameAnimationController frameAnimationController =
  21. FrameAnimationController(autoPlay: false);
  22. final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
  23. final RxDouble currentDuration = 0.0.obs;
  24. final AudioRecorder _record = AudioRecorder();
  25. final RecordConfig _recordConfig = const RecordConfig(
  26. encoder: AudioEncoder.pcm16bits,
  27. bitRate: 128000,
  28. sampleRate: 44100,
  29. numChannels: 2);
  30. late final String _lastRecordId;
  31. @override
  32. void onInit() {
  33. super.onInit();
  34. _initLastRecordId();
  35. _initLastRecordStatus();
  36. }
  37. @override
  38. void onClose() {
  39. super.onClose();
  40. _record.dispose();
  41. }
  42. void _initLastRecordId() {
  43. String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
  44. if (lastRecordId == null || lastRecordId.isEmpty) {
  45. _lastRecordId = const Uuid().v4();
  46. KVUtil.putString(keyLastRecordId, _lastRecordId);
  47. } else {
  48. _lastRecordId = lastRecordId;
  49. }
  50. }
  51. Future<void> _initLastRecordStatus() async {
  52. var currentRecordFile = await _getCurrentRecordFile();
  53. var fileLength = currentRecordFile.lengthSync();
  54. if (currentRecordFile.existsSync() && fileLength > 0) {
  55. _changeRecordStatus(RecordStatus.paused);
  56. currentDuration.value = _getPcmDuration(
  57. fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
  58. }
  59. }
  60. void addShortcut() {}
  61. void onBackClick() {
  62. if (currentStatus.value == RecordStatus.pending ||
  63. currentStatus.value == RecordStatus.paused) {
  64. Get.back();
  65. } else {
  66. EAAlertDialog.show(
  67. title: "是否保存当前录音?",
  68. confirmText: "确定",
  69. cancelText: "取消",
  70. confirmOnTap: () {
  71. _saveCurrentRecord();
  72. EAAlertDialog.dismiss();
  73. },
  74. cancelOnTap: () {
  75. EAAlertDialog.dismiss();
  76. _stopRecord().then((_) => Get.back());
  77. },
  78. );
  79. }
  80. }
  81. void onActionClick() {
  82. RecordStatus nextStatus = currentStatus.value.nextStatus;
  83. if (nextStatus == RecordStatus.recording) {
  84. _startOrContinueRecord();
  85. } else {
  86. _stopRecord();
  87. }
  88. }
  89. void onCancelClick() {
  90. if (currentStatus.value == RecordStatus.pending) {
  91. return;
  92. }
  93. EAAlertDialog.show(
  94. title: "是否删除当前录音?",
  95. confirmText: "删除",
  96. cancelText: "取消",
  97. confirmOnTap: () {
  98. _deleteCurrentRecord();
  99. EAAlertDialog.dismiss();
  100. },
  101. cancelOnTap: () {
  102. EAAlertDialog.dismiss();
  103. },
  104. );
  105. }
  106. void onSaveClick() {
  107. if (currentStatus.value == RecordStatus.pending) {
  108. return;
  109. }
  110. _saveCurrentRecord();
  111. }
  112. Future<void> _startOrContinueRecord() async {
  113. bool hasPermission = await _record.hasPermission();
  114. if (!hasPermission) {
  115. _onRecordPermissionDenied();
  116. return;
  117. }
  118. File targetFile = await _getCurrentRecordFile();
  119. Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
  120. _changeRecordStatus(RecordStatus.recording);
  121. recordStream.listen((data) async {
  122. targetFile.writeAsBytesSync(data, mode: FileMode.append);
  123. currentDuration.value = currentDuration.value +
  124. _getPcmDuration(data.length, _recordConfig.sampleRate, 16,
  125. _recordConfig.numChannels);
  126. }, onDone: () {
  127. _changeRecordStatus(RecordStatus.paused);
  128. }, onError: (error) {
  129. _changeRecordStatus(RecordStatus.paused);
  130. });
  131. }
  132. _onRecordPermissionDenied() {}
  133. Future<void> _stopRecord() {
  134. return _record
  135. .pause()
  136. .then((_) => _changeRecordStatus(RecordStatus.paused));
  137. }
  138. Future<File> _getCurrentRecordFile() async {
  139. Directory documentDir = await getApplicationDocumentsDirectory();
  140. File file = File("${documentDir.path}/.atmob/record/$_lastRecordId");
  141. if (!file.existsSync()) {
  142. file.createSync(recursive: true);
  143. }
  144. return file;
  145. }
  146. double _getPcmDuration(
  147. int fileSize, int sampleRate, int bitDepth, int channels) {
  148. final bytesPerSecond = sampleRate * (bitDepth / 8) * channels;
  149. final durationInSeconds = fileSize / bytesPerSecond;
  150. return durationInSeconds;
  151. }
  152. Future<void> _deleteCurrentRecord() async {
  153. await _stopRecord();
  154. _getCurrentRecordFile().then((file) {
  155. if (file.existsSync()) {
  156. file.deleteSync();
  157. }
  158. }).then((_) {
  159. currentDuration.value = 0;
  160. _changeRecordStatus(RecordStatus.pending);
  161. });
  162. }
  163. Future<void> _saveCurrentRecord() async {
  164. final currentDurationValue = currentDuration.value;
  165. if (currentDurationValue < minRecordDuration) {
  166. ToastUtil.showToast("录音时长不足$minRecordDuration秒");
  167. return;
  168. }
  169. await _stopRecord();
  170. talkRepository
  171. .talkCreate(_lastRecordId, currentDuration.value.toInt())
  172. .then((talkInfo) async {
  173. //添加新的录音到最新记录
  174. talkRepository.addNewTalkData(talkInfo);
  175. File pcmFile = await _getCurrentRecordFile();
  176. if (pcmFile.existsSync()) {
  177. File wavFile = await getRecordFile(talkInfo.id);
  178. PcmWavConverter.convert(pcmFile, wavFile, _recordConfig.sampleRate,
  179. _recordConfig.numChannels, 16);
  180. pcmFile.delete();
  181. KVUtil.putString(keyLastRecordId, "");
  182. Get.back();
  183. TalkPage.start(talkInfo);
  184. } else {
  185. throw Exception("pcm file not found");
  186. }
  187. }).catchError((error) {
  188. if (error is ServerErrorException) {
  189. ToastUtil.showToast("${error.message}");
  190. } else {
  191. ToastUtil.showToast("保存失败, 请重试");
  192. }
  193. });
  194. }
  195. void _changeRecordStatus(RecordStatus status) {
  196. currentStatus.value = status;
  197. status == RecordStatus.recording ? frameAnimationController.play() : null;
  198. }
  199. static Future<File> getRecordFile(String talkId) async {
  200. Directory documentDir = await getApplicationDocumentsDirectory();
  201. return File("${documentDir.path}/.atmob/record/$talkId.wav");
  202. }
  203. }