広告 Flutter Dart Unity アプリ開発

Android でmp3ファイルを再生する。

一週間もの試行錯誤の結果、、、なんとか成功

いや〜本当に苦労しました。WEBのChromeで音が出るのに、Androidでは音が出ず。😭
ただのBEEP音でも良いのに、flutter_beepは開発が止まっていて。ブログの古い記事に翻弄されました。
最終的にflutterのaudioplayersのみ開発が継続されていることに気づきました。

鉄則:ブログ記事を鵜呑みにせず、公式パッケージのExampleソースを読もう!!

何が駄目だったのか、どのようにして解決したのか

当初、flutterの開発版を使っていたのですが、日替わりでガンガン仕様が変わり(開発版だから当たり前)
困惑しっぱなしでした。まだ初心者なんだから、素直にStable版を使うことにしました。
(flutter のinstall記事も書き直しました )
僕はUbuntuからクリーンインストールしました。(心配性なので)

必要なのはBEEP音だったので、短すぎるMp3を使っていました。最低0.3秒くらいは無いと駄目でした。
実験するときは、ちゃんと再生できるMp3ファイルを準備しないと泥沼です!!

/android/app/build.gradle.kts に、ndkバージョン指定が必要でした。(コンパイル中にエラーが表示されます。)

......
android {
    namespace = "com.example.audioapp"
    compileSdk = flutter.compileSdkVersion
    // ndkVersion = flutter.ndkVersion
    ndkVersion = "27.0.12077973"
.......

サンプルソース(最低限の音出し)

下記のソースは、公式で紹介しているソースをちょっと改良したものです。
assets/sounds/pi2.mp3 というファイルが入っていることを前提にしています。

pubspeck.yaml のassetの記載も忘れずに。

flutter:
  # ......
  assets:
    - assets/images/
    - assets/sounds/

#スペースによる段つけにも全て意味があります!! 上記のようにフォルダ名のみ記載しましょう。- assetsで始まるのも重要
ソース内では、'sounds/pi2.mp3'のように記載します。

main.dart 赤色部分がaudioplayersに関連しているところです。

import 'dart:async';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'playerwidget.dart';

void main() {
  runApp(const MaterialApp(home: _SimpleExampleApp()));
}

class _SimpleExampleApp extends StatefulWidget {
  const _SimpleExampleApp();

  @override
  _SimpleExampleAppState createState() => _SimpleExampleAppState();
}

class _SimpleExampleAppState extends State<_SimpleExampleApp> {
  late AudioPlayer player = AudioPlayer();  // 空の入れ物を宣言している 

  @override
  void initState() {   // Widget初期化処理
    super.initState();

    // Create the audio player.
    player = AudioPlayer();

    // Set the release mode to keep the source after playback has completed.
    player.setReleaseMode(ReleaseMode.stop);

    // Start the player as soon as the app is displayed.
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await player.setSource(AssetSource('sounds/pi2.mp3'));

      await player.resume();// いきなり音がでます。最初音を出したくなければ、この行は消しましょう。
    });
  }

  @override
  void dispose() { // ゴミ掃除
    // Release all sources and dispose the player.
    player.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Simple Player'),
      ),
      body: PlayerWidget(player: player),      // 見通しが良いように、別ファイルにしてみました。
    );
  }
}

playerwidget.dart 以下は音を再生するICONボタン付きのWidgetです。パッケージを使うためのノウハウが殆ど包含されています。

//playerwidget.dart

import 'dart:async';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';


class PlayerWidget extends StatefulWidget {
  final AudioPlayer player;

  const PlayerWidget({
    required this.player,
    super.key,
  });

  @override
  State<StatefulWidget> createState() {
    return _PlayerWidgetState();
  }
}

class _PlayerWidgetState extends State<PlayerWidget> {
  PlayerState? _playerState;
  Duration? _duration;
  Duration? _position;

  StreamSubscription? _durationSubscription;
  StreamSubscription? _positionSubscription;
  StreamSubscription? _playerCompleteSubscription;
  StreamSubscription? _playerStateChangeSubscription;

  bool get _isPlaying => _playerState == PlayerState.playing;
  bool get _isPaused => _playerState == PlayerState.paused;
  String get _durationText => _duration?.toString().split('.').first ?? '';
  String get _positionText => _position?.toString().split('.').first ?? '';

  AudioPlayer get player => widget.player;

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

    // Use initial values from player
    _playerState = player.state;

    player.getDuration().then(
          (value) => setState(() {
            _duration = value;
          }),
        );

    player.getCurrentPosition().then(
          (value) => setState(() {
            _position = value;
          }),
        );
    _initStreams();
  }

  @override
  void setState(VoidCallback fn) {
    // Subscriptions only can be closed asynchronously,
    // therefore events can occur after widget has been disposed.
    if (mounted) {
      super.setState(fn);
    }
  }

  @override
  void dispose() {
    _durationSubscription?.cancel();
    _positionSubscription?.cancel();
    _playerCompleteSubscription?.cancel();
    _playerStateChangeSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final color = Theme.of(context).primaryColor;
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              key: const Key('play_button'),
              onPressed: _isPlaying ? null : _play,
              iconSize: 48.0,
              icon: const Icon(Icons.play_arrow),
              color: color,
            ),
            IconButton(
              key: const Key('pause_button'),
              onPressed: _isPlaying ? _pause : null,
              iconSize: 48.0,
              icon: const Icon(Icons.pause),
              color: color,
            ),
            IconButton(
              key: const Key('stop_button'),
              onPressed: _isPlaying || _isPaused ? _stop : null,
              iconSize: 48.0,
              icon: const Icon(Icons.stop),
              color: color,
            ),
          ],
        ),
        Slider(    // スライダーをセットした位置から再生する
          onChanged: (value) {
            final duration = _duration;
            if (duration == null) {
              return;
            }
            final position = value * duration.inMilliseconds;
            player.seek(Duration(milliseconds: position.round()));
          },
          value: (_position != null &&
                  _duration != null &&
                  _position!.inMilliseconds > 0 &&
                  _position!.inMilliseconds < _duration!.inMilliseconds)
              ? _position!.inMilliseconds / _duration!.inMilliseconds
              : 0.0,
        ),
        Text(
          _position != null
              ? '$_positionText / $_durationText'
              : _duration != null
                  ? _durationText
                  : '',
          style: const TextStyle(fontSize: 16.0),
        ),
      ],
    );
  }

  void _initStreams() {
    _durationSubscription = player.onDurationChanged.listen((duration) {
      setState(() => _duration = duration);
    });

    _positionSubscription = player.onPositionChanged.listen(
      (p) => setState(() => _position = p),
    );

    _playerCompleteSubscription = player.onPlayerComplete.listen((event) {
      setState(() {
        _playerState = PlayerState.stopped;
        _position = Duration.zero;
      });
    });

    _playerStateChangeSubscription =
        player.onPlayerStateChanged.listen((state) {
      setState(() {
        _playerState = state;
      });
    });
  }

  Future<void> _play() async {
    await player.resume();
    setState(() => _playerState = PlayerState.playing);
  }

  Future<void> _pause() async {
    await player.pause();
    setState(() => _playerState = PlayerState.paused);
  }

  Future<void> _stop() async {
    await player.stop();
    setState(() {
      _playerState = PlayerState.stopped;
      _position = Duration.zero;
    });
  }
}

なお、一定間隔で音を出す例は、日数計算アプリのdripsound.dartを見てください。
(Dart のTimer.periodicを使いましたが、それが良いのかはわからない。再生中に何度も再生コマンドを叩くと落ちるので、ひと工夫必要です)

-Flutter Dart Unity アプリ開発