controller.dart 7.6 KB

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