はじめに
前回、「flutter_soundを用いた音声録音再生、アップロード機能の実装」の記事を記載したのだが、ESP32のミーア本体に音声ファイルがダウンロードされるものの、なぜか再生できない現象が生じた。
エラーも出ないので、なかなか原因特定が難しかったのだが、flutter_soundでm4a形式で録音していたものを拡張子だけmp3に変更しており、中身はm4aのままだったのでESP32で再生するときに再生できなくなっていたことが原因だった。
そのままm4aを再生できるようにESP32側のコードを修正しても良いのだが、機械音声合成でmp3をすでに出力しているので、今回はmp3に統一したいと思う。
flutter_soundではmp3をサポートしていない!?
flutter_sound_platform_interfaceパッケージでは、一応mp3形式でのエンコードもサポートしてそうな雰囲気が出ているが(shame on you!と記載されているがw)、実際にmp3を使ったところエラーになりエンコードできなかった。
同じ問題に遭遇している人を発見
https://github.com/Canardoux/flutter_sound/issues/175
回答者のこちらのテーブルに記載されているようにflutter_soundではMP3のエンコードはiOSもAndroidもサポートされていない。
record + audioplayersパッケージの場合は?
代替案として、flutter_sound以外のパッケージを使うことを考え、リサーチしたところ音声録音としてrecordパッケージ、音声再生としてaudioplayersパッケージを使うことを検討。
しかし、recordパッケージも録音ファイルの形式はm4aやwavなどが中心で、mp3形式には対応していないことが判明したので、結局今のflutter_soundパッケージの場合と同じ課題が残る。
そこで、ffmpeg_kit_flutter_audio
パッケージを使用して、m4aをmp3に変換する方針に変更。このパッケージは、外部ライブラリを使用して幅広い形式に対応しており、mp3形式への変換に必要な lame
ライブラリが含まれている。
ffmpeg_kit_flutter_audioを使った変換
ffmpeg_kit_flutterパッケージはこちら。
https://pub.dev/packages/ffmpeg_kit_flutter
ffmpeg_kit_flutter パッケージ自体はmp3変換ライブラリを含んでいないが、さらに8つの外部ライブラリを提供しており、audioライブラリはmp3変換ライブラリを含んでいる。
https://github.com/arthenica/ffmpeg-kit?tab=readme-ov-file
というわけで、ffmpeg_kit_flutter_audioパッケージをインストール。
dependencies:
ffmpeg_kit_flutter_audio: 6.0.3-LTS
ffmpeg_kit_flutter_audioパッケージを使って、m4aをmp3に変換するコードを下記のように記載。
Future<void> _convertToMp3() async {
if (_recordedFilePath == null) return;
final directory = await getTemporaryDirectory();
final outputPath =
'${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.mp3';
final ffmpegCommand =
'-i $_recordedFilePath -codec:a libmp3lame -qscale:a 2 $outputPath';
await FFmpegKit.execute(ffmpegCommand).then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
setState(() {
_convertedFilePath = outputPath;
});
print('MP3への変換が成功しました: $outputPath');
} else {
setState(() {
_errorText = "MP3への変換に失敗しました";
});
print('MP3への変換に失敗しました。エラーメッセージを確認してください。');
}
});
}
そして、変換したMP3の音声ファイルパスを使用してAPIに送信する。
Future<void> _addOrUpdatePhrase() async {
if ((_selectedTime != null && _selectedDays.isEmpty) ||
(_selectedTime == null && _selectedDays.isNotEmpty)) {
setState(() {
_errorText = '再生時間と再生曜日は両方設定するか、両方未設定にしてください。';
});
return;
}
await _convertToMp3(); // MP3に変換
List<String> englishDays = translateDaysToEnglish(_selectedDays);
final userPhraseNotifier = ref.read(userPhraseProvider.notifier);
try {
if (_convertedFilePath != null) {
// 音声ファイルがある場合は必ず非公開に設定
_isPublic = false;
if (widget.phraseWithSchedule != null) {
await userPhraseNotifier.updateVoiceWithSchedule(
widget.phraseWithSchedule!.phrase.id,
_newPhrase,
_convertedFilePath!,
!_isPublic,
_selectedTime,
englishDays,
);
} else {
final newPhrase = await userPhraseNotifier.uploadVoiceWithSchedule(
_newPhrase,
_convertedFilePath!,
!_isPublic,
_selectedTime,
englishDays,
);
// IDを取得して音声ファイルパスを保存
int? newPhraseId = newPhrase.phrase.id;
debugPrint("newphrase id: $newPhraseId");
if (newPhraseId != null) {
await AudioStorageService.saveRecordedFilePath(
newPhraseId,
_convertedFilePath!,
);
}
}
} else {
// 音声ファイルがない場合の処理
if (widget.phraseWithSchedule == null) {
await userPhraseNotifier.addUserPhraseWithSchedule(
_newPhrase,
false,
!_isPublic,
_selectedTime,
englishDays,
);
} else {
await userPhraseNotifier.updateUserPhraseWithSchedule(
widget.phraseWithSchedule!.phrase.id,
_newPhrase,
false,
!_isPublic,
_selectedTime,
englishDays,
);
}
}
Navigator.of(context).pop();
} catch (e) {
setState(() {
_errorText = 'フレーズの保存に失敗しました: $e';
});
}
}
動作確認
XCodeのログを見ると下記のように
ffmpeg version n6.0 Copyright (c) 2000-2023 the FFmpeg developers
...
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '<file_path>/recording.m4a':
...
Output #0, mp3, to '<file_path>/recording.mp3':
...
flutter: MP3への変換が成功しました: <file_path>/recording.mp3
flutter: 元のM4Aファイル: <file_path>/recording.m4a
無事MP3への変換ができ、ESP32でも再生された。