はじめに (対象読者・この記事でわかること)

本記事は、Android アプリ開発に携わる Java エンジニア、もしくは Kotlin から Java へ移行中の方を対象としています。特に、ゲームやマルチメディアアプリで「複数の BGM を同時に再生しつつ、特定の BGM の音量だけを個別にコントロールしたい」シーンに直面したことがある方に役立ちます。この記事を読むことで、MediaPlayerSoundPool の組み合わせによるマルチストリーム再生の基本構成、音量制御の実装手順、実装時に遭遇しやすい問題とその回避策を具体的に理解し、自分のプロジェクトにすぐに適用できるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Android 開発の基礎(Activity、Lifecycle 等)
  • Java の基本的な文法と例外処理
  • Android の Audio API(MediaPlayerSoundPool)の概要

概要と背景

Android アプリで BGM を複数同時に流すケースは、ゲームのステージごとのバックグラウンド音楽に加えて、環境音や効果音を同時に再生したい場合によく見られます。標準的な MediaPlayer は単一の音源を扱うことが前提で、複数インスタンスを作成すれば同時再生は可能ですが、音量を個別に細かくコントロールする API が限定的です。一方、SoundPool は多数の短音声を同時にミックス再生でき、個々のストリームに対して音量や再生速度をリアルタイムで変更できます。ただし、SoundPool は主に短いサウンドエフェクト向けに最適化されており、数分以上の長時間 BGM の再生にはメモリ消費が大きくなるリスクがあります。

そこで、本稿では「長時間の BGM は MediaPlayer、短時間のループ系 BGM や効果音は SoundPool」というハイブリッド構成を採用し、特定の MediaPlayer インスタンスだけ音量を変更できる 方法を示します。ポイントは以下です。

  1. AudioAttributes の設定MediaPlayerSoundPool の両方に同一の AudioAttributes を付与し、システム側で同一オーディオセッションとして扱わせる。
  2. 音量制御の抽象化AudioManagersetStreamVolume ではなく、MediaPlayer#setVolume(float left, float right) を直接呼び出すことで、他のストリームに影響を与えずに目的の BGM のみ音量を調整できる。
  3. リソース管理MediaPlayerrelease()SoundPoolunload()release() を忘れずに実行し、メモリリークを防止する。

この構成により、ステージ BGM(長時間)と環境音(短時間)を同時に流しながら、ユーザーが「BGM のボリュームだけ上げたい」操作に対して瞬時に反応できます。

実装手順とコード例

以下では、具体的な実装手順をステップごとに示します。コード例は Android Studio (API 21 以上) を前提にしています。

ステップ 1:共通 AudioAttributes の定義

Java
// AudioAttributes を共通化 AudioAttributes audioAttrs = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_GAME) // ゲーム系アプリの場合 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build();

AudioAttributes を共通化することで、MediaPlayerSoundPool が同一オーディオセッションに属し、相互に干渉しにくくなります。

ステップ 2:MediaPlayer インスタンスの作成と BGM の準備

Java
// ステージ BGM(長時間)用 MediaPlayer MediaPlayer stagePlayer = MediaPlayer.create(context, R.raw.stage_bgm); stagePlayer.setAudioAttributes(audioAttrs); stagePlayer.setLooping(true); // ループ再生 stagePlayer.setVolume(1.0f, 1.0f); // 初期音量はフル stagePlayer.start();

ステップ 3:SoundPool の初期化と環境音のロード

Java
// SoundPool の初期化(最大同時再生数は 5 とする) SoundPool soundPool = new SoundPool.Builder() .setMaxStreams(5) .setAudioAttributes(audioAttrs) .build(); // 環境音をロード int envSoundId = soundPool.load(context, R.raw.environment_loop, 1);

ステップ 4:環境音の再生(ループ)

Java
// 再生時にループフラグを true に設定 int envStreamId = soundPool.play(envSoundId, 0.6f, 0.6f, 1, -1, 1.0f);

上記で -1 を指定すると無限ループ再生になるため、ステージ全体で環境音が常に流れ続けます。

ステップ 5:特定 BGM の音量変更 UI の実装例

XML(例):

Xml
<SeekBar android:id="@+id/volumeSeekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="100"/>

Activity での処理:

Java
SeekBar volumeSeekBar = findViewById(R.id.volumeSeekBar); volumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { float volume = progress / 100f; // 0.0〜1.0 の範囲に正規化 stagePlayer.setVolume(volume, volume); // MediaPlayer のみ音量調整 } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} });

ここで stagePlayer.setVolume を利用するため、SoundPool の音量は独立したままです。ユーザーはスライダーでステージ BGM の音量だけを変えることができます。

ハマった点やエラー解決

問題 1:SoundPool の音量が MediaPlayer の音量変更に追随してしまう

  • 原因AudioAttributessetUsageUSAGE_MEDIA にしていたため、システムが同一ストリームとして扱い、全体音量が変動した。
  • 解決策USAGE_GAME を使用し、ContentTypeMUSIC に統一。これにより、MediaPlayerSoundPool が別々のストリームとして扱われ、個別音量制御が可能になる。

問題 2:長時間 BGM の途中で MediaPlayer が停止する

  • 原因:画面回転や Activity 再生成時に MediaPlayer が再生成されず、リソースが解放される。
  • 解決策ViewModelMediaPlayer ラッパーを保持し、onCleared()release() する。もしくは android:configChanges="orientation|screenSize" をマニフェストに追加し、Activity の再生成を防ぐ。

問題 3:SoundPool のロードが完了する前に play() を呼び出すとエラーになる

  • 原因:非同期ロードのまま play() を実行したため、サウンド ID が有効でない。
  • 解決策SoundPool#setOnLoadCompleteListener を使用し、ロード完了コールバックで play() を呼び出す。
Java
soundPool.setOnLoadCompleteListener((sp, sampleId, status) -> { if (status == 0) { envStreamId = sp.play(sampleId, 0.6f, 0.6f, 1, -1, 1.0f); } });

解決策のまとめ

  • AudioAttributesUSAGE_GAME に統一し、ストリームを分離。
  • MediaPlayer#setVolume で個別音量制御し、SoundPool の音量は固定または別途 UI で調整。
  • SoundPool のロード完了を必ず待つ。
  • MediaPlayer のライフサイクルは ViewModel で管理し、画面回転時のリソース破棄を防止。

まとめ

本記事では、Android (Java) 環境において 複数の BGM を同時再生しつつ、特定の BGM のみ音量を個別に変更する 方法を解説しました。

  • ハイブリッド構成:長時間 BGM は MediaPlayer、短時間ループ音は SoundPool を使用。
  • 音量制御MediaPlayer#setVolume による個別調整と AudioAttributes でのストリーム分離。
  • 実装上の落とし穴:AudioAttributes の設定ミス、SoundPool のロードタイミング、Activity ライフサイクル管理。

これにより、ユーザーが直感的に BGM の音量だけを調整でき、アプリ全体のサウンド体験を向上させることができます。今後は、リアルタイムエフェクトのクロスフェード複数トラックの同期再生 といった発展的なテーマについても取り上げていく予定です。

参考資料