🎶

[Flutter] just_audioで音を再生する

2021/11/18に公開

Flutter製アプリで音を再生する方法をご紹介します。
今回は、just_audioと呼ばれるライブラリを活用します。
https://pub.dev/packages/just_audio
このライブラリは

  • 機能が豊富(クラスが多すぎて特定の機能を探すのに苦労するほどw)
  • 評価が高い(2021/11/17時点 1391 LIKES / 130 PUBPOINTS)
  • Exampleを通して、アプリで使う必要最小限の実装が説明されている

以上の理由で筆者は魅力を感じています。

↓はデモ動画です。

準備

$ flutter pub add just_audio
pubspec.yaml
flutter:
  assets:
    - assets/audio/cute.mp3

説明

初期化

初期化
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';


late AudioPlayer _player;
bool _changeAudioSource = false;

@override
void initState() {
   super.initState();
   _setupSession();
}

Future<void> _setupSession() async {
   _player = AudioPlayer();
   final session = await AudioSession.instance;
   await session.configure(AudioSessionConfiguration.speech());
   await _loadAudioFile();
}

Future<void> _loadAudioFile() async {
   try {
      if(_changeAudioSource) {
         await _player.setUrl('https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3'); // ストリーミング
      } else {
         await _player.setAsset('assets/audio/cute.mp3'); // アセット(ローカル)のファイル
      }
   } catch(e) {
      print(e);
   }
}

再生する音声ファイルの種類に合わせ、プレーヤーを最適化させる

プレーヤーの最適化
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());

OS側にオーディオの詳細設定を通知するaudio_sessionパッケージを活用します。
オーディオは

  • AudioSessionConfiguration.music:音楽プレーヤー
  • AudioSessionConfiguration.speech:連続した音声(ポッドキャストやオーディオブックなど)

上記2種類用意されており、アプリ応じて適時宣言するとクオリティが向上すると思われる。

ローカルの音声ファイルをロードする

ローカル
await _player.setAsset('assets/audio/cute.mp3');

pubspec.yamlに宣言した音声ファイルのパスを指定します。またsetAsset methodFutureのため、awaitを付与すると安心な設計になります。

WEB上の音声ファイルをロードする

ストリーミング
await _player.setUrl('https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3');

URLを指定してあげれば実装できます。またsetUrl methodFutureのため、awaitを付与すると安心な設計になります。

再生状態と現在地を検出する

再生状態と現在地を検出する
_player.playbackEventStream.listen((event) {
   switch(event.processingState) {
      case ProcessingState.idle:
         print('オーディオファイルをロードしていないよ');
         break;
      case ProcessingState.loading:
         print('オーディオファイルをロード中だよ');
         break;
      case ProcessingState.buffering:
         print('バッファリング(読み込み)中だよ');
         break;
      case ProcessingState.ready:
         print('再生できるよ');
         break;
      case ProcessingState.completed:
         print('再生終了したよ');
         break;
       default:
         print(event.processingState);
         break;
      }
});

playbackEventStream property PlaybackEvent classprocessingState propertyを取得します。これはプレーヤーの処理状態を列挙し、再生状態に合わせて処理を適時宣言することができます。

ProcessingState enumのConstants

  • buffering : バッファリング中
  • completed : 再生完了
  • idle : ロードしていない
  • loading : ロード中
  • ready : バッファリングが完了し、再生できる
  • values : 宣言順の定数リスト

別の方法で再生状態を検知する

playerStateStream propertyで再生状態を検知可能です。こちらはPlayerState classで再生状態と処理状態を検知可能だが、stop methodと相性が悪い。
処理状態も取得するため、stop methodでリソースを解放すると状態が検知できずエラーを吐く原因になるため、筆者は基本的にplaybackEventStream propertyを使う。だが、ケースバイケースで使い分けしなければいけない。

再生

再生
Future<void> _playSoundFile() async {
   // 再生終了状態の場合、新たなオーディオファイルを定義し再生できる状態にする
   if(_player.processingState == ProcessingState.completed) {
      await _loadAudioFile();
   }

   await _player.setSpeed(_currentSliderValue); // 再生速度を指定
   await _player.play();
}

オーディオファイルを再定義する

再生終了を確認
if(_player.processingState == ProcessingState.completed) {
   await _loadAudioFile();
}

再生終了した場合、オーディオソースを再読み込みせずProcessingStatecompletedのままです。そのため、再生処理する前に一度if文で状態を確認しソースを再定義してください。

再生速度を設定する

再生速度
await _player.setSpeed(_currentSliderValue);

doubleを指定することで再生速度を調整可能。(デフォルトは1.0)
1.1以上の値を設定した場合、再生速度が(音声ファイルの)ダウンロード速度より速いとフリーズする可能性があるため、注意が必要である。

再生する

再生
await _player.play();

再生する。またplay methodFutureのため、awaitを付与すると安心な設計になります。

全体

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Demo just_audio Package'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late AudioPlayer _player;
  double _currentSliderValue = 1.0;
  bool _changeAudioSource = false;
  String _stateSource = 'アセットを再生';

  @override
  void initState() {
    super.initState();
    _setupSession();

    // AudioPlayerの状態を取得
    _player.playbackEventStream.listen((event) {
      switch(event.processingState) {
        case ProcessingState.idle:
          print('オーディオファイルをロードしていないよ');
          break;
        case ProcessingState.loading:
          print('オーディオファイルをロード中だよ');
          break;
        case ProcessingState.buffering:
          print('バッファリング(読み込み)中だよ');
          break;
        case ProcessingState.ready:
          print('再生できるよ');
          break;
        case ProcessingState.completed:
          print('再生終了したよ');
          break;
        default:
          print(event.processingState);
          break;
      }
    });
  }

  Future<void> _setupSession() async {
    _player = AudioPlayer();
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration.speech());
    await _loadAudioFile();
  }

  @override
  void dispose() {
    _player.dispose();
    super.dispose();
  }

  void _takeTurns() {
    late String _changeStateText;
    _changeAudioSource = _changeAudioSource ? false : true; // 真偽値を反転

    _player.stop();
    _loadAudioFile().then((_) {
      if(_changeAudioSource) {
        _changeStateText = 'ストリーミング再生';
      } else {
        _changeStateText = 'アセットを再生';
      }
      setState((){
        _stateSource = _changeStateText;
      });
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(_stateSource),
            Slider(
              value: _currentSliderValue,
              min: 0,
              max: 10.0,
              divisions: 10,
              label: _currentSliderValue.toString(),
              onChanged: (double value) {
                setState(() {
                  _currentSliderValue = value;
                });
              },
            ),
            Text(_currentSliderValue.toString()),
            IconButton(
              icon: const Icon(Icons.play_arrow),
              onPressed: () async => await _playSoundFile(),
            ),
            IconButton(
              icon: const Icon(Icons.pause),
              onPressed: () async => await _player.pause(),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _takeTurns,
        tooltip: 'Increment',
        child: const Icon(Icons.autorenew),
      ),
    );
  }

  Future<void> _playSoundFile() async {
    // 再生終了状態の場合、新たなオーディオファイルを定義し再生できる状態にする
    if(_player.processingState == ProcessingState.completed) {
      await _loadAudioFile();
    }

    await _player.setSpeed(_currentSliderValue); // 再生速度を指定
    await _player.play();
  }

  Future<void> _loadAudioFile() async {
    try {
      if(_changeAudioSource) {
        await _player.setUrl('https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3'); // ストリーミング
      } else {
        await _player.setAsset('assets/audio/cute.mp3'); // アセット(ローカル)のファイル
      }
    } catch(e) {
      print(e);
    }
  }
}

リポジトリ

https://github.com/r0227n/Learn_Flutter_App/tree/main/just_audio_sample

スクラップ

https://zenn.dev/r0227n/scraps/68dc79482f9f69

Discussion