recording_test
This commit is contained in:
177
lib/audio_player.dart
Normal file
177
lib/audio_player.dart
Normal file
@ -0,0 +1,177 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart' as ap;
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AudioPlayer extends StatefulWidget {
|
||||
/// Path from where to play recorded audio
|
||||
final String source;
|
||||
|
||||
/// Callback when audio file should be removed
|
||||
/// Setting this to null hides the delete button
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const AudioPlayer({
|
||||
super.key,
|
||||
required this.source,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
AudioPlayerState createState() => AudioPlayerState();
|
||||
}
|
||||
|
||||
class AudioPlayerState extends State<AudioPlayer> {
|
||||
static const double _controlSize = 56;
|
||||
static const double _deleteBtnSize = 24;
|
||||
|
||||
final _audioPlayer = ap.AudioPlayer()..setReleaseMode(ReleaseMode.stop);
|
||||
late StreamSubscription<void> _playerStateChangedSubscription;
|
||||
late StreamSubscription<Duration?> _durationChangedSubscription;
|
||||
late StreamSubscription<Duration> _positionChangedSubscription;
|
||||
Duration? _position;
|
||||
Duration? _duration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_playerStateChangedSubscription =
|
||||
_audioPlayer.onPlayerComplete.listen((state) async {
|
||||
await stop();
|
||||
});
|
||||
_positionChangedSubscription = _audioPlayer.onPositionChanged.listen(
|
||||
(position) => setState(() {
|
||||
_position = position;
|
||||
}),
|
||||
);
|
||||
_durationChangedSubscription = _audioPlayer.onDurationChanged.listen(
|
||||
(duration) => setState(() {
|
||||
_duration = duration;
|
||||
}),
|
||||
);
|
||||
|
||||
_audioPlayer.setSource(_source);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playerStateChangedSubscription.cancel();
|
||||
_positionChangedSubscription.cancel();
|
||||
_durationChangedSubscription.cancel();
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_buildControl(),
|
||||
_buildSlider(constraints.maxWidth),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Color(0xFF73748D), size: _deleteBtnSize),
|
||||
onPressed: () {
|
||||
if (_audioPlayer.state == ap.PlayerState.playing) {
|
||||
stop().then((value) => widget.onDelete());
|
||||
} else {
|
||||
widget.onDelete();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text('${_duration ?? 0.0}'),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControl() {
|
||||
Icon icon;
|
||||
Color color;
|
||||
|
||||
if (_audioPlayer.state == ap.PlayerState.playing) {
|
||||
icon = const Icon(Icons.pause, color: Colors.red, size: 30);
|
||||
color = Colors.red.withOpacity(0.1);
|
||||
} else {
|
||||
final theme = Theme.of(context);
|
||||
icon = Icon(Icons.play_arrow, color: theme.primaryColor, size: 30);
|
||||
color = theme.primaryColor.withOpacity(0.1);
|
||||
}
|
||||
|
||||
return ClipOval(
|
||||
child: Material(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
child:
|
||||
SizedBox(width: _controlSize, height: _controlSize, child: icon),
|
||||
onTap: () {
|
||||
if (_audioPlayer.state == ap.PlayerState.playing) {
|
||||
pause();
|
||||
} else {
|
||||
play();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlider(double widgetWidth) {
|
||||
bool canSetValue = false;
|
||||
final duration = _duration;
|
||||
final position = _position;
|
||||
|
||||
if (duration != null && position != null) {
|
||||
canSetValue = position.inMilliseconds > 0;
|
||||
canSetValue &= position.inMilliseconds < duration.inMilliseconds;
|
||||
}
|
||||
|
||||
double width = widgetWidth - _controlSize - _deleteBtnSize;
|
||||
width -= _deleteBtnSize;
|
||||
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Slider(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
inactiveColor: Theme.of(context).colorScheme.secondary,
|
||||
onChanged: (v) {
|
||||
if (duration != null) {
|
||||
final position = v * duration.inMilliseconds;
|
||||
_audioPlayer.seek(Duration(milliseconds: position.round()));
|
||||
}
|
||||
},
|
||||
value: canSetValue && duration != null && position != null
|
||||
? position.inMilliseconds / duration.inMilliseconds
|
||||
: 0.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> play() => _audioPlayer.play(_source);
|
||||
|
||||
Future<void> pause() async {
|
||||
await _audioPlayer.pause();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _audioPlayer.stop();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Source get _source =>
|
||||
kIsWeb ? ap.UrlSource(widget.source) : ap.DeviceFileSource(widget.source);
|
||||
}
|
252
lib/audio_recorder.dart
Normal file
252
lib/audio_recorder.dart
Normal file
@ -0,0 +1,252 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
import 'platform/audio_recorder_platform.dart';
|
||||
|
||||
class Recorder extends StatefulWidget {
|
||||
final void Function(String path) onStop;
|
||||
|
||||
const Recorder({super.key, required this.onStop});
|
||||
|
||||
@override
|
||||
State<Recorder> createState() => _RecorderState();
|
||||
}
|
||||
|
||||
class _RecorderState extends State<Recorder> with AudioRecorderMixin {
|
||||
int _recordDuration = 0;
|
||||
Timer? _timer;
|
||||
late final AudioRecorder _audioRecorder;
|
||||
StreamSubscription<RecordState>? _recordSub;
|
||||
RecordState _recordState = RecordState.stop;
|
||||
StreamSubscription<Amplitude>? _amplitudeSub;
|
||||
Amplitude? _amplitude;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_audioRecorder = AudioRecorder();
|
||||
|
||||
_recordSub = _audioRecorder.onStateChanged().listen((recordState) {
|
||||
_updateRecordState(recordState);
|
||||
});
|
||||
|
||||
_amplitudeSub = _audioRecorder
|
||||
.onAmplitudeChanged(const Duration(milliseconds: 300))
|
||||
.listen((amp) {
|
||||
setState(() => _amplitude = amp);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _start() async {
|
||||
try {
|
||||
if (await _audioRecorder.hasPermission()) {
|
||||
const encoder = AudioEncoder.aacLc;
|
||||
|
||||
if (!await _isEncoderSupported(encoder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final devs = await _audioRecorder.listInputDevices();
|
||||
debugPrint(devs.toString());
|
||||
|
||||
const config = RecordConfig(encoder: encoder, numChannels: 1);
|
||||
|
||||
// Record to file
|
||||
await recordFile(_audioRecorder, config);
|
||||
|
||||
// Record to stream
|
||||
// await recordStream(_audioRecorder, config);
|
||||
|
||||
_recordDuration = 0;
|
||||
|
||||
_startTimer();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stop() async {
|
||||
final path = await _audioRecorder.stop();
|
||||
|
||||
if (path != null) {
|
||||
widget.onStop(path);
|
||||
|
||||
downloadWebData(path);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pause() => _audioRecorder.pause();
|
||||
|
||||
Future<void> _resume() => _audioRecorder.resume();
|
||||
|
||||
void _updateRecordState(RecordState recordState) {
|
||||
setState(() => _recordState = recordState);
|
||||
|
||||
switch (recordState) {
|
||||
case RecordState.pause:
|
||||
_timer?.cancel();
|
||||
break;
|
||||
case RecordState.record:
|
||||
_startTimer();
|
||||
break;
|
||||
case RecordState.stop:
|
||||
_timer?.cancel();
|
||||
_recordDuration = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isEncoderSupported(AudioEncoder encoder) async {
|
||||
final isSupported = await _audioRecorder.isEncoderSupported(
|
||||
encoder,
|
||||
);
|
||||
|
||||
if (!isSupported) {
|
||||
debugPrint('${encoder.name} is not supported on this platform.');
|
||||
debugPrint('Supported encoders are:');
|
||||
|
||||
for (final e in AudioEncoder.values) {
|
||||
if (await _audioRecorder.isEncoderSupported(e)) {
|
||||
debugPrint('- ${encoder.name}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_buildRecordStopControl(),
|
||||
const SizedBox(width: 20),
|
||||
_buildPauseResumeControl(),
|
||||
const SizedBox(width: 20),
|
||||
_buildText(),
|
||||
],
|
||||
),
|
||||
if (_amplitude != null) ...[
|
||||
const SizedBox(height: 40),
|
||||
Text('Current: ${_amplitude?.current ?? 0.0}'),
|
||||
Text('Max: ${_amplitude?.max ?? 0.0}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_recordSub?.cancel();
|
||||
_amplitudeSub?.cancel();
|
||||
_audioRecorder.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildRecordStopControl() {
|
||||
late Icon icon;
|
||||
late Color color;
|
||||
|
||||
if (_recordState != RecordState.stop) {
|
||||
icon = const Icon(Icons.stop, color: Colors.red, size: 30);
|
||||
color = Colors.red.withOpacity(0.1);
|
||||
} else {
|
||||
final theme = Theme.of(context);
|
||||
icon = Icon(Icons.mic, color: theme.primaryColor, size: 30);
|
||||
color = theme.primaryColor.withOpacity(0.1);
|
||||
}
|
||||
|
||||
return ClipOval(
|
||||
child: Material(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
child: SizedBox(width: 56, height: 56, child: icon),
|
||||
onTap: () {
|
||||
(_recordState != RecordState.stop) ? _stop() : _start();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPauseResumeControl() {
|
||||
if (_recordState == RecordState.stop) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
late Icon icon;
|
||||
late Color color;
|
||||
|
||||
if (_recordState == RecordState.record) {
|
||||
icon = const Icon(Icons.pause, color: Colors.red, size: 30);
|
||||
color = Colors.red.withOpacity(0.1);
|
||||
} else {
|
||||
final theme = Theme.of(context);
|
||||
icon = const Icon(Icons.play_arrow, color: Colors.red, size: 30);
|
||||
color = theme.primaryColor.withOpacity(0.1);
|
||||
}
|
||||
|
||||
return ClipOval(
|
||||
child: Material(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
child: SizedBox(width: 56, height: 56, child: icon),
|
||||
onTap: () {
|
||||
(_recordState == RecordState.pause) ? _resume() : _pause();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildText() {
|
||||
if (_recordState != RecordState.stop) {
|
||||
return _buildTimer();
|
||||
}
|
||||
|
||||
return const Text("Waiting to record");
|
||||
}
|
||||
|
||||
Widget _buildTimer() {
|
||||
final String minutes = _formatNumber(_recordDuration ~/ 60);
|
||||
final String seconds = _formatNumber(_recordDuration % 60);
|
||||
|
||||
return Text(
|
||||
'$minutes : $seconds',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatNumber(int number) {
|
||||
String numberStr = number.toString();
|
||||
if (number < 10) {
|
||||
numberStr = '0$numberStr';
|
||||
}
|
||||
|
||||
return numberStr;
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
setState(() => _recordDuration++);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'audio_player.dart';
|
||||
import 'audio_recorder.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@ -57,10 +60,14 @@ class MyHomePage extends StatefulWidget {
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
String _key = ' ';
|
||||
bool showPlayer = false;
|
||||
bool record = true;
|
||||
String? audioPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
showPlayer = false;
|
||||
ServicesBinding.instance.keyboard.addHandler(_onKey);
|
||||
}
|
||||
|
||||
@ -134,36 +141,40 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
child: record
|
||||
? showPlayer
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: AudioPlayer(
|
||||
source: audioPath!,
|
||||
onDelete: () {
|
||||
setState(() => showPlayer = false);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Recorder(
|
||||
onStop: (path) {
|
||||
if (kDebugMode) print('Recorded file path: $path');
|
||||
setState(() {
|
||||
audioPath = path;
|
||||
showPlayer = true;
|
||||
});
|
||||
},
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'$_key',
|
||||
// set text color to white
|
||||
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'$_key',
|
||||
// set text color to white
|
||||
|
||||
style: TextStyle(
|
||||
color: Colors.lightGreen,
|
||||
fontSize: 160.0, // Change this to your preferred color
|
||||
style: TextStyle(
|
||||
color: Colors.lightGreen,
|
||||
fontSize: 160.0, // Change this to your preferred color
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementKey,
|
||||
|
47
lib/platform/audio_recorder_io.dart
Normal file
47
lib/platform/audio_recorder_io.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
mixin AudioRecorderMixin {
|
||||
Future<void> recordFile(AudioRecorder recorder, RecordConfig config) async {
|
||||
final path = await _getPath();
|
||||
|
||||
await recorder.start(config, path: path);
|
||||
}
|
||||
|
||||
Future<void> recordStream(AudioRecorder recorder, RecordConfig config) async {
|
||||
final path = await _getPath();
|
||||
|
||||
final file = File(path);
|
||||
|
||||
final stream = await recorder.startStream(config);
|
||||
|
||||
stream.listen(
|
||||
(data) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
recorder.convertBytesToInt16(Uint8List.fromList(data)),
|
||||
);
|
||||
file.writeAsBytesSync(data, mode: FileMode.append);
|
||||
},
|
||||
// ignore: avoid_print
|
||||
onDone: () {
|
||||
// ignore: avoid_print
|
||||
print('End of stream. File written to $path.');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void downloadWebData(String path) {}
|
||||
|
||||
Future<String> _getPath() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return p.join(
|
||||
dir.path,
|
||||
'audio_${DateTime.now().millisecondsSinceEpoch}.m4a',
|
||||
);
|
||||
}
|
||||
}
|
1
lib/platform/audio_recorder_platform.dart
Normal file
1
lib/platform/audio_recorder_platform.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'audio_recorder_web.dart' if (dart.library.io) 'audio_recorder_io.dart';
|
33
lib/platform/audio_recorder_web.dart
Normal file
33
lib/platform/audio_recorder_web.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:record/record.dart';
|
||||
|
||||
mixin AudioRecorderMixin {
|
||||
Future<void> recordFile(AudioRecorder recorder, RecordConfig config) {
|
||||
return recorder.start(config, path: '');
|
||||
}
|
||||
|
||||
Future<void> recordStream(AudioRecorder recorder, RecordConfig config) async {
|
||||
final b = <Uint8List>[];
|
||||
final stream = await recorder.startStream(config);
|
||||
|
||||
stream.listen(
|
||||
(data) => b.add(data),
|
||||
onDone: () => downloadWebData(html.Url.createObjectUrl(html.Blob(b))),
|
||||
);
|
||||
}
|
||||
|
||||
void downloadWebData(String path) {
|
||||
// Simple download code for web testing
|
||||
final anchor = html.document.createElement('a') as html.AnchorElement
|
||||
..href = path
|
||||
..style.display = 'none'
|
||||
..download = 'audio.wav';
|
||||
html.document.body!.children.add(anchor);
|
||||
anchor.click();
|
||||
html.document.body!.children.remove(anchor);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user