SoundPoolを使ってゲームの効果音を再生する【Android/Kotlin】

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

今回のAndroidサンプルプロジェクトでは、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)
}

参考

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

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

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

参考

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

SoundPoolのバグ?

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

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

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

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

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

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

212K    ./ogg
2.3M    ./wav

Anrdoidアプリ開発にオススメの書籍

こちらの本は、図や絵が多くてわかりやすく、Androidアプリ開発初心者でも理解しやすい内容。基本的なことはしっかりおさえられるので、この手の本は一冊読んでおくと良と思う。

基本からしっかり身につくAndroidアプリ開発入門
基本からしっかり身につくAndroidアプリ開発入門

圧倒的な多数のユーザーが使っているヤフーのアプリ。その制作の最前線にいる黒帯エンジニアが、ユーザーが使いやすいアプリの大切な基本をしっかりと解説します。

KindleAmazon

Kotlinの実践的な使い方が学べる内容の本で、プログラミング技術をもっと磨きたい人向け。センスの良いプログラミングがたくさん紹介されていて、個人的には「目からうろこ」の連続だった本。

Kotlinプログラミング
Kotlinプログラミング

Kotlinは、Javaとの相互運用を可能にし、Android OSでGoogleがフルサポートする静的型プログラミング言語です。この言語は、Javaだけでは十全ではない(Javaだけでは実装に手間がかかりすぎる)、軽量かつ豊かな表現形式や、他言語ではすでに実装されている最新の機能を盛り込んでいます。

KindleAmazon

最後まで読んでいただきありがとうございました。

「この記事が参考になったよ」という方は、ぜひ記事をシェアをしていただけるととても嬉しいです。

今後も有益な記事を書くモチベーションにつながりますので、どうかよろしくお願いいたします。↓↓↓↓↓↓↓

あなたにおすすめ