audio_capture_service.dart 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:record/record.dart';
  5. import 'package:asr_client/common/constants.dart';
  6. abstract interface class AudioCaptureService {
  7. Stream<double> get audioLevelStream;
  8. Future<bool> requestPermission();
  9. Future<bool> get isPermissionPermanentlyDenied;
  10. Future<void> openSettings();
  11. Future<void> startRecording();
  12. Future<void> stopRecording();
  13. Uint8List? takeBuffer();
  14. bool get isRecording;
  15. void dispose();
  16. }
  17. final class AudioCaptureServiceImpl implements AudioCaptureService {
  18. AudioCaptureServiceImpl({AudioRecorder? recorder})
  19. : _recorder = recorder ?? AudioRecorder();
  20. final AudioRecorder _recorder;
  21. final _levelController = StreamController<double>.broadcast();
  22. StreamSubscription<Amplitude>? _amplitudeSubscription;
  23. String? _path;
  24. var _readOffset = 0;
  25. bool _isRecording = false;
  26. @override
  27. Stream<double> get audioLevelStream => _levelController.stream;
  28. @override
  29. bool get isRecording => _isRecording;
  30. @override
  31. Future<bool> requestPermission() async {
  32. return await _recorder.hasPermission();
  33. }
  34. @override
  35. Future<bool> get isPermissionPermanentlyDenied async => false;
  36. @override
  37. Future<void> openSettings() async {}
  38. @override
  39. Future<void> startRecording() async {
  40. if (_isRecording) return;
  41. final hasPermission = await _recorder.hasPermission();
  42. if (!hasPermission) {
  43. throw Exception('Microphone permission not granted');
  44. }
  45. final dir = await Directory.systemTemp.createTemp('asr_');
  46. final path =
  47. '${dir.path}/asr_buffer_${DateTime.now().millisecondsSinceEpoch}.pcm';
  48. _path = path;
  49. _readOffset = 0;
  50. await _recorder.start(
  51. const RecordConfig(
  52. encoder: AudioEncoder.pcm16bits,
  53. sampleRate: AppConstants.targetSampleRate,
  54. numChannels: 1,
  55. ),
  56. path: path,
  57. );
  58. _amplitudeSubscription = _recorder.onAmplitudeChanged(
  59. const Duration(milliseconds: 200),
  60. ).listen((amp) {
  61. final normalized = ((amp.current + 160) / 160).clamp(0.0, 1.0);
  62. _levelController.add(normalized);
  63. });
  64. _isRecording = true;
  65. }
  66. @override
  67. Future<void> stopRecording() async {
  68. _amplitudeSubscription?.cancel();
  69. _amplitudeSubscription = null;
  70. if (_isRecording) {
  71. await _recorder.stop();
  72. _isRecording = false;
  73. _levelController.add(0);
  74. }
  75. }
  76. @override
  77. Uint8List? takeBuffer() {
  78. final path = _path;
  79. if (path == null) return null;
  80. final file = File(path);
  81. if (!file.existsSync()) return null;
  82. RandomAccessFile? raf;
  83. try {
  84. raf = file.openSync(mode: FileMode.read);
  85. final length = raf.lengthSync();
  86. if (length <= _readOffset) return null;
  87. final available = length - _readOffset;
  88. raf.setPositionSync(_readOffset);
  89. final bytes = raf.readSync(available);
  90. _readOffset += available;
  91. return bytes;
  92. } catch (_) {
  93. return null;
  94. } finally {
  95. raf?.closeSync();
  96. }
  97. }
  98. @override
  99. void dispose() {
  100. _amplitudeSubscription?.cancel();
  101. _levelController.close();
  102. _recorder.dispose();
  103. }
  104. }