Flutter ゲームにサウンドと音楽を追加する

Flutter ゲームに音声と音楽を追加する

この Codelab について

subject最終更新: 6月 6, 2025
account_circle作成者: Filip Hracek

1. 始める前に

ゲームは映像と音声を伴うエクスペリエンスです。Flutter は、美しいビジュアルと堅牢な UI を構築するための優れたツールであり、ビジュアル面で大きなメリットがあります。残りの要素は音声です。この Codelab では、flutter_soloud プラグインを使用して、低レイテンシのサウンドと音楽をプロジェクトに導入する方法を学びます。すぐに面白い部分を体験できるように、基本的なスキャフォールドから始めます。

手描きのヘッドフォンのイラスト。

ここで学んだ内容は、ゲームだけでなくアプリにも音声を追加するために使用できます。ただし、ほとんどのゲームではサウンドと音楽が必要ですが、ほとんどのアプリでは必要ないため、この Codelab ではゲームに焦点を当てます。

前提条件

  • Flutter に関する基本的な知識。
  • Flutter アプリを実行してデバッグする方法に関する知識。

学習内容

  • ワンショット音声を再生する方法。
  • ギャップレスの音楽ループを再生してカスタマイズする方法。
  • 音声をフェードイン / フェードアウトする方法。
  • 環境効果を音に適用する方法。
  • 例外への対処方法。
  • これらの機能をすべて 1 つのオーディオ コントローラにカプセル化する方法。

必要なもの

  • Flutter SDK
  • 任意のコードエディタ

2. 設定

  1. 次のファイルをダウンロードします。接続が遅い場合でも、後で実際のファイルが必要になる場合は、作業中にダウンロードしておきます。
  1. 任意の名前で Flutter プロジェクトを作成します。
  1. プロジェクトに lib/audio/audio_controller.dart ファイルを作成します。
  2. ファイルに次のコードを入力します。

lib/audio/audio_controller.dart

import 'dart:async';

import 'package:logging/logging.dart';

class AudioController {
 
static final Logger _log = Logger('AudioController');

 
Future<void> initialize() async {
   
// TODO
 
}

 
void dispose() {
   
// TODO
 
}

 
Future<void> playSound(String assetKey) async {
   
_log.warning('Not implemented yet.');
 
}

 
Future<void> startMusic() async {
   
_log.warning('Not implemented yet.');
 
}

 
void fadeOutMusic() {
   
_log.warning('Not implemented yet.');
 
}

 
void applyFilter() {
   
// TODO
 
}

 
void removeFilter() {
   
// TODO
 
}
}

ご覧のとおり、これは今後の機能の骨組みにすぎません。この Codelab ですべて実装します。

  1. 次に、lib/main.dart ファイルを開き、その内容を次のコードに置き換えます。

lib/main.dart

import 'dart:developer' as dev;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';

import 'audio/audio_controller.dart';

void main() async {
 
// The `flutter_soloud` package logs everything
 
// (from severe warnings to fine debug messages)
 
// using the standard `package:logging`.
 
// You can listen to the logs as shown below.
 
Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
 
Logger.root.onRecord.listen((record) {
   
dev.log(
     
record.message,
     
time: record.time,
     
level: record.level.value,
     
name: record.loggerName,
     
zone: record.zone,
     
error: record.error,
     
stackTrace: record.stackTrace,
   
);
 
});

 
WidgetsFlutterBinding.ensureInitialized();

 
final audioController = AudioController();
 
await audioController.initialize();

 
runApp(MyApp(audioController: audioController));
}

class MyApp extends StatelessWidget {
 
const MyApp({required this.audioController, super.key});

 
final AudioController audioController;

 
@override
 
Widget build(BuildContext context) {
   
return MaterialApp(
     
title: 'Flutter SoLoud Demo',
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
     
),
     
home: MyHomePage(audioController: audioController),
   
);
 
}
}

class MyHomePage extends StatefulWidget {
 
const MyHomePage({super.key, required this.audioController});

 
final AudioController audioController;

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

class _MyHomePageState extends State<MyHomePage> {
 
static const _gap = SizedBox(height: 16);

 
bool filterApplied = false;

 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
appBar: AppBar(title: const Text('Flutter SoLoud Demo')),
     
body: Center(
       
child: Column(
         
mainAxisAlignment: MainAxisAlignment.center,
         
children: <Widget>[
           
OutlinedButton(
             
onPressed: () {
               
widget.audioController.playSound('assets/sounds/pew1.mp3');
             
},
             
child: const Text('Play Sound'),
           
),
           
_gap,
           
OutlinedButton(
             
onPressed: () {
               
widget.audioController.startMusic();
             
},
             
child: const Text('Start Music'),
           
),
           
_gap,
           
OutlinedButton(
             
onPressed: () {
               
widget.audioController.fadeOutMusic();
             
},
             
child: const Text('Fade Out Music'),
           
),
           
_gap,
           
Row(
             
mainAxisSize: MainAxisSize.min,
             
children: [
               
const Text('Apply Filter'),
               
Checkbox(
                 
value: filterApplied,
                 
onChanged: (value) {
                   
setState(() {
                     
filterApplied = value!;
                   
});
                   
if (filterApplied) {
                     
widget.audioController.applyFilter();
                   
} else {
                     
widget.audioController.removeFilter();
                   
}
                 
},
               
),
             
],
           
),
         
],
       
),
     
),
   
);
 
}
}
  1. 音声ファイルがダウンロードされたら、プロジェクトのルートに assets というディレクトリを作成します。
  2. assets ディレクトリに、musicsounds という 2 つのサブディレクトリを作成します。
  3. ダウンロードしたファイルをプロジェクトに移動して、曲ファイルが assets/music/looped-song.ogg ファイルに、ピュー音が次のファイルに配置されるようにします。
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

プロジェクトの構造は次のようになります。

プロジェクトのツリービュー。[android] や [ios] などのフォルダ、[README.md] や [analysis_options.yaml] などのファイルがあります。これらの中には、[music] サブディレクトリと [sounds] サブディレクトリを含む [assets] ディレクトリ、[main.dart] を含む [lib] ディレクトリ、[audio_controller.dart] を含む [audio] サブディレクトリ、[pubspec.yaml] ファイルがあります。矢印は、新しいディレクトリと、これまでに操作したファイルを示しています。

ファイルが作成されたので、Flutter にそのファイルがあることを伝える必要があります。

  1. pubspec.yaml ファイルを開き、ファイルの下部にある flutter: セクションを次のように置き換えます。

pubspec.yaml

...

flutter
:
  uses
-material-design: true

  assets
:
   
- assets/music/
   
- assets/sounds/
  1. flutter_soloud パッケージと logging パッケージへの依存関係を追加します。
flutter pub add flutter_soloud logging

これで、pubspec.yaml ファイルに flutter_soloud パッケージと logging パッケージに対する追加の依存関係が設定されます。

pubspec.yaml

...

dependencies
:
  flutter
:
    sdk
: flutter

  flutter_soloud
: ^3.1.10
  logging
: ^1.3.0

...
  1. プロジェクトを実行します。まだ機能しません。この機能は次のセクションで追加します。

10f0f751c9c47038.png

3. 初期化とシャットダウン

音声を再生するには、flutter_soloud プラグインを使用します。このプラグインは、Nintendo SNES Classic などで使用されているゲーム用の C++ オーディオ エンジンである SoLoud プロジェクトに基づいています。

7ce23849b6d0d09a.png

SoLoud オーディオ エンジンを初期化する手順は次のとおりです。

  1. audio_controller.dart ファイルで、flutter_soloud パッケージをインポートし、プライベート _soloud フィールドをクラスに追加します。

lib/audio/audio_controller.dart

import 'dart:async';

import 'package:flutter_soloud/flutter_soloud.dart';  // Add this...
import 'package:logging/logging.dart';

class AudioController {
 
static final Logger _log = Logger('AudioController');

 
SoLoud? _soloud;                                    // ... and this.

 
Future<void> initialize() async {
   
// TODO
 
}

 
...

オーディオ コントローラは、このフィールドを介して基盤となる SoLoud エンジンを管理し、すべての呼び出しを転送します。

  1. initialize() メソッドに次のコードを入力します。

lib/audio/audio_controller.dart

...

 
Future<void> initialize() async {
    _soloud
= SoLoud.instance;
    await _soloud
!.init();
 
}

...

これにより、_soloud フィールドが入力され、初期化を待機します。次の点にご注意ください。

  • SoLoud はシングルトンの instance フィールドを提供します。複数の SoLoud インスタンスをインスタンス化することはできません。これは C++ エンジンでは許可されていないため、Dart プラグインでも許可されません。
  • プラグインの初期化は非同期で、init() メソッドが返されるまで完了しません。
  • この例では簡潔にするため、try/catch ブロックでエラーをキャッチしていません。本番環境のコードでは、エラーが発生した場合はユーザーに報告する必要があります。
  1. dispose() メソッドに次のコードを入力します。

lib/audio/audio_controller.dart

...

 
void dispose() {
    _soloud
?.deinit();
 
}

...

アプリの終了時に SoLoud をシャットダウンすることをおすすめしますが、シャットダウンしなくても問題なく動作します。

  1. AudioController.initialize() メソッドはすでに main() 関数から呼び出されています。つまり、プロジェクトをホット リブートすると SoLoud がバックグラウンドで初期化されますが、実際に音を再生するまで何も機能しません。

4. ワンショット音声を再生する

アセットを読み込んで再生する

SoLoud が起動時に初期化されることがわかったので、音を鳴らすように指示できます。

SoLoud は、音声ソース(音声の記述に使用されるデータとメタデータ)と、実際に再生される音声の「音声インスタンス」を区別します。音声ソースの例としては、メモリに読み込まれて再生可能な状態の MP3 ファイルがあり、AudioSource クラスのインスタンスで表されます。このオーディオ ソースを再生するたびに、SoLoud は SoundHandle 型で表される「サウンド インスタンス」を作成します。

AudioSource インスタンスは読み込むことで取得できます。たとえば、アセットに mp3 ファイルがある場合は、そのファイルを読み込んで AudioSource を取得できます。次に、この AudioSource を再生するよう SoLoud に指示します。複数回プレイすることも、同時にプレイすることもできます。

音声ソースの使用が完了したら、SoLoud.disposeSource() メソッドで破棄します。

アセットを読み込んで再生する手順は次のとおりです。

  1. AudioController クラスの playSound() メソッドに次のコードを入力します。

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    final source = await _soloud!.loadAsset(assetKey);
    await _soloud!.play(source);
  }

  ...
  1. ファイルを保存してホットリロードし、[サウンドを再生] を選択します。シューという音が聞こえます。次の点にご注意ください。
  • 指定された assetKey 引数は assets/sounds/pew1.mp3 のようになります。これは、Image.asset() ウィジェットなど、他のアセット読み込み Flutter API に指定する文字列と同じです。
  • SoLoud インスタンスには、Flutter プロジェクトのアセットから音声ファイルを非同期で読み込み、AudioSource クラスのインスタンスを返す loadAsset() メソッドが用意されています。ファイル システムからファイルを読み込むメソッド(loadFile() メソッド)と、URL からネットワーク経由で読み込むメソッド(loadUrl() メソッド)があります。
  • 新しく取得した AudioSource インスタンスは、SoLoud の play() メソッドに渡されます。このメソッドは、新しく再生されるサウンドを表す SoundHandle 型のインスタンスを返します。このハンドルは、他の SoLoud メソッドに渡して、音の一時停止、停止、音量の変更などの操作を行うことができます。
  • play() は非同期メソッドですが、基本的には再生は即座に開始されます。flutter_soloud パッケージは、Dart の外部関数インターフェース(FFI)を使用して、C コードを直接同期的に呼び出します。ほとんどの Flutter プラグインに特徴的な、Dart コードとプラットフォーム コードとの間での通常のメッセージのやり取りは、どこにも見当たりません。一部のメソッドが非同期である唯一の理由は、プラグインのコードの一部が独自のアイソレートで実行され、Dart アイソレート間の通信が非同期であるためです。
  • _soloud! を使用して、_soloud フィールドが null でないことを確認します。これは簡潔にするためです。本番環境のコードでは、オーディオ コントローラが完全に初期化される前にデベロッパーが音声を再生しようとした場合に、適切に対処する必要があります。

例外に対処する

例外が発生する可能性を無視していることに、気づいたかもしれません。学習目的で、この特定のメソッドを修正する時が来ました。(簡潔にするために、このセクションの後で例外の無視に戻ります)。

  • この場合の例外に対処するには、playSound() メソッドの 2 行を try/catch ブロックでラップし、SoLoudException のインスタンスのみをキャッチします。

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    try {
      final source = await _soloud!.loadAsset(assetKey);
      await _soloud!.play(source);
    } on SoLoudException catch (e) {
      _log.severe("Cannot play sound '$assetKey'. Ignoring.", e);
    }
  }

  ...

SoLoud は、SoLoudNotInitializedException 例外や SoLoudTemporaryFolderFailedException 例外など、さまざまな例外をスローします。各メソッドの API ドキュメントには、スローされる可能性のある例外の種類が記載されています。

SoLoud には、すべての例外の親クラスである SoLoudException 例外も用意されているため、オーディオ エンジンの機能に関連するすべてのエラーをキャッチできます。これは、音声の再生が重要でない場合に特に役立ちます。たとえば、銃撃音の 1 つが読み込まれなかったという理由だけで、プレーヤーのゲーム セッションをクラッシュさせたくない場合などです。

当然のことながら、存在しないアセットキーを指定すると、loadAsset() メソッドで FlutterError エラーがスローされることもあります。ゲームにバンドルされていないアセットを読み込もうとすることは、通常は対処すべき問題であるため、エラーとなります

さまざまな音を再生する

pew1.mp3 ファイルのみが再生されていることに気付いたかもしれません。しかし、assets ディレクトリには、この音声の他にも 2 つのバージョンがあります。ゲームで同じ音声の複数のバージョンを用意し、ランダムに再生したり、ローテーションで再生したりすると、より自然な音になります。これにより、足音や銃声が均一になりすぎ、偽物のように聞こえるのを防ぐことができます。

  • オプションの演習として、ボタンをタップするたびに異なるピュー音を鳴らすようにコードを変更します。

An illustration of

5. 音楽ループを再生する

長時間再生される音声を管理する

一部の音声は長時間再生することを目的としています。音楽が最もわかりやすい例ですが、多くのゲームでは、廊下を吹き抜ける風、遠くから聞こえる僧侶の唱歌、何百年も前の金属のきしむ音、遠くから聞こえる患者の咳など、環境音も再生されます。

再生時間が分単位で測定できるオーディオ ソースです。必要に応じて一時停止または停止できるように、これらのタスクを記録しておく必要があります。また、多くの場合、大きなファイルが基盤となっているため、メモリを大量に消費する可能性があります。追跡するもう 1 つの理由は、不要になった AudioSource インスタンスを破棄できるようにするためです。

このため、AudioController に新しいプライベート フィールドを導入します。再生中の曲のハンドル(ある場合)。次の行を追加します。

lib/audio/audio_controller.dart

...

class AudioController {
 
static final Logger _log = Logger('AudioController');

 
SoLoud? _soloud;

 
SoundHandle? _musicHandle;    // ← Add this.

 
...

音楽を開始する

基本的に、音楽の再生はワンショット音声の再生と変わりません。それでも、まず assets/music/looped-song.ogg ファイルを AudioSource クラスのインスタンスとして読み込み、次に SoLoud の play() メソッドを使用して再生する必要があります。

今回は、play() メソッドが返すサウンドハンドルを取得して、再生中に音声を操作します。

  • 必要に応じて、AudioController.startMusic() メソッドを独自に実装します。一部の詳細が正しくなくても問題ありません。重要なのは、[音楽を開始] を選択すると音楽が開始されることです。

リファレンス実装は次のとおりです。

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    final musicSource = await _soloud!.loadAsset(
      'assets/music/looped-song.ogg',
      mode: LoadMode.disk,
    );
  }

...

音楽ファイルをディスクモード(LoadMode.disk 列挙型)で読み込んでいます。つまり、ファイルは必要に応じてチャンクでのみ読み込まれます。長時間再生する音声の場合は、通常はディスクモードで読み込むことをおすすめします。短い効果音の場合は、メモリに読み込んで解凍する(デフォルトの LoadMode.memory 列挙型)方が理にかなっています。

ただし、いくつか問題があります。まず、音楽が大きすぎて、音声が聞こえません。ほとんどのゲームでは、音楽はほとんどの場合バックグラウンドで再生され、音声や効果音などのより有益な音声が中心になります。これは、play メソッドの volume パラメータの使用を修正するためのものです。たとえば、_soloud!.play(musicSource, volume: 0.6) と入力すると、音量を 60% にして曲を再生できます。または、_soloud!.setVolume(_musicHandle, 0.6) などで後で音量を設定することもできます。

2 つ目の問題は、曲が突然停止することです。これは、ループ再生が想定されている曲で、ループの開始点が音声ファイルの開始点ではないためです。

88d2c57fffdfe996.png

曲が自然なイントロで始まり、ループポイントが目立たずに必要な長さだけ再生されるため、ゲーム音楽によく使用されます。ゲームが再生中の曲から移行する必要がある場合は、曲がフェードアウトします。

幸い、SoLoud にはループ再生する方法が用意されています。play() メソッドは、looping パラメータにブール値を、loopingStartAt パラメータにループの開始点の値を受け取ります。変更後のコードは次のようになります。

lib/audio/audio_controller.dart

...

_musicHandle = await _soloud!.play(
  musicSource,
  volume: 0.6,
  looping: true,
  // The exact timestamp of the start of the loop.
  loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);

...

loopingStartAt パラメータを設定しない場合、デフォルトは Duration.zero(つまり、音声ファイルの先頭)になります。導入部分のない完璧なループの音楽トラックがある場合は、この方法が適しています。

  • 再生が終了した後、音声ソースが適切に破棄されていることを確認するには、各音声ソースが提供する allInstancesFinished ストリームをリッスンします。ログ呼び出しを追加した startMusic() メソッドは次のようになります。

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    _log.info('Loading music');
    final musicSource = await _soloud!.loadAsset(
      'assets/music/looped-song.ogg',
      mode: LoadMode.disk,
    );
    musicSource.allInstancesFinished.first.then((_) {
      _soloud!.disposeSource(musicSource);
      _log.info('Music source disposed');
      _musicHandle = null;
    });

    _log.info('Playing music');
    _musicHandle = await _soloud!.play(
      musicSource,
      volume: 0.6,
      looping: true,
      loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
    );
  }

...

音声をフェードする

次の問題は、音楽が終わらないことです。フェードを実装しましょう。

フェードを実装する方法の一つは、TickerTimer.periodic など、毎秒数回呼び出される関数を用意し、音楽の音量を少しずつ下げる方法です。これは機能しますが、手間がかかります。

幸い、SoLoud には、この処理を自動で行う便利なファイア アンド フォーゲット メソッドが用意されています。以下では、5 秒かけて音楽をフェードアウトし、音声インスタンスを停止して CPU リソースを不必要に消費しないようにする方法について説明します。fadeOutMusic() メソッドを次のコードで置き換えます。

lib/audio/audio_controller.dart

...

  void fadeOutMusic() {
    if (_musicHandle == null) {
      _log.info('Nothing to fade out');
      return;
    }
    const length = Duration(seconds: 5);
    _soloud!.fadeVolume(_musicHandle!, 0, length);
    _soloud!.scheduleStop(_musicHandle!, length);
  }

...

6. エフェクトを適用する

適切なオーディオ エンジンを利用できる大きなメリットの 1 つは、リバーブ、イコライザー、ローパス フィルタなどを介して音声をルーティングするなど、オーディオ処理を行えることです。

ゲームでは、場所を音で区別するために使用できます。たとえば、森林とコンクリート製のバンカーでは、拍手音が異なります。森は音を拡散して吸収するのに役立ちますが、バンカーのむき出しの壁は音波を反射して残響を生みます。同様に、壁越しに聞こえる人の声は、直接聞こえる声とは異なります。これらの音の高周波は、固体媒体を伝わるにつれて減衰し、ローパス フィルタ効果が生じます。

部屋で会話している 2 人のイラスト。音波は、ある人から別の人へと直接伝わるだけでなく、壁や天井に反射して伝わります。

SoLoud には、オーディオに適用できるさまざまなオーディオ エフェクトが用意されています。

  • プレイヤーが大聖堂や洞窟などの大きな部屋にいるように聞こえるようにするには、SoLoud.filters フィールドを使用します。

lib/audio/audio_controller.dart

...

 
void applyFilter() {
    _soloud
!.filters.freeverbFilter.activate();
    _soloud
!.filters.freeverbFilter.wet.value = 0.2;
    _soloud
!.filters.freeverbFilter.roomSize.value = 0.9;
 
}

 
void removeFilter() {
    _soloud
!.filters.freeverbFilter.deactivate();
 
}

...

SoLoud.filters フィールドを使用すると、すべてのフィルタタイプとそのパラメータにアクセスできます。すべてのパラメータには、徐々にフェードする、振動するなどの機能も組み込まれています。

注: _soloud!.filters はグローバル フィルタを公開します。1 つのソースにフィルタを適用する場合は、同じ動作をする対応する AudioSource.filters を使用します。

上記のコードでは、次の処理が行われます。

  • freeverb フィルタをグローバルに有効にします。
  • Wet パラメータを 0.2 に設定します。これにより、生成されるオーディオは 80% が元の音声で、20% がリバーブ エフェクトの出力になります。このパラメータを 1.0 に設定すると、部屋の遠くにある壁から反射した音波のみが聞こえ、元の音声は聞こえなくなります。
  • [Room Size] パラメータを 0.9 に設定します。このパラメータは必要に応じて調整したり、動的に変更したりできます。1.0 は巨大な洞窟で、0.0 はバスルームです。
  • 可能であれば、コードを変更して、次のフィルタのいずれか、または次のフィルタの組み合わせを適用します。
  • biquadFilter(低周波フィルタとして使用可能)
  • pitchShiftFilter
  • equalizerFilter
  • echoFilter
  • lofiFilter
  • flangerFilter
  • bassboostFilter
  • waveShaperFilter
  • robotizeFilter

7. 完了

音声を再生し、音楽をループして、エフェクトを適用するオーディオ コントローラを実装しました。

その他の情報

  • 起動時にサウンドをプリロードする、曲を順番に再生する、時間の経過とともにフィルタを徐々に適用するなどの機能を使って、オーディオ コントローラをさらに活用しましょう。
  • flutter_soloudパッケージ ドキュメントを読む。
  • 基盤となる C++ ライブラリのホームページをご覧ください。
  • C++ ライブラリとのインターフェースに使用される技術である Dart FFI の詳細を確認する。
  • ゲーム サウンド プログラミングに関する Guy Somberg の講演を見て、アイデアを得ましょう。(長い動画もあります)。ガイが「ミドルウェア」と言っているのは、SoLoud や FMOD などのライブラリを指しています。残りのコードはゲームごとに固有のものです。
  • ゲームをビルドしてリリースします。

ヘッドフォンのイラスト