Recording and playing audio in Flutter

Thanks to the hard work of package developers, it is easy to record audio in Flutter.

First try

I used one of the first results from a search on pub.dev. The package is called flutter_sound. After setting the permissions in the Android and iOS project as documented, I copy pasted their code snippet in my app. Doesn't compile. I mean, even the first line is obviously wrong:

Future<String> result = await flutterSound.startRecorder(null);

result.then(path) {
  print('startRecorder: $path');

  _recorderSubscription = flutterSound.onRecorderStateChanged.listen((e) {
    DateTime date =
      new DateTime.fromMillisecondsSinceEpoch(e.currentPosition.toInt());
    String txt = DateFormat('mm:ss:SS', 'en_US').format(date);
  });
}

If you await the string, result should be typed as a String, not a Future<String>. Anyhow, idiomatic Dart would have simply declared it as final result = ... and let the interpreter infer the type.

But then result.then(...) won't work if it is a String. My guess is that someone added "await" to the code snippet, and forgot to fix the rest of the snippet accordingly.

Finally printing the duration by creating a DateTime and using DateFormat is really hacky and hard to read. They'd be better off using a Duration, which is what you use for... durations!

final duration = Duration(milliseconds: e.currentPosition.toInt());
print(duration.toString());

The last remark about the code snippet is that currentDuration is typed as a double instead of a Duration. So it is not obvious at first whether it is storing seconds, milliseconds or something else. It turns out it is in milliseconds.

Anyhow the fixed snippet is:

final path = await flutterSound.startRecorder(null);
print('startRecorder: $path');

_recorderSubscription = flutterSound.onRecorderStateChanged.listen((e) {
  final duration = Duration(milliseconds: e.currentPosition.toInt());
  print(duration.toString());
});

Recording & Playback

The first time I tried the record button, it requested permissions, but as soon as I granted both, nothing happened. I had to click a second time to actually start recording. More on that later.

Once you start recording, flutter_sound records the audio in a file. On Android it's an mp4 file, while on iOS it's an m4a file, says the doc. If you don't specify a path, it chooses a default path for you, which is convenient. This way I don't have to have platform specific code to choose a filename.

Playback worked surprisingly smoothly as well once I fixed the syntax errors from their snippets. However, the recorded sound quality was.

Fixing sound quality

It turns out two issues have been filed about it: https://github.com/dooboolab/flutter_sound/issues/79 and https://github.com/dooboolab/flutter_sound/issues/95. It looks like you can tweak the recording settings and get acceptable quality that way. Some people suggested adjusting the bitrate, etc. I ended up using this snippet:

flutterSound.startRecorder(null, androidEncoder: AndroidEncoder.AMR_WB);

People said this won't work on iOS. I have to admit I haven't taken the time to try the feature both on iOS and Android. For now this will do.

Fixing permissions

One thing that still puzzled me was why, when you first grant recording and storage permissions to the plugin, it does not start recording. So I put a breakpoint right after the await flutterSound.startRecorder(...) call.

It turns out, it did not return a null path, nor did it throw an exception. It simply never returned!! Their code must have been stuck somewhere.

Anyhow, my fix was to manually request permissions, so that by the time I start recording, everything has been granted. This gives the additional benefit of showing my own error message if the user denies some permission.

I used permission_handler since it seems to be backed by a company and gets updated often. Here's how it ended up looking:

// Request permissions.
Map<PermissionGroup, PermissionStatus> permissions =
  await PermissionHandler().requestPermissions([
    PermissionGroup.microphone,
    PermissionGroup.storage,
  ]);
if (permissions[PermissionGroup.microphone] != PermissionStatus.granted) {
  showSnackBarMessage(context, 'some error message about the microphone');
  return;
}
if (permissions[PermissionGroup.storage] != PermissionStatus.granted) {
  showSnackBarMessage(context, 'some error message about storage');
  return;
}

// Now that they are granted, start recording.
final path = await flutterSound.startRecorder(
  null, androidEncoder: AndroidEncoder.AMR_WB);

The API is quite convenient: if permission is already granted, requestPermissions does nothing.

After fixing sound quality and permissions, everything works well! Next I could check out these two things:

  • Try a few more recording settings. The audio it records on Android with the current setting is fine, but it could be better.
  • Try recording on iOS.
  • Try recording on one platform, then playing it back on the other.