【Flutter】audioplayersを使用して効果音を高速連続再生するとクラッシュする

2022/06/28に公開

はじめに

株式会社エヌ・エイ・シーの細野です。
audioplayers を使用して、複数の効果音を高速かつ連続で再生するという実装をしていました。

すると、効果音の再生後しばらくすると再生の遅延、アプリのクラッシュという事象が発生しました。

その際の解決法について記載していきます。

動作環境

  • Flutter 2.10.0
  • audioplayers: ^0.20.1

サンプルコード

クラッシュするコード

まず、アセットに含まれる音声ファイルの一覧を定義しています。

final soundList = [
  "C.wav",
  "D.wav",
  "E.wav",
  "F.wav",
  "G.wav",
  "A.wav",
  "B.wav",
];

ボタン押下時に Timer を作成し、音声ファイルを 100 milliseconds の間隔でランダム再生します。

            ElevatedButton(
                onPressed: () {
                  var audioCache = AudioCache();
                  crashTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
                    final idx = Random().nextInt(soundList.length);
                    audioCache.play(soundList[idx]);
                  });
                },
                child: const Text("play crash sound")
            ),

最後に、音声を停止するボタンを作成します。

            ElevatedButton(
                onPressed: () {
                  crashTimer?.cancel();
                },
                child: const Text("stop crash sound")
            ),

この状態で再生ボタンを押下します。

一定時間再生されますが、しばらくするとクラッシュが発生しました。

Mac でのシミュレータ実行ではクラッシュしない場合がありますが、IPhone 実機での実行時、かなりの頻度でクラッシュします。

クラッシュしないコード

Timer を作成するボタン押下時の処理を、以下のように書き換えます。

soundList の数だけ AudioCache を生成し、Map に変換して保持します。

また AudioCache の引数 fixedPlayer には、 AudioPlayer のインスタンスを渡します。

            ElevatedButton(
                onPressed: () {
                  var audioMap = Map.fromIterables(
                      soundList,
                      soundList.map((e) => AudioCache(fixedPlayer: AudioPlayer())).toList()
                  );
                  noCrashTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
                    final idx = Random().nextInt(soundList.length);
                    audioMap[soundList[idx]]?.play(soundList[idx]);
                  });
                },
                child: const Text("play no crash sound")
            ),

音声の停止は、変更箇所はありません。

            ElevatedButton(
                onPressed: () {
                  crashTimer?.cancel();
                },
                child: const Text("stop crash sound")
            ),

なぜクラッシュしなくなるのか

AudioCache.play の実装を見てみます。

player という変数に格納された AudioPlayer に対して、 play メソッドを呼び出しています。

get player とコメントされた箇所で取得しているようなので、その中身を確認します。

  Future<AudioPlayer> play(
    String fileName, {
    double volume = 1.0,
    bool? isNotification,
    PlayerMode mode = PlayerMode.MEDIA_PLAYER,
    bool stayAwake = false,
    bool recordingActive = false,
    bool? duckAudio,
  }) async {
    final uri = await load(fileName);
    final player = _player(mode); # get player
    if (fixedPlayer != null) {
      await player.setReleaseMode(ReleaseMode.STOP);
    }
    await player.play(
      uri.toString(),
      volume: volume,
      respectSilence: isNotification ?? respectSilence,
      stayAwake: stayAwake,
      recordingActive: recordingActive,
      duckAudio: duckAudio ?? this.duckAudio,
    );
    return player;
  }

_player の実装です。

引数から fixedPlayer を受け取っていない場合、 play される度に AudioPlayer インスタンスが新規生成されて使用されます。

今回のように高速で何度も play メソッドを呼び出す場合に、この処理はメモリ不足を引き起こし、クラッシュや再生の遅延などを発生させていました。

  AudioPlayer _player(PlayerMode mode) {
    return fixedPlayer ?? AudioPlayer(mode: mode);
  }

参考文献

Discussion