From 584e30624db0a6f3c4391c6394bf2d3338e83bd1 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 14 May 2026 09:16:31 +0700 Subject: [PATCH] Use single microphone stream for recording --- lib/screens/mapflow_shell.dart | 96 +++++++++++++++++++--------------- pubspec.lock | 18 +------ pubspec.yaml | 3 +- 3 files changed, 56 insertions(+), 61 deletions(-) diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 1087a2f..e636594 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:latlong2/latlong.dart' hide Path; -import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude; -import 'package:waveform_recorder/waveform_recorder.dart'; +import 'package:record/record.dart' as record; import '../api/mapflow_api.dart'; import '../auth/telegram_login_button.dart'; @@ -98,29 +98,29 @@ class _MapContent extends ConsumerWidget { SafeArea( child: Align( alignment: Alignment.topLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _UserAvatar( - user: state.currentUser, - onLogout: () { - telegram_session.clearMapflowSession(); - ref.invalidate(placeControllerProvider); - telegram_session.reloadApp(); - }, - ), - if (state.currentUser?.isAdmin == true) - _AdminReviewsButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const AdminVoiceExperiencesScreen(), - ), - ), - ), - ], + child: _UserAvatar( + user: state.currentUser, + onLogout: () { + telegram_session.clearMapflowSession(); + ref.invalidate(placeControllerProvider); + telegram_session.reloadApp(); + }, ), ), ), + if (state.currentUser?.isAdmin == true) + SafeArea( + child: Align( + alignment: Alignment.topRight, + child: _AdminReviewsButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const AdminVoiceExperiencesScreen(), + ), + ), + ), + ), + ), if (availableTraits.isNotEmpty) SafeArea( child: Align( @@ -281,7 +281,7 @@ class _AdminReviewsButton extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.only(top: 8, right: 12), child: FilledButton.icon( onPressed: onPressed, icon: const Icon(Icons.table_rows_outlined, size: 18), @@ -711,21 +711,22 @@ class AddExperienceFlow extends ConsumerStatefulWidget { class _AddExperienceFlowState extends ConsumerState { static const _minimumInformationUnits = 16.0; static const _nearbyPlaceRadiusMeters = 200; - - final _api = MapflowApi(); - final _waveController = WaveformRecorderController( - interval: const Duration(milliseconds: 45), - config: const RecordConfig( - numChannels: 1, - sampleRate: 44100, - autoGain: true, - echoCancel: true, - noiseSuppress: true, - ), + static const _recordConfig = record.RecordConfig( + encoder: record.AudioEncoder.wav, + numChannels: 1, + sampleRate: 44100, + autoGain: true, + echoCancel: true, + noiseSuppress: true, ); + final _api = MapflowApi(); + final _audioRecorder = record.AudioRecorder(); + final _recordStopwatch = Stopwatch(); + Future>? _nearbyPlacesFuture; - StreamSubscription? _amplitudeSub; + StreamSubscription? _amplitudeSub; + XFile? _recordedFile; var _step = 0; var _informationUnits = 0.0; var _recording = false; @@ -744,7 +745,7 @@ class _AddExperienceFlowState extends ConsumerState { @override void dispose() { _amplitudeSub?.cancel(); - _waveController.dispose(); + _audioRecorder.dispose(); super.dispose(); } @@ -770,10 +771,17 @@ class _AddExperienceFlowState extends ConsumerState { } Future _startRecording() async { - await _waveController.startRecording(); + _recordedFile = null; + final path = 'mapflow-${DateTime.now().microsecondsSinceEpoch}.wav'; + await _audioRecorder.start(_recordConfig, path: path); await _amplitudeSub?.cancel(); + _recordStopwatch + ..reset() + ..start(); _lastInformationAt = DateTime.now(); - _amplitudeSub = _waveController.amplitudeStream.listen(_handleAmplitude); + _amplitudeSub = _audioRecorder + .onAmplitudeChanged(const Duration(milliseconds: 45)) + .listen(_handleAmplitude); setState(() { _micAllowed = true; @@ -786,7 +794,11 @@ class _AddExperienceFlowState extends ConsumerState { Future _stopRecording() async { await _amplitudeSub?.cancel(); _amplitudeSub = null; - await _waveController.stopRecording(); + final path = await _audioRecorder.stop(); + _recordStopwatch.stop(); + if (path != null && path.isNotEmpty) { + _recordedFile = XFile(path, mimeType: 'audio/wav'); + } _lastInformationAt = null; if (!mounted) { return; @@ -797,7 +809,7 @@ class _AddExperienceFlowState extends ConsumerState { }); } - void _handleAmplitude(Amplitude amplitude) { + void _handleAmplitude(record.Amplitude amplitude) { final currentDb = amplitude.current; final now = DateTime.now(); final level = _normalizeDbLevel(currentDb); @@ -812,7 +824,7 @@ class _AddExperienceFlowState extends ConsumerState { }); ref .read(placeControllerProvider.notifier) - .setReviewDuration(_waveController.timeElapsed); + .setReviewDuration(_recordStopwatch.elapsed); } double _normalizeDbLevel(double currentDb) { @@ -880,7 +892,7 @@ class _AddExperienceFlowState extends ConsumerState { onSelect: (place) async { setState(() => _submitting = true); controller.setReviewPlace(place.name); - final file = _waveController.file; + final file = _recordedFile; if (file == null) { throw StateError('Voice recording file is required.'); } diff --git a/pubspec.lock b/pubspec.lock index cfdebb3..9832987 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,7 +106,7 @@ packages: source: hosted version: "1.15.0" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" @@ -925,22 +925,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - waveform_flutter: - dependency: "direct main" - description: - name: waveform_flutter - sha256: "08c9e98d4cf119428d8b3c083ed42c11c468623eaffdf30420ae38e36662922a" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - waveform_recorder: - dependency: "direct main" - description: - name: waveform_recorder - sha256: "1ca0a19b143d1bdef2adfb3d28f0627c18aee5285235c8cf81a89bf29a0420e1" - url: "https://pub.dev" - source: hosted - version: "1.8.0" web: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 85f4d88..8efd4eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,9 +41,8 @@ dependencies: web: ^1.1.1 geolocator: ^14.0.2 record: ^6.2.0 - waveform_recorder: ^1.8.0 - waveform_flutter: ^1.2.0 flutter_svg: ^2.3.0 + cross_file: ^0.3.5+2 dev_dependencies: flutter_test: