🎵

【Flutter】just_audioを使って音声ファイルを再生する

2022/03/26に公開

概要

Flutterで音声ファイルの再生を可能にするパッケージはいくつかありますが、この記事ではjust_audioを使って、基本となる再生とバックグラウンド再生などを解説していきます

その1: 音声ファイルを再生する ⬅︎ 今回
その2: 音声ファイルをバックグラウンド再生する

just_audio以外のパッケージを知りたい方はこちらを参考にしてみてください

環境

Dart: 2.14.4
Flutter: 2.10.3
just_audio: 0.9.19
rxdart: 0.27.3
audio_video_progress_bar: 0.9.0

全体像

今回作るサンプルの全体像は下記の通りです

UI:

  • 再生ファイルの進捗を表示するスライダー
  • 音声ファイルの操作を行うボタン(再生、停止)

State:

  • Sliderの状態値 ProgressBarState
  • 音声ファイルの状態値 AudioState
  • AudioPlayerオブジェクト
  • UI側へはProviderを使って依存関係を流し込んでいきます

just_audio.drawio (1).png

全体のコードはこちらを参照ください Github

AudioPlayerクラスについて

音声ファイルの状態や操作は、just_audioパッケージで提供されているAudioPlayerクラスを使って行っていきます

音声ファイルの操作に必要あメソッドや音声ファイルに関する様々な状態値のStreamを兼ね備えており、今回のコアとなるクラスです

このオブジェクトから今回は下記のメソッドとStreamを利用していきます

今回使うメソッド

  • setUrl():音声ファイルをURLからダウンロード
  • play():音声ファイルの再生
  • pause():音声ファイルの停止
  • seek():再生位置の変更
  • dispose():AudioPlayerオブジェクトの破棄

状態値のStream

  • PlayerStateStream:音声ファイルの再生状況を表すPlayerStateオブジェクトが流れるStream
  • PositionStream:現在の再生位置を表すStream。Durationオブジェクトが流れています。
  • BufferedPositionStream:ダウンロードが完了した位置を表すStream。Durationオブジェクトが流れています。
  • DurationStream:音声ファイル全体の分数を表す。全体の分数が分かった時点で一度だけDurationオブジェクトを流します。

状態管理クラスで準備する変数

状態管理クラスでは以下変数を管理します。

just_audio_screen_state.dart
class JustAudioScreenState extends ChangeNotifier {
  AudioPlayer _audioPlayer; // AudioPlayerオブジェクト
  ProgressBarState progressBarState = ProgressBarState( // スライダーの状態値
    current: Duration.zero,
    buffered: Duration.zero,
    total: Duration.zero,
  );
  AudioState audioState = AudioState.paused; // ボタン用の音声ファイルの状態値
  StreamSubscription _playerStateSubscription; // playerStateStreamへのサブスクリプション
  StreamSubscription _progressBarSubscription; // スライダー状態値用のStreamへのサブスクリプション

  static final _url = // 再生する音楽ファイルのダウンロードURL
      'https://firebasestorage.googleapis.com/v0/b/flutter-toybox.appspot.com/o/audios%2Fmusic_box.mp3?alt=media';
}

上述の通り、Providerを使ってUI側に注入していきます。

状態管理クラスで準備するメソッド

初期化メソッド

状態管理クラス生成時に走らせるメソッドです 
AudioPlayerクラスのインスタンス化し、setUrl()で音声ファイルのダウンロード先を設定しています。

just_audio_screen_state.dart
  /* --- INITIALIZE --- */
  void init() {
    _audioPlayer = AudioPlayer()..setUrl(_url); // インスタンス化と音声ファイルの設定
    _listenToPlaybackState(); // ボタン用のサブスクリプションを行うメソッド
    _listenForProgressBarState(); // スライダー用のサブスクリプションを行うメソッド
  }

音声ファイルの操作メソッド

ボタンとスライダー操作でこちらのメソッドを発火させます

just_audio_screen_state.dart
  /* --- PLAYER CONTROL  --- */
  void play() => _audioPlayer.play();

  void pause() => _audioPlayer.pause();

  void seek(Duration position) => _audioPlayer.seek(position);

状態値の操作メソッド

ボタン用の状態値AudioStateとスライダー用の状態値ProgressBarStateの更新をUI側に通知します

just_audio_screen_state.dart
  /* --- STATE CONTROL --- */
  void setAudioState(AudioState state) {
    audioState = state;
    notifyListeners();
  }

  void setProgressBarState(ProgressBarState state) {
    progressBarState = state;
    notifyListeners();
  }

disposeメソッド

状態管理クラスが破棄される際にAudioPlayerオブジェクトの破棄とStreamへのサブスクリプションを閉じてメモリを解放します

just_audio_screen_state.dart
  
  void dispose() {
    _audioPlayer.dispose();
    _playbackSubscription.cancel();
    _progressBarSubscription.cancel();
    super.dispose();
  }

ボタン、スライダー

https://www.youtube.com/watch?v=YvYcPmvIXew

ボタンUI

ボタンのUIは音声ファイルの状態に応じて、「再生ボタン」、「停止ボタン」、「ローディング中」の三つを切り替えます

just_audio_screen.dart
switch (state) {
  // ローディング中
  case AudioState.loading:
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: SizedBox(
        height: 32,
        width: 32,
        child: CircularProgressIndicator(),
      ),
    );
  case AudioState.ready:
  case AudioState.paused:
  // 再生ボタン
    return IconButton(
      onPressed: () =>
          context.read<JustAudioScreenState>().play(),
      icon: Icon(Icons.play_arrow),
      iconSize: 32.0,
    );
  case AudioState.playing:
  // 停止ボタン
    return IconButton(
      onPressed: () =>
          context.read<JustAudioScreenState>().pause(),
      icon: Icon(Icons.pause),
      iconSize: 32.0,
    );
  default:
    return SizedBox(
      height: 32,
      width: 32,
    );
}

ボタンState

Stateクラス

ボタンが監視する状態値は音声ファイルの状態です。以下4つの状態のいずれかになる状態値としてenumで定義したAudioStateを状態管理クラス側で管理します。

  • ready:再生準備OK
  • paused:停止中
  • playing:再生中
  • loading:読み込み中
just_audio_screen_state.dart
enum AudioState {
  ready,
  paused,
  playing,
  loading,
}

Streamにサブスクライブ

音声ファイルの状態値はAudioPlayerオブジェクトのPlayerStateStreamにサブスクライブする事で監視します

PlayerStateStreamから流れてくるPlayerStateオブジェクトはprocessingStateplayingというフィールドを持っており、この2つの値を見る事で音声ファイルの状態を判定していきます

  • PlayerState.processingState:音声ファイルのダウンロード状況。以下4つの状態を取ります
    • idle:ロード前
    • loading:ローディング中
    • buffering:バッファリング中で再生出来ない状態
    • ready:再生可能な量データのローディング完了
    • completed:ローディング完了
  • PlayerState.playing:音声ファイルを再生中かのbool値
just_audio_screen_state.dart
  void _listenToPlaybackState() {
    _playerStateSubscription =
        _audioPlayer.playerStateStream.listen((PlayerState state) {

      if (isLoadingState(state)) {
        setAudioState(AudioState.loading);
      } else if (isAudioReady(state)) {
        setAudioState(AudioState.ready);
      } else if (isAudioPlaying(state)) {
        setAudioState(AudioState.playing);
      } else if (isAudioPaused(state)) {
        setAudioState(AudioState.paused);
      } else if (hasCompleted(state)) {
        setAudioState(AudioState.paused);
      }
    });
  }

  /* --- UTILITY METHODS --- */
  bool isLoadingState(PlayerState state) {
    return state.processingState == ProcessingState.loading ||
        state.processingState == ProcessingState.buffering;
  }

  bool isAudioReady(PlayerState state) {
    return state.processingState == ProcessingState.ready && !state.playing;
  }

  bool isAudioPlaying(PlayerState state) {
    return state.playing && !hasCompleted(state);
  }

  bool isAudioPaused(PlayerState state) {
    return !state.playing && !isLoadingState(state);
  }

  bool hasCompleted(PlayerState state) {
    return state.processingState == ProcessingState.completed;
  }

スライダーUI

自前でSliderを用意する事もできますが、音声メディア再生に特化したaudio_video_progress_barというライブラリを使っていきます

ProgressBarクラスは音声メディア再生に特化した様々なフィールドを持っていますが、その中から今回は下記の4つを使っていきます

just_audio_screen.dart
 ProgressBar(
    progress: null, // 現在の再生位置
    buffered: null, // ダウンロードが完了した分数
    total: null,    // 音声ファイル全体の分数
    onSeek: (Duration position) => null,   // 再生位置の変更メソッド
  ),

スライダーState

Stateクラス

上記のProgressBarクラスに対して、progressbufferedtotalをState側から与え続ける事で再生に伴って、スライダーを変化させていきます

State側でprogressbufferedtotalに対応する「現在の再生位置」、「ダウンロードが完了した分数」、「音声ファイルのトータル分数」を併せ持つProgressBarStateというクラスを作ってまとめて管理してしまいましょう

just_audio_screen_state.dart
class ProgressBarState {
  ProgressBarState({
    this.progress,
    this.buffered,
    this.total,
  });

  final Duration progress;
  final Duration buffered;
  final Duration total;
}

Streamにサブスクライブ

ProgressBarStateクラスの1つ1つのフィールドの状態値はAudioPlayerクラスの以下3つのStreamから受け取ります

  • PositionStream: progressに対応
  • BufferedPositionStream: bufferedに対応
  • DurationStream: totalに対応

1本1本のStreamに個別にサブスクライブし、それぞれProgressBarStateオブジェクトを更新する様に書く事も出来ますが、記述量が多くなるので、今回はStreamの加工などが容易に出来るrx_dartパッケージを使って、3本のStreamを1本にまとめて、それをサブスクライブしましょう

just_audio_screen_state.dart
  void _listenForProgressBarState() {
    _progressBarSubscription = CombineLatestStream.combine3(
      _audioPlayer.positionStream,
      _audioPlayer.bufferedPositionStream,
      _audioPlayer.durationStream,
      (Duration current, Duration buffer, Duration total) => ProgressBarState(
        current: current,
        buffered: buffer,
        total: total ?? Duration.zero,
      ),
    ).listen((ProgressBarState state) => setProgressBarState(state));
  }

  void setProgressBarState(ProgressBarState state) {
    progressBarState = state;
    notifyListeners();
  }

完成

https://www.youtube.com/watch?v=YeTpNYepzg4

以上で完成です。様々なStreamを組み合わせて挙動を操作しているので少し複雑に見えますが、実際にはStreamの値をそのままUIに反映させているだけであまり難しい事はしていません。

バックグラウンド再生についてもこちらでご紹介しているので参考にしてみてください

https://zenn.dev/heyhey1028/articles/5e1229c0f2224b

参考

https://suragch.medium.com/steaming-audio-in-flutter-with-just-audio-7435fcf672bf

Discussion