flutter_recorder 2.0.4 copy "flutter_recorder: ^2.0.4" to clipboard
flutter_recorder: ^2.0.4 copied to clipboard

A low-level audio recorder plugin which uses miniaudio as backend. Detect silence and save to WAV audio file. Audio wave, FFT and volume level can be get in real-time.

example/lib/main.dart

import 'dart:async';
import 'dart:developer' as dev;
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_recorder/flutter_recorder.dart';
import 'package:flutter_recorder_example/ui/bars.dart';
import 'package:logging/logging.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

/// Demostrate how to use flutter_recorder.
///
/// The silence detection and the visualizer works when using [PCMFormat.f32].
/// Writing audio stream to file is not implemented on Web.
void main() async {
  // The `flutter_recorder` package logs everything
  // (from severe warnings to fine debug messages)
  // using the standard `package:logging`.
  // You can listen to the logs as shown below.
  Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
  Logger.root.onRecord.listen((record) {
    dev.log(
      record.message,
      time: record.time,
      level: record.level.value,
      name: record.loggerName,
      zone: record.zone,
      error: record.error,
      stackTrace: record.stackTrace,
    );
  });

  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Flutter Recorder')),
        body: MyApp(),
      ),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Directory? savingDir;
  final format = PCMFormat.f32le;
  final sampleRate = 22050;
  final channels = RecorderChannels.stereo;
  final recorder = Recorder.instance;
  String? filePath;
  var thresholdDb = -20.0;
  var silenceDuration = 2.0;
  var secondsOfAudioToWriteBefore = 0.0;
  var androidInputPresetValue = 0;
  var iosInputPresetValue = 0;
  var webInputPresetValue = 0;
  var micStatus = 'stopped';
  List<CaptureDevice> devices = [];
  int selectedDeviceId = -1;

  File? file;

  AndroidInputPreset? get selectedAndroidInputPreset =>
      switch (androidInputPresetValue) {
        1 => AndroidInputPreset.generic,
        2 => AndroidInputPreset.camcorder,
        3 => AndroidInputPreset.voiceRecognition,
        4 => AndroidInputPreset.voiceCommunication,
        5 => AndroidInputPreset.unprocessed,
        _ => null,
      };

  String androidInputPresetLabel(int value) => switch (value) {
    1 => 'Generic',
    2 => 'Camcorder',
    3 => 'Voice recognition',
    4 => 'Voice communication',
    5 => 'Unprocessed',
    _ => 'System default',
  };

  IosInputPreset? get selectedIosInputPreset => switch (iosInputPresetValue) {
    1 => IosInputPreset.generic,
    2 => IosInputPreset.voiceCommunication,
    3 => IosInputPreset.videoChat,
    4 => IosInputPreset.speechRecognition,
    5 => IosInputPreset.unprocessed,
    _ => null,
  };

  String iosInputPresetLabel(int value) => switch (value) {
    1 => 'Generic',
    2 => 'Voice communication (AEC/AGC)',
    3 => 'Video chat (AEC)',
    4 => 'Speech recognition',
    5 => 'Unprocessed (Measurement)',
    _ => 'System default',
  };

  WebInputPreset get selectedWebInputPreset => switch (webInputPresetValue) {
    1 => WebInputPreset.voiceCommunication,
    2 => WebInputPreset.voiceRecognition,
    3 => WebInputPreset.noiseSuppression,
    4 => WebInputPreset.echoCancellation,
    _ => WebInputPreset.unprocessed,
  };

  String webInputPresetLabel(int value) => switch (value) {
    1 => 'Voice communication (AEC/AGC/NS)',
    2 => 'Voice recognition (AGC/NS)',
    3 => 'Noise suppression only',
    4 => 'Echo cancellation only',
    _ => 'Unprocessed (Raw audio)',
  };

  late final AppLifecycleListener _lifecycleListener;
  StreamSubscription<AudioDataContainer>? _audioStreamSubscription;
  StreamSubscription<RecorderDeviceNotification>? _deviceNotifSubscription;

  @override
  void initState() {
    super.initState();
    _lifecycleListener = AppLifecycleListener(
      onDetach: () {
        recorder.deinit();
      },
    );

    if (defaultTargetPlatform == TargetPlatform.android ||
        defaultTargetPlatform == TargetPlatform.iOS) {
      Permission.microphone.request().isGranted.then((value) async {
        if (!value) {
          await [Permission.microphone].request();
        }
      });
    }

    /// Listen to device notification and microphone lifecycle events.
    _deviceNotifSubscription = recorder.deviceNotificationEvents.listen((
      event,
    ) {
      setState(() {
        micStatus = event.name;
      });
      if (event == RecorderDeviceNotification.rerouted) {
        _refreshDevices();
      }
    });

    _refreshDevices();

    /// Listen to audio data stream. The data is received as Uint8List.
    _audioStreamSubscription = recorder.uint8ListStream.listen((data) {
      /// Write the PCM data to file. It can then be imported with the correct
      /// parameters with for example Audacity.
      /// Not testing on Web platform.
      if (!kIsWeb) {
        file?.writeAsBytesSync(
          // If you want a conversion, call one of the `to*List` methods.
          // data.toF32List(from: format).buffer.asUint8List(),
          data.rawData,
          mode: FileMode.writeOnlyAppend,
        );
      }
    });
  }

  @override
  void dispose() {
    _lifecycleListener.dispose();
    _deviceNotifSubscription?.cancel();
    _audioStreamSubscription?.cancel();
    recorder.deinit();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.topCenter,
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(10),
        child: Column(
          children: [
            /// List capture devices, init, start, deinit
            Wrap(
              runSpacing: 6,
              spacing: 6,
              children: [
                if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android)
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text('Android preset: '),
                      DropdownButton<int>(
                        value: androidInputPresetValue,
                        items: List<DropdownMenuItem<int>>.generate(
                          AndroidInputPreset.values.length + 1,
                          (index) => DropdownMenuItem<int>(
                            value: index,
                            child: Text(androidInputPresetLabel(index)),
                          ),
                        ),
                        onChanged: (value) {
                          if (value == null) return;
                          setState(() => androidInputPresetValue = value);
                        },
                      ),
                    ],
                  ),
                if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS)
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text('iOS preset: '),
                      DropdownButton<int>(
                        value: iosInputPresetValue,
                        items: List<DropdownMenuItem<int>>.generate(
                          IosInputPreset.values.length + 1,
                          (index) => DropdownMenuItem<int>(
                            value: index,
                            child: Text(iosInputPresetLabel(index)),
                          ),
                        ),
                        onChanged: (value) {
                          if (value == null) return;
                          setState(() => iosInputPresetValue = value);
                        },
                      ),
                    ],
                  ),
                if (kIsWeb)
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text('Web preset: '),
                      DropdownButton<int>(
                        value: webInputPresetValue,
                        items: List<DropdownMenuItem<int>>.generate(
                          WebInputPreset.values.length,
                          (index) => DropdownMenuItem<int>(
                            value: index,
                            child: Text(webInputPresetLabel(index)),
                          ),
                        ),
                        onChanged: (value) {
                          if (value == null) return;
                          setState(() => webInputPresetValue = value);
                        },
                      ),
                    ],
                  ),
                OutlinedButton(
                  onPressed: () {
                    showDeviceListDialog();
                  },
                  child: const Text('listCaptureDevices'),
                ),
                OutlinedButton(
                  onPressed: () async {
                    try {
                      await recorder.init(
                        deviceID: selectedDeviceId,
                        format: format,
                        sampleRate: sampleRate,
                        channels: channels,
                        androidInputPreset: selectedAndroidInputPreset,
                        iosInputPreset: selectedIosInputPreset,
                        webInputPreset: selectedWebInputPreset,
                      );
                    } on Exception catch (e) {
                      debugPrint('-------------- init() error: $e\n');
                    }
                  },
                  child: const Text('init'),
                ),
                OutlinedButton(
                  onPressed: () {
                    try {
                      recorder.start();
                    } on Exception catch (e) {
                      debugPrint('-------------- start() error: $e\n');
                    }
                  },
                  child: const Text('start'),
                ),
                OutlinedButton(
                  onPressed: () {
                    recorder.deinit();
                    setState(() {
                      micStatus = 'stopped';
                    });
                  },
                  child: const Text('deinit'),
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 8,
                    vertical: 8,
                  ),
                  child: Text(
                    'Mic status: $micStatus',
                    style: const TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 10),

            /// Recording
            Wrap(
              runSpacing: 6,
              spacing: 6,
              children: [
                ElevatedButton(
                  onPressed: () async {
                    try {
                      /// Asking for file path to store the audio file.
                      /// On web platform, it will be asked internally
                      /// from the browser.
                      if (!kIsWeb) {
                        final Directory saveDir;
                        if (defaultTargetPlatform == TargetPlatform.iOS ||
                            defaultTargetPlatform == TargetPlatform.android) {
                          // On mobile, use app documents directory
                          saveDir = await getApplicationDocumentsDirectory();
                        } else {
                          // On desktop, use downloads directory
                          final downloadsDir = await getDownloadsDirectory();
                          if (downloadsDir == null) {
                            debugPrint(
                              '-------------- startRecording() '
                              'Could not get downloads directory\n',
                            );
                            return;
                          }
                          saveDir = downloadsDir;
                        }
                        // Ensure the directory exists
                        if (!saveDir.existsSync()) {
                          saveDir.createSync(recursive: true);
                        }
                        filePath = '${saveDir.path}/flutter_recorder.ogg';
                        recorder.startRecording(
                          completeFilePath: filePath!,
                          format: RecordingFormat.opusOgg,
                        );
                      } else {
                        recorder.startRecording();
                      }
                    } on Exception catch (e) {
                      debugPrint('-------------- startRecording() $e\n');
                    }
                  },
                  child: const Text('Start recording'),
                ),
                ElevatedButton(
                  onPressed: () {
                    recorder.setPauseRecording(pause: true);
                  },
                  child: const Text('Pause recording'),
                ),
                ElevatedButton(
                  onPressed: () {
                    recorder.setPauseRecording(pause: false);
                  },
                  child: const Text('UN-Pause recording'),
                ),
                ElevatedButton(
                  onPressed: () {
                    recorder.stopRecording();
                    if (!kIsWeb) {
                      debugPrint('Audio recorded to "$filePath"');
                      showFileRecordedDialog(filePath!);
                    }
                  },
                  child: const Text('Stop recording'),
                ),
              ],
            ),
            const SizedBox(height: 10),

            /// Streaming
            Wrap(
              runSpacing: 6,
              spacing: 6,
              children: [
                CircularProgressIndicator(),
                OutlinedButton(
                  onPressed: () async {
                    recorder.startStreamingData(format: StreamingFormat.opus);

                    if (!kIsWeb) {
                      final Directory baseDir;
                      if (defaultTargetPlatform == TargetPlatform.iOS ||
                          defaultTargetPlatform == TargetPlatform.android) {
                        // On mobile, use app documents directory
                        baseDir = await getApplicationDocumentsDirectory();
                      } else {
                        // On desktop, use downloads directory
                        final downloadsDir = await getDownloadsDirectory();
                        if (downloadsDir == null) {
                          debugPrint('Cannot get download directory!');
                          return;
                        }
                        baseDir = downloadsDir;
                      }
                      savingDir = Directory('${baseDir.path}/flutter_recorder');
                      savingDir!.createSync();

                      file = File(
                        '${savingDir?.path}/fr_${sampleRate}_${format.name}_'
                        '${channels.count}.pcm',
                      );
                      try {
                        if (file?.existsSync() ?? false) {
                          file?.deleteSync();
                        }
                      } catch (e) {
                        debugPrint('Error deleting file: $e');
                      }
                    }
                  },
                  child: const Text('start stream'),
                ),
                OutlinedButton(
                  onPressed: () {
                    recorder.stopStreamingData();
                  },
                  child: const Text('stop stream'),
                ),
              ],
            ),
            const SizedBox(height: 10),

            /// The silence detection is available only with f32 format and
            /// the visualization is adapted only with that format.
            if (format == PCMFormat.f32le)
              Column(
                children: [
                  Column(
                    children: [
                      StreamBuilder(
                        stream: recorder.silenceChangedEvents,
                        builder: (context, snapshot) {
                          return ColoredBox(
                            color: snapshot.hasData && snapshot.data!.isSilent
                                ? Colors.green
                                : Colors.red,
                            child: SizedBox(
                              width: 70,
                              height: 50,
                              child: Center(
                                child: Text(
                                  recorder.getVolumeDb().toStringAsFixed(1),
                                ),
                              ),
                            ),
                          );
                        },
                      ),
                      const SizedBox(height: 10),
                      Wrap(
                        runSpacing: 6,
                        spacing: 6,
                        children: [
                          OutlinedButton(
                            onPressed: () {
                              recorder.setSilenceDetection(
                                enable: true,
                                onSilenceChanged: (isSilent, decibel) {
                                  /// Here you can check if silence is changed.
                                  /// Or you can do the same thing with the Stream
                                  /// [Recorder.instance.silenceChangedEvents]
                                  // debugPrint('SILENCE CHANGED: $isSilent, $decibel');
                                },
                              );
                              recorder.setSilenceThresholdDb(-27);
                              recorder.setSilenceDuration(0.5);
                              recorder.setSecondsOfAudioToWriteBefore(0.0);
                              setState(() {
                                thresholdDb = -27;
                                silenceDuration = 0.5;
                                secondsOfAudioToWriteBefore = 0;
                              });
                            },
                            child: const Text(
                              'setSilenceDetection ON -27, 0.5, 0.0',
                            ),
                          ),
                          OutlinedButton(
                            onPressed: () {
                              recorder.setSilenceDetection(enable: false);
                            },
                            child: const Text('setSilenceDetection OFF'),
                          ),
                        ],
                      ),

                      // Threshold dB slider
                      Row(
                        mainAxisSize: MainAxisSize.max,
                        children: [
                          Text(
                            'Threshold: ${thresholdDb.toStringAsFixed(1)}dB',
                          ),
                          Expanded(
                            child: Slider(
                              value: thresholdDb,
                              min: -100,
                              max: 0,
                              label: thresholdDb.toStringAsFixed(1),
                              onChanged: (value) {
                                recorder.setSilenceThresholdDb(value);
                                setState(() {
                                  thresholdDb = value;
                                });
                              },
                            ),
                          ),
                        ],
                      ),

                      // Silence duration slider
                      Row(
                        mainAxisSize: MainAxisSize.max,
                        children: [
                          Text(
                            'Silence duration: '
                            '${silenceDuration.toStringAsFixed(1)}',
                          ),
                          Expanded(
                            child: Slider(
                              value: silenceDuration,
                              min: 0,
                              max: 10,
                              label: silenceDuration.toStringAsFixed(1),
                              onChanged: (value) {
                                recorder.setSilenceDuration(value);
                                setState(() {
                                  silenceDuration = value;
                                });
                              },
                            ),
                          ),
                        ],
                      ),

                      // Silence duration slider
                      Row(
                        mainAxisSize: MainAxisSize.max,
                        children: [
                          Text(
                            'Write before: '
                            '${secondsOfAudioToWriteBefore.toStringAsFixed(1)}',
                          ),
                          Expanded(
                            child: Slider(
                              value: secondsOfAudioToWriteBefore,
                              min: 0,
                              max: 5,
                              label: silenceDuration.toStringAsFixed(1),
                              onChanged: (value) {
                                recorder.setSecondsOfAudioToWriteBefore(value);
                                setState(() {
                                  secondsOfAudioToWriteBefore = value;
                                });
                              },
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                  const Bars(),
                ],
              ),
          ],
        ),
      ),
    );
  }

  Future<void> showFileRecordedDialog(String filePath) async {
    final fileExists = await File(filePath).exists();
    if (!mounted) return;
    return showDialog<void>(
      context: context,
      barrierDismissible: true,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Recording saved!'),
          content: Text('Audio saved to:\n$filePath\nFile exists: $fileExists'),
          actions: <Widget>[
            TextButton(
              child: const Text('open'),
              onPressed: () async {
                OpenFilex.open(filePath, type: 'audio/wav');
              },
            ),
            TextButton(
              child: const Text('close'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _refreshDevices() {
    try {
      final list = recorder.listCaptureDevices();
      setState(() {
        devices = list;
        if (selectedDeviceId != -1 &&
            !devices.any((d) => d.id == selectedDeviceId)) {
          selectedDeviceId = -1;
        }
      });
    } on Exception catch (e) {
      debugPrint('Failed to list capture devices: $e');
    }
  }

  Future<void> showDeviceListDialog() async {
    _refreshDevices();
    final list = recorder.listCaptureDevices();

    return showDialog<void>(
      context: context,
      barrierDismissible: true,
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            final activeDeviceId = list.any((d) => d.id == selectedDeviceId)
                ? selectedDeviceId
                : -1;

            return AlertDialog(
              title: const Text('Input capture devices'),
              content: SizedBox(
                width: 360,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Select active device:',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    DropdownButton<int>(
                      isExpanded: true,
                      value: activeDeviceId,
                      items: [
                        const DropdownMenuItem<int>(
                          value: -1,
                          child: Text('Default device'),
                        ),
                        ...list.map(
                          (device) => DropdownMenuItem<int>(
                            value: device.id,
                            child: Text(
                              '${device.id}: ${device.name}'
                              '${device.isDefault ? ' (DEFAULT)' : ''}',
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ),
                      ],
                      onChanged: (value) async {
                        if (value == null) return;
                        setState(() => selectedDeviceId = value);
                        setDialogState(() {});
                        if (recorder.isInitialized) {
                          final wasStarted = recorder.isDeviceStarted();
                          recorder.deinit();
                          try {
                            await recorder.init(
                              deviceID: value,
                              format: format,
                              sampleRate: sampleRate,
                              channels: channels,
                              androidInputPreset: selectedAndroidInputPreset,
                              iosInputPreset: selectedIosInputPreset,
                              webInputPreset: selectedWebInputPreset,
                            );
                            if (wasStarted) {
                              recorder.start();
                            }
                          } on Exception catch (e) {
                            debugPrint(
                              '-------------- init() on device change error: $e\n',
                            );
                          }
                        }
                      },
                    ),
                  ],
                ),
              ),
              actions: <Widget>[
                TextButton(
                  child: const Text('close'),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                ),
              ],
            );
          },
        );
      },
    );
  }
}
92
likes
160
points
2.76k
downloads
screenshot

Documentation

API reference

Publisher

verified publishermarcobavagnoli.com

Weekly Downloads

A low-level audio recorder plugin which uses miniaudio as backend. Detect silence and save to WAV audio file. Audio wave, FFT and volume level can be get in real-time.

Repository (GitHub)
View/report issues

Topics

#audio #recorder #visualizer

Funding

Consider supporting this project:

github.com

License

Apache-2.0 (license)

Dependencies

code_assets, ffi, flutter, hooks, logging, meta, native_toolchain_c, plugin_platform_interface, web

More

Packages that depend on flutter_recorder

Packages that implement flutter_recorder