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

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

その1： 音声ファイルを再生する ⬅︎ 今回
[その2： 音声ファイルをバックグラウンド再生する](https://qiita.com/heyhey1028/items/0504730eb594c7e5f59a)

`just_audio`以外のパッケージを知りたい方は[こちら](https://fluttergems.dev/audio/)を参考にしてみてください

### 環境
Dart: 2.14.4
Flutter: 2.10.3
just_audio: [0.9.19](https://pub.dev/packages/just_audio)
rxdart: [0.27.3](https://pub.dev/packages/rxdart)
audio_video_progress_bar: [0.9.0](https://pub.dev/packages/audio_video_progress_bar)

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

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

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

![just_audio.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334813/dc445e9c-654c-2dbc-0f53-e8a66c657ceb.png)

全体のコードはこちらを参照ください [Github](https://github.com/heyhey1028/flutter-toybox/tree/master/lib/screens/just_audio)

# 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`オブジェクトを流します。

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

```dart: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&token=cf88a17e-bbe9-46de-95a8-e855a23fbb3b';
}
```
上述の通り、`Provider`を使ってUI側に注入していきます。
# 状態管理クラスで準備するメソッド
### 初期化メソッド
状態管理クラス生成時に走らせるメソッドです　
`AudioPlayer`クラスのインスタンス化し、`setUrl()`で音声ファイルのダウンロード先を設定しています。

```dart:just_audio_screen_state.dart
  /* --- INITIALIZE --- */
  void init() {
    _audioPlayer = AudioPlayer()..setUrl(_url); // インスタンス化と音声ファイルの設定
    _listenToPlaybackState(); // ボタン用のサブスクリプションを行うメソッド
    _listenForProgressBarState(); // スライダー用のサブスクリプションを行うメソッド
  }
```
### 音声ファイルの操作メソッド
ボタンとスライダー操作でこちらのメソッドを発火させます
```dart: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側に通知します
```dart:just_audio_screen_state.dart
  /* --- STATE CONTROL --- */
  void setAudioState(AudioState state) {
    audioState = state;
    notifyListeners();
  }

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

### disposeメソッド
状態管理クラスが破棄される際にAudioPlayerオブジェクトの破棄とStreamへのサブスクリプションを閉じてメモリを解放します
```dart:just_audio_screen_state.dart
  @override
  void dispose() {
    _audioPlayer.dispose();
    _playbackSubscription.cancel();
    _progressBarSubscription.cancel();
    super.dispose();
  }
```

# ボタン、スライダー
<iframe width="560" height="315" src="https://www.youtube.com/embed/YvYcPmvIXew" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

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

```dart: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クラス
ボタンが監視する状態値は音声ファイルの状態です。以下４つの状態のいずれかになる状態値としてenumで定義した`AudioState`を状態管理クラス側で管理します。
- `ready`：再生準備OK
- `paused`：停止中
- `playing`：再生中
- `loading`：読み込み中

```dart:just_audio_screen_state.dart
enum AudioState {
  ready,
  paused,
  playing,
  loading,
}
```

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

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

- `PlayerState.processingState`：音声ファイルのダウンロード状況。以下４つの状態を取ります
    - `idle`：ロード前
    - `loading`:ローディング中
    - `buffering`:バッファリング中で再生出来ない状態
    - `ready`：再生可能な量データのローディング完了
    - `completed`:ローディング完了
- `PlayerState.playing`：音声ファイルを再生中かのbool値

```dart: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`クラスは音声メディア再生に特化した様々なフィールドを持っていますが、その中から今回は下記の４つを使っていきます
```dart:just_audio_screen.dart
 ProgressBar(
    progress: null, // 現在の再生位置
    buffered: null, // ダウンロードが完了した分数
    total: null,    // 音声ファイル全体の分数
    onSeek: (Duration position) => null,   // 再生位置の変更メソッド
  ),
```

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

State側で`progress`、`buffered`、`total`に対応する「現在の再生位置」、「ダウンロードが完了した分数」、「音声ファイルのトータル分数」を併せ持つ`ProgressBarState`というクラスを作ってまとめて管理してしまいましょう
```dart: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`クラスの１つ１つのフィールドの状態値は`AudioPlayer`クラスの以下３つのStreamから受け取ります
- `PositionStream`：　progressに対応
- `BufferedPositionStream`： bufferedに対応
- `DurationStream`： totalに対応

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

```dart: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();
  }

```

## 完成

<iframe width="100%" height="315" src="https://www.youtube.com/embed/YeTpNYepzg4" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

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

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

https://qiita.com/heyhey1028/items/0504730eb594c7e5f59a


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