システム開発部のTです。
アプリの機能として、効果音を出したいって思うときってありませんか?
ありますよね!
ということで、今回は効果音の実装について書いていきます!

概要

今回は簡単にボタン押したら指定の効果音を鳴らす機能を実装していきたいと思います。

ライブラリについて

今回は、SoundPoolというライブラリを利用します。
Androidにも効果音を利用するときに同名のAPIがございますが、それと同じ感覚で利用できるものになります。


以下のライブラリをpubspec.yamlのdependenciesに設定してください。

soundpool

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  soundpool: ^2.1.0  # <-- これを追加

flutter:

  # To add assets to your application, add an assets section, like this:
  assets:
      - assets/se/   # <-- これも追加

また、効果音のリソースファイルをどこに置くかも合わせて定義しておきましょう。
本件では、プロジェクト直下に「/assets/se/」フォルダを用意したところにファイルを置くので、pubspec.yamlの「flutter:」の「assets:」に「assets/se/」を定義しておきます。

これで準備はできました。

効果音のリソースファイルの準備

本件では、効果音素材としてMP3を利用します。
OGGやAACなどがありますが、MP3は両OSでサポートしているので、こちらを利用します。

プロジェクトトップにて「assets」フォルダを作って、更にその下に「se」フォルダを作ったなかにMP3ファイルを入れていきます。

効果音クラスの実装

早速ですが、効果音クラスとしてSeSoundというクラスを作成していきます。

import 'package:flutter/services.dart';
import 'package:soundpool/soundpool.dart';
import 'dart:io';

enum SeSoundIds {
  Button1,
  Button2,
  Button3,
  Button4,
  Button5,
}

class SeSound {
  String os = Platform.operatingSystem;
  bool isIOS = Platform.isIOS;
  late Soundpool _soundPool;

  final Map<SeSoundIds, int> _seContainer = Map<SeSoundIds, int>();
  final Map<int, int> _streamContainer = Map<int, int>();

  SeSound() {
    // インスタンス生成
    this._soundPool = Soundpool.fromOptions(options: SoundpoolOptions(
        streamType: StreamType.music,
        maxStreams: 5 // 5音同時発音に対応させる
    ));
    // 以降、非同期で実施
        () async {
      // 読み込んだ効果音をバッファに保持
      var button1Id = await rootBundle.load("assets/se/button1.mp3").then((value) => this._soundPool.load(value));
      var button2Id = await rootBundle.load("assets/se/button2.mp3").then((value) => this._soundPool.load(value));
      var button3Id = await rootBundle.load("assets/se/button3.mp3").then((value) => this._soundPool.load(value));
      var button4Id = await rootBundle.load("assets/se/button4.mp3").then((value) => this._soundPool.load(value));
      var button5Id = await rootBundle.load("assets/se/button5.mp3").then((value) => this._soundPool.load(value));
      // バッファに保持した効果音のIDを以下のコンテナに入れておく
      this._seContainer[SeSoundIds.Button1] = button1Id;
      this._seContainer[SeSoundIds.Button2] = button2Id;
      this._seContainer[SeSoundIds.Button3] = button3Id;
      this._seContainer[SeSoundIds.Button4] = button4Id;
      this._seContainer[SeSoundIds.Button5] = button5Id;
      // 効果音を鳴らしたときに保持するためのstreamIdのコンテナを初期化
      // 対象の効果音を強制的に停止する際に使用する
      this._streamContainer[button1Id] = 0;
      this._streamContainer[button2Id] = 0;
      this._streamContainer[button3Id] = 0;
      this._streamContainer[button4Id] = 0;
      this._streamContainer[button5Id] = 0;
    }();
  }

  // 効果音を鳴らすときに本メソッドをEnum属性のSeSoundIdsを引数として実行する
  void playSe(SeSoundIds ids) async {
    // 効果音のIDを取得
    var seId = this._seContainer[ids];
    if (seId != null) {
      // 効果音として存在していたら、以降を実施
      // streamIdを取得
      var streamId = this._streamContainer[seId] ?? 0;
      if (streamId > 0 && isIOS) {
        // streamIdが存在し、かつOSがiOSだった場合、再生中の効果音を強制的に停止させる
        // iOSの場合、再生中は再度の効果音再生に対応していないため、ボタン連打しても再生されないため
        await _soundPool.stop(streamId);
      }
      // 効果音のIDをplayメソッドに渡して再生処理を実施
      // 再生処理の戻り値をstreamIdのコンテナに設定する
      this._streamContainer[seId] = await _soundPool.play(seId);
    } else {
      print("se resource not found! ids: $ids");
    }
  }

  Future<void> dispose() async {
    // 終了時の後始末処理
    await _soundPool.release();
    _soundPool.dispose();
    return Future.value(0);
  }

}

コンストラクタ内で効果音を読み込んで、事前にバッファに保持させることによって、
効果音再生時の遅延を防ぐことができます。
ただし、大きな容量のファイルだった場合、メモリを圧迫してしまうため、効果音の再生時間が極力短いものが望ましいです。だいたい多くても5秒くらいかと思います。

コード上のコメントにも記載していますが、iOSの場合は効果音再生中に、別の効果音再生には対応していません。そのため、OSがiOSに限り別の再生処理が来たら、再生中の効果音を強制的に停止する処理を入れております。

なお、AndroidとWebについても問題なく再生できます。

画面への実装

画面への実装になります。

import 'package:flutter/material.dart';
import 'package:soundtestapp/SeSound.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

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

// 各画面で共通の効果音を使うことを考慮し、外部変数として定義
SeSound se = SeSound();

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(onPressed: (){
              // ボタン押下のタイミングで効果音を再生
              se.playSe(SeSoundIds.Button1);
              }, child: Text("Button1の効果音")),
            ElevatedButton(onPressed: (){
              se.playSe(SeSoundIds.Button2);
              }, child: Text("Button2の効果音")),
            ElevatedButton(onPressed: (){
              se.playSe(SeSoundIds.Button3);
              }, child: Text("Button3の効果音")),
            ElevatedButton(onPressed: (){
              se.playSe(SeSoundIds.Button4);
              }, child: Text("Button4の効果音")),
            ElevatedButton(onPressed: (){
              se.playSe(SeSoundIds.Button5);
              }, child: Text("Button5の効果音")),
          ],
        ),
        alignment: Alignment.center,
      ),
    );
  }
}

上記まで実装したら、アプリを実行してみましょう。
各ボタンを押下すると、対応する効果音を再生されたかと思います。
もちろん、Android、iOSともにボタンを連打しても、連打に追従して音も鳴ると思います。

以上、効果音の実装について書いてみました。

まとめ

今回紹介した効果音の実装は、いかがだったでしょうか?
SoundPoolのクセとか、OSごとの挙動の違いを把握しておけば、コードの内容自体は単純かと思います。
業務用アプリだと音を鳴らす機会は少ないかと思いますが、ゲーム的な効果を狙うと面白いと思いますので、皆さんも使ってみてください。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる