SoundPoolを使ってゲームの効果音を再生する (Android Kotlin)




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



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




図のようにresrawディレクトリを作り、そこに用意したサウンドファイルを配置する。
コード内からファイルを参照するにはR.raw.drumrollのような形でアクセスが可能だ。

参考: リソースの提供



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)
}


参考: Play sound using soundpool example



同時に複数のサウンドを鳴らしたい


maxStreamsに1以上の数を指定してあげることで、その数分だけ同時再生が可能になる。maxStreamsを大きくすればそれだけ処理に負荷がかかるので、必要以上に値を大きくすべきではないだろう。

soundPool = SoundPool.Builder().apply {
	setMaxStreams(2)
	setAudioAttributes(attributes)
}.build()


参考: 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)に一度触れて事前に初期化している。

Sound.getInstance(this)


この問題はこちらでも議論になっているので参考に。

Why does my SoundPool sound not play the first time on Android?