方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【Flutter】ffmpeg_kit_flutter_audioを使って、m4aをmp3に変換する方法

flutter-m4a-to-mp3
この記事は約9分で読めます。

はじめに

様々な方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

前回、「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://pub.dev/documentation/flutter_sound_platform_interface/latest/flutter_sound_platform_interface/Codec.html

同じ問題に遭遇している人を発見

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パッケージをインストール。

Dart
dependencies:
  ffmpeg_kit_flutter_audio: 6.0.3-LTS

ffmpeg_kit_flutter_audioパッケージを使って、m4aをmp3に変換するコードを下記のように記載。

Dart
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に送信する。

Dart
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のログを見ると下記のように

ShellScript
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でも再生された。

タイトルとURLをコピーしました