Use single microphone stream for recording
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m50s

This commit is contained in:
Ruslan Bakiev
2026-05-14 09:16:31 +07:00
parent 21945b2335
commit 584e30624d
3 changed files with 56 additions and 61 deletions

View File

@@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:latlong2/latlong.dart' hide Path; import 'package:latlong2/latlong.dart' hide Path;
import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude; import 'package:record/record.dart' as record;
import 'package:waveform_recorder/waveform_recorder.dart';
import '../api/mapflow_api.dart'; import '../api/mapflow_api.dart';
import '../auth/telegram_login_button.dart'; import '../auth/telegram_login_button.dart';
@@ -98,29 +98,29 @@ class _MapContent extends ConsumerWidget {
SafeArea( SafeArea(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Row( child: _UserAvatar(
mainAxisSize: MainAxisSize.min, user: state.currentUser,
children: [ onLogout: () {
_UserAvatar( telegram_session.clearMapflowSession();
user: state.currentUser, ref.invalidate(placeControllerProvider);
onLogout: () { telegram_session.reloadApp();
telegram_session.clearMapflowSession(); },
ref.invalidate(placeControllerProvider);
telegram_session.reloadApp();
},
),
if (state.currentUser?.isAdmin == true)
_AdminReviewsButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AdminVoiceExperiencesScreen(),
),
),
),
],
), ),
), ),
), ),
if (state.currentUser?.isAdmin == true)
SafeArea(
child: Align(
alignment: Alignment.topRight,
child: _AdminReviewsButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AdminVoiceExperiencesScreen(),
),
),
),
),
),
if (availableTraits.isNotEmpty) if (availableTraits.isNotEmpty)
SafeArea( SafeArea(
child: Align( child: Align(
@@ -281,7 +281,7 @@ class _AdminReviewsButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8, right: 12),
child: FilledButton.icon( child: FilledButton.icon(
onPressed: onPressed, onPressed: onPressed,
icon: const Icon(Icons.table_rows_outlined, size: 18), icon: const Icon(Icons.table_rows_outlined, size: 18),
@@ -711,21 +711,22 @@ class AddExperienceFlow extends ConsumerStatefulWidget {
class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> { class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
static const _minimumInformationUnits = 16.0; static const _minimumInformationUnits = 16.0;
static const _nearbyPlaceRadiusMeters = 200; static const _nearbyPlaceRadiusMeters = 200;
static const _recordConfig = record.RecordConfig(
final _api = MapflowApi(); encoder: record.AudioEncoder.wav,
final _waveController = WaveformRecorderController( numChannels: 1,
interval: const Duration(milliseconds: 45), sampleRate: 44100,
config: const RecordConfig( autoGain: true,
numChannels: 1, echoCancel: true,
sampleRate: 44100, noiseSuppress: true,
autoGain: true,
echoCancel: true,
noiseSuppress: true,
),
); );
final _api = MapflowApi();
final _audioRecorder = record.AudioRecorder();
final _recordStopwatch = Stopwatch();
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture; Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
StreamSubscription<Amplitude>? _amplitudeSub; StreamSubscription<record.Amplitude>? _amplitudeSub;
XFile? _recordedFile;
var _step = 0; var _step = 0;
var _informationUnits = 0.0; var _informationUnits = 0.0;
var _recording = false; var _recording = false;
@@ -744,7 +745,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@override @override
void dispose() { void dispose() {
_amplitudeSub?.cancel(); _amplitudeSub?.cancel();
_waveController.dispose(); _audioRecorder.dispose();
super.dispose(); super.dispose();
} }
@@ -770,10 +771,17 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
} }
Future<void> _startRecording() async { Future<void> _startRecording() async {
await _waveController.startRecording(); _recordedFile = null;
final path = 'mapflow-${DateTime.now().microsecondsSinceEpoch}.wav';
await _audioRecorder.start(_recordConfig, path: path);
await _amplitudeSub?.cancel(); await _amplitudeSub?.cancel();
_recordStopwatch
..reset()
..start();
_lastInformationAt = DateTime.now(); _lastInformationAt = DateTime.now();
_amplitudeSub = _waveController.amplitudeStream.listen(_handleAmplitude); _amplitudeSub = _audioRecorder
.onAmplitudeChanged(const Duration(milliseconds: 45))
.listen(_handleAmplitude);
setState(() { setState(() {
_micAllowed = true; _micAllowed = true;
@@ -786,7 +794,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
Future<void> _stopRecording() async { Future<void> _stopRecording() async {
await _amplitudeSub?.cancel(); await _amplitudeSub?.cancel();
_amplitudeSub = null; _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; _lastInformationAt = null;
if (!mounted) { if (!mounted) {
return; return;
@@ -797,7 +809,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
}); });
} }
void _handleAmplitude(Amplitude amplitude) { void _handleAmplitude(record.Amplitude amplitude) {
final currentDb = amplitude.current; final currentDb = amplitude.current;
final now = DateTime.now(); final now = DateTime.now();
final level = _normalizeDbLevel(currentDb); final level = _normalizeDbLevel(currentDb);
@@ -812,7 +824,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
}); });
ref ref
.read(placeControllerProvider.notifier) .read(placeControllerProvider.notifier)
.setReviewDuration(_waveController.timeElapsed); .setReviewDuration(_recordStopwatch.elapsed);
} }
double _normalizeDbLevel(double currentDb) { double _normalizeDbLevel(double currentDb) {
@@ -880,7 +892,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
onSelect: (place) async { onSelect: (place) async {
setState(() => _submitting = true); setState(() => _submitting = true);
controller.setReviewPlace(place.name); controller.setReviewPlace(place.name);
final file = _waveController.file; final file = _recordedFile;
if (file == null) { if (file == null) {
throw StateError('Voice recording file is required.'); throw StateError('Voice recording file is required.');
} }

View File

@@ -106,7 +106,7 @@ packages:
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
cross_file: cross_file:
dependency: transitive dependency: "direct main"
description: description:
name: cross_file name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
@@ -925,22 +925,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" 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: web:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -41,9 +41,8 @@ dependencies:
web: ^1.1.1 web: ^1.1.1
geolocator: ^14.0.2 geolocator: ^14.0.2
record: ^6.2.0 record: ^6.2.0
waveform_recorder: ^1.8.0
waveform_flutter: ^1.2.0
flutter_svg: ^2.3.0 flutter_svg: ^2.3.0
cross_file: ^0.3.5+2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: