|
|
@@ -1,9 +1,8 @@
|
|
|
import 'dart:async';
|
|
|
-import 'dart:convert';
|
|
|
import 'dart:io';
|
|
|
+import 'dart:typed_data';
|
|
|
|
|
|
import 'package:record/record.dart';
|
|
|
-import 'package:permission_handler/permission_handler.dart';
|
|
|
|
|
|
import 'package:asr_client/common/constants.dart';
|
|
|
|
|
|
@@ -14,7 +13,7 @@ abstract interface class AudioCaptureService {
|
|
|
Future<void> openSettings();
|
|
|
Future<void> startRecording();
|
|
|
Future<void> stopRecording();
|
|
|
- Future<String> readLatestChunkAsBase64();
|
|
|
+ Uint8List? takeBuffer();
|
|
|
bool get isRecording;
|
|
|
void dispose();
|
|
|
}
|
|
|
@@ -25,8 +24,8 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
|
|
|
final AudioRecorder _recorder;
|
|
|
final _levelController = StreamController<double>.broadcast();
|
|
|
- StreamSubscription? _amplitudeSubscription;
|
|
|
- String? _latestPath;
|
|
|
+ StreamSubscription<Amplitude>? _amplitudeSubscription;
|
|
|
+ String? _path;
|
|
|
var _readOffset = 0;
|
|
|
bool _isRecording = false;
|
|
|
|
|
|
@@ -38,17 +37,6 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
|
|
|
@override
|
|
|
Future<bool> requestPermission() async {
|
|
|
- // Use record's native hasPermission() which reads AVAudioSession directly.
|
|
|
- // permission_handler is broken on iOS 18 Simulator (always returns
|
|
|
- // permanentlyDenied even when TCC.db shows granted).
|
|
|
- final hasPermission = await _recorder.hasPermission();
|
|
|
- if (hasPermission) return true;
|
|
|
-
|
|
|
- // Fallback: trigger permission_handler to show the system dialog.
|
|
|
- final status = await Permission.microphone.request();
|
|
|
- if (status.isGranted) return true;
|
|
|
-
|
|
|
- // Last resort: re-check record in case the dialog just granted it.
|
|
|
return await _recorder.hasPermission();
|
|
|
}
|
|
|
|
|
|
@@ -56,7 +44,7 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
Future<bool> get isPermissionPermanentlyDenied async => false;
|
|
|
|
|
|
@override
|
|
|
- Future<void> openSettings() => openAppSettings();
|
|
|
+ Future<void> openSettings() async {}
|
|
|
|
|
|
@override
|
|
|
Future<void> startRecording() async {
|
|
|
@@ -69,8 +57,9 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
final dir = await Directory.systemTemp.createTemp('asr_');
|
|
|
final path =
|
|
|
'${dir.path}/asr_buffer_${DateTime.now().millisecondsSinceEpoch}.pcm';
|
|
|
- _latestPath = path;
|
|
|
+ _path = path;
|
|
|
_readOffset = 0;
|
|
|
+
|
|
|
await _recorder.start(
|
|
|
const RecordConfig(
|
|
|
encoder: AudioEncoder.pcm16bits,
|
|
|
@@ -80,6 +69,13 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
path: path,
|
|
|
);
|
|
|
|
|
|
+ _amplitudeSubscription = _recorder.onAmplitudeChanged(
|
|
|
+ const Duration(milliseconds: 200),
|
|
|
+ ).listen((amp) {
|
|
|
+ final normalized = ((amp.current + 160) / 160).clamp(0.0, 1.0);
|
|
|
+ _levelController.add(normalized);
|
|
|
+ });
|
|
|
+
|
|
|
_isRecording = true;
|
|
|
}
|
|
|
|
|
|
@@ -88,40 +84,34 @@ final class AudioCaptureServiceImpl implements AudioCaptureService {
|
|
|
_amplitudeSubscription?.cancel();
|
|
|
_amplitudeSubscription = null;
|
|
|
if (_isRecording) {
|
|
|
- _latestPath = await _recorder.stop();
|
|
|
+ await _recorder.stop();
|
|
|
_isRecording = false;
|
|
|
_levelController.add(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
- Future<String> readLatestChunkAsBase64() async {
|
|
|
- final path = _latestPath;
|
|
|
- if (path == null) return '';
|
|
|
-
|
|
|
+ Uint8List? takeBuffer() {
|
|
|
+ final path = _path;
|
|
|
+ if (path == null) return null;
|
|
|
final file = File(path);
|
|
|
- if (!await file.exists()) return '';
|
|
|
+ if (!file.existsSync()) return null;
|
|
|
|
|
|
RandomAccessFile? raf;
|
|
|
try {
|
|
|
- raf = await file.open(mode: FileMode.read);
|
|
|
- final length = await raf.length();
|
|
|
- if (length <= _readOffset) return '';
|
|
|
+ raf = file.openSync(mode: FileMode.read);
|
|
|
+ final length = raf.lengthSync();
|
|
|
+ if (length <= _readOffset) return null;
|
|
|
|
|
|
final available = length - _readOffset;
|
|
|
- final count = available > AppConstants.maxAudioChunkBytes
|
|
|
- ? AppConstants.maxAudioChunkBytes
|
|
|
- : available;
|
|
|
- if (count <= 0) return '';
|
|
|
-
|
|
|
- await raf.setPosition(_readOffset);
|
|
|
- final bytes = await raf.read(count);
|
|
|
- _readOffset += count;
|
|
|
- return base64Encode(bytes);
|
|
|
- } on Exception catch (_) {
|
|
|
- return '';
|
|
|
+ raf.setPositionSync(_readOffset);
|
|
|
+ final bytes = raf.readSync(available);
|
|
|
+ _readOffset += available;
|
|
|
+ return bytes;
|
|
|
+ } catch (_) {
|
|
|
+ return null;
|
|
|
} finally {
|
|
|
- await raf?.close();
|
|
|
+ raf?.closeSync();
|
|
|
}
|
|
|
}
|
|
|
|