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

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

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

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

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

参考

アプリリソースの概要 - Androidデベロッパードキュメントガイド

SoundPoolをシングルトンで管理する

複数Activityをまたがった時にサウンドが途切れないようにしたいので、次のようにシングルトンで実装してみた。

kotlin
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のバージョンを比較して条件分岐することになる。

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

参考

android-Play sound using sound pool example-StackOverflow

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

maxStreamsに1以上の数を指定してあげることで、その数分だけ同時再生が可能になるmaxStreamsを大きくすればそれだけ処理に負荷がかかるので、必要以上に値を大きくすべきではないだろう。
kotlin
soundPool = SoundPool.Builder().apply {
	setMaxStreams(2)
	setAudioAttributes(attributes)
}.build()

参考

android-Playing multiple Sound Pool at the same time-StackOverflow

音楽ファイルはsoundIDで管理する

SoundPoolloadメソッドで音声データをロードするとint型のsoundIDが返ってくる。このsoundIDplayメソッドで再生するときに必要になるのでメンバ変数に保存しておく。
kotlin
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から次の形で呼び出せるので便利だ。
kotlin
Sound.getInstance(this).playSound(Sound.SOUND_DRUMROLL)

Soundクラスの全容

以上の内容で作ったSoundクラスの全容を載せておく。

kotlin
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クラスのインスタンスを生成してサウンドを鳴らしてみよう。

kotlin
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)に一度触れて事前に初期化している。この問題はこちらでも議論になっているので参考に。

Why does my Sound Pool sound not play the first time on Android?-StackOverflow

SoundPoolのバグ?

SoundPoolを鳴らしていると、そのうち鳴らなくなったりする現象を発見した。その時コンソールには次のエラーが表示された。

E/AudioTrack: AudioFlinger could not create track, status: -12
E/SoundPool: Error creating AudioTrack

ググってみるとこちらの記事が該当するようだ。

android - Audio Flinger could not create track, status: -12; Error creating AudioTrack - Stack Overflow

記事とは違って、シングルトンの場合どうしたら良いかわからない。一定時間または一定回数鳴らしたら強制的にインスタンスを再生成するとか?

AudioTrackはシングルトンにした方が安定する。ちなみに、wavからoggファイルへ変換したところSoundPoolの不安定さがなくなった。wavからoggへ変換するにはffmpegで可能。

10個分のwavとoggファイルの合計サイズを比較してみた。oggはwavの1/10程度まで圧縮されている。効果音に使う分には音質は気にならない。oggがなかなか良さげだ。

212K    ./ogg
2.3M    ./wav

▼ こんな記事も書いてます。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事