Use single microphone stream for recording
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m50s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m50s
This commit is contained in:
@@ -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.');
|
||||||
}
|
}
|
||||||
|
|||||||
18
pubspec.lock
18
pubspec.lock
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user