【Kotlin】SoundPoolでゲームの効果音を再生する【Androidアプリ開発】
はじめに
今回のAndroidサンプルプロジェクトでは、SoundPoolを使ってサウンド再生をやってみた。図のように2つのボタンを設置して、クリックしたときに効果音が鳴るだけのシンプルなプログラムである。
サウンドファイルはres/rawへ配置する

図のようにres
にraw
ディレクトリを作り、そこに用意したサウンドファイルを配置する。コード内からファイルを参照するにはR.raw.drumroll
の形でアクセスが可能だ。
参考
https://developer.android.com/guide/topics/resources/providing-resources.html
SoundPoolをシングルトンで管理する
複数Activityをまたがった時にサウンドが途切れないようにしたいので、次のようにシングルトンで実装してみた。
companion object {
var SOUND_DRUMROLL = 0
var SAD_TROMBONE = 0
var INSTANCE:Sound? = null
fun getInstance(context: Context) =
INSTANCE ?: Sound(context).also {
INSTANCE = it
}
}
LOLLIPOP以降はBuilderで生成する
LOLLIPOP以前と以降ではSoundPool
の生成方法が違うので注意が必要だ。LOLLIPOP以降ではコンストラクタが使えなくなっておりBuilder
で生成することになっている。次のようにSDKのバージョンを比較して条件分岐することになる。
private fun createSoundPool() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createNewSoundPool()
} else {
createOldSoundPool()
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun createNewSoundPool() {
val attributes = AudioAttributes.Builder().apply {
setUsage(AudioAttributes.USAGE_GAME)
setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
}.build()
soundPool = SoundPool.Builder().apply {
setMaxStreams(2)
setAudioAttributes(attributes)
}.build()
}
private fun createOldSoundPool() {
soundPool = SoundPool(2, AudioManager.STREAM_MUSIC, 0)
}
参考
https://stackoverflow.com/questions/17069955/play-sound-using-soundpool-example
同時に複数のサウンドを鳴らしたい
maxStreams
に1以上の数を指定してあげることで、その数分だけ同時再生が可能になるmaxStreams
を大きくすればそれだけ処理に負荷がかかるので、必要以上に値を大きくすべきではないだろう。
soundPool = SoundPool.Builder().apply {
setMaxStreams(2)
setAudioAttributes(attributes)
}.build()
参考
https://stackoverflow.com/questions/41127386/playing-multiple-soundpool-at-the-same-time
音楽ファイルはsoundIDで管理する
SoundPool
のload
メソッドで音声データをロードするとint
型のsoundID
が返ってくる。このsoundID
はplay
メソッドで再生するときに必要になるのでメンバ変数に保存しておく。
private fun loadSoundIDs(context:Context) {
soundPool?.let {
println("サウンドファイルロード")
SOUND_DRUMROLL = it.load(context, R.raw.drumroll, 1)
SAD_TROMBONE = it.load(context, R.raw.sad_trombone, 1)
}
}
companion object
にsoundID
のメンバ変数を定義すると、Activity
から次の形で呼び出せるので便利だ。
Sound.getInstance(this).playSound(Sound.SOUND_DRUMROLL)
Soundクラスの全容
以上の内容で作ったSound
クラスの全容を載せておく。
class Sound constructor(context:Context) {
private var soundPool: SoundPool? = null
companion object {
var SOUND_DRUMROLL = 0
var SAD_TROMBONE = 0
var INSTANCE:Sound? = null
fun getInstance(context: Context) =
INSTANCE ?: Sound(context).also {
INSTANCE = it
}
}
init {
createSoundPool()
loadSoundIDs(context)
}
private fun createSoundPool() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createNewSoundPool()
} else {
createOldSoundPool()
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun createNewSoundPool() {
val attributes = AudioAttributes.Builder().apply {
setUsage(AudioAttributes.USAGE_GAME)
setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
}.build()
soundPool = SoundPool.Builder().apply {
setMaxStreams(2)
setAudioAttributes(attributes)
}.build()
}
private fun createOldSoundPool() {
soundPool = SoundPool(2, AudioManager.STREAM_MUSIC, 0)
}
private fun loadSoundIDs(context:Context) {
soundPool?.let {
println("サウンドファイルロード")
SOUND_DRUMROLL = it.load(context, R.raw.drumroll, 1)
SAD_TROMBONE = it.load(context, R.raw.sad_trombone, 1)
}
}
fun playSound(soundID:Int) {
soundPool?.let{
it.play(soundID, 1.0f, 1.0f, 1, 0, 1.0f)
println("サウンド再生")
}
}
fun close() { // シングルトンの場合呼びようがない?
soundPool?.release()
soundPool = null
}
}
MainActivityからサウンド再生する
最後にMainActivity
からSound
クラスのインスタンスを生成してサウンドを鳴らしてみよう。
class MainActivity : AppCompatActivity() {
private lateinit var sound:Sound
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初回のみ再生出来ないのでgetInstanceに触れて初期化しておく
Sound.getInstance(this)
findViewById<Button>(R.id.sound1).setOnClickListener {
Sound.getInstance(this).playSound(Sound.SOUND_DRUMROLL)
}
findViewById<Button>(R.id.sound2).setOnClickListener {
Sound.getInstance(this).playSound(Sound.SAD_TROMBONE)
}
}
}
SoundPool
生成のタイミングの問題で、初回のみ再生出来ない現象があるのでSound.getInstance(this)
に一度触れて事前に初期化している。この問題はこちらでも議論になっているので参考に。
https://stackoverflow.com/questions/8458498/why-does-my-soundpool-sound-not-play-the-first-time-on-android
SoundPoolのバグ?
SoundPoolを鳴らしていると、そのうち鳴らなくなったりする現象を発見した。その時コンソールには次のエラーが表示された。
E/AudioTrack: AudioFlinger could not create track, status: -12
E/SoundPool: Error creating AudioTrack
ググってみるとこちらの記事が該当するようだ。
https://stackoverflow.com/questions/9599059/soundpool-error-creating-audiotrack/9724138#9724138
記事とは違って、シングルトンの場合どうしたら良いかわからない。一定時間または一定回数鳴らしたら強制的にインスタンスを再生成するとか?
AudioTrackはシングルトンにした方が安定する。ちなみに、wavからoggファイルへ変換したところSoundPoolの不安定さがなくなった。wavからoggへ変換するにはffmpegで可能。
10個分のwavとoggファイルの合計サイズを比較してみた。oggはwavの1/10程度まで圧縮されている。効果音に使う分には音質は気にならない。oggがなかなか良さげだ。
212K ./ogg
2.3M ./wav