サイトロゴ

【Kotlin】SoundPoolでゲームの効果音を再生する【Androidアプリ開発】

著者画像
Toshihiko Arai

はじめに

今回のAndroidサンプルプロジェクトでは、SoundPoolを使ってサウンド再生をやってみた。図のように2つのボタンを設置して、クリックしたときに効果音が鳴るだけのシンプルなプログラムである。

サウンドファイルはres/rawへ配置する

図のようにresrawディレクトリを作り、そこに用意したサウンドファイルを配置する。コード内からファイルを参照するには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で管理する

SoundPoolloadメソッドで音声データをロードするとint型のsoundIDが返ってくる。このsoundIDplayメソッドで再生するときに必要になるのでメンバ変数に保存しておく。

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 objectsoundIDのメンバ変数を定義すると、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