controller.dart 6.5 KB

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