ゲーム開発の備忘録

趣味のゲーム開発でのノウハウや、技術的に嵌ったポイントを忘れないように書き記しておくブログです。

libGDXでBGMの任意位置からのスムーズなループ再生を実現する

libGDXで用意されているBGMのループ処理

libGDXではBGM再生ももちろんサポートされています。
具体的には、任意の楽曲ファイルを指定してMusicインスタンスを生成します。
このMusicインスタンスを介して、楽曲の再生や停止、音量調整を行います。

BGMのループ処理も、MusicのisLoopingをtrueにすることで実現可能です。
ただし、このループ再生は楽曲終端から楽曲先頭までのループしか実行できません。
つまり、楽曲にイントロがある場合は、イントロが終了した時点からのループ再生を行いたいものですが、それが実現できないのです。

シークによる任意位置からの再生

こうなったら、手動でループ処理を書くしかありません。
幸い、Musicにはpositionという、現在の再生位置を保持するフィールドがあるため、
その値を変更することでBGMのシークが可能です。

ただし、このpositionの扱いには注意が必要です!!
なんと、BGMを一度stopした後に、再度playするとpositionの値を変更しても楽曲の再生位置に反映されなくなるのです。
(おそらく、libGDX側のバグだと思われます)
これを回避するには、楽曲をstopしたら、一度dispose, unloadしてメモリから削除し、再度loadを実行しなければなりません。
因みに、上記の事象はAssetManagerを使用していてもいなくても発生しました。

setOnCompletionListenerによる楽曲終端の検知

isLoopingを利用すると否が応にも楽曲先頭からの再生になるため、
別の手段で再生位置が楽曲終端に到達したことを検知する必要があります。
ここで、libGDXのJavaDocを確認するとMusicクラスのメソッドに、
setOnCompletionListenerというイベントリスナがあるのを発見できます。
実際にこのイベントリスナを通じて、楽曲の再生位置が終端に到達したことを検知できます。

しかし、このイベントリスナにも欠点があります。
イベントリスナが発火する時点では既にBGMの再生は終了しているので、
イベントリスナ内でpositionの値を変更してから再度BGMを再生しなくてはならないのですが、
BGMを再生し直す際にラグが発生してしまうのです。
結果、ループするたびに音がブツッと切れて違和感が凄まじいです。

スムーズなループ再生の実現

結局、自分でBGMの再生状況を監視するスレッドを立てて、
それを用いてループ再生を実現することにしました。
コードは以下の通りです。(Kotlinで書いています)

class BGMObserveRunner(private val bgm :Music, private val loop_position :Float,
        private val finish_position :Float, private val fix_space :Float) :Runnable{

        override fun run() {
            try {
                while (!Thread.currentThread().isInterrupted && bgm.isPlaying) {
                    if (bgm.position >= finish_position - fix_space) {
                        bgm.position = loop_position
                    }
                }
            }catch(ex :IllegalStateException){
                return
            }
        }
    }

ここで注意すべき点は、BGMの終端ぴったりでの検知にすると、
スレッド内で再生位置を取得した時点では既にBGMの再生が終了していたなんてことが起こります。
そこで、楽曲の末尾に数秒の空白を挿入しておき、その空白の時間を差し引いた値をfinish_positionとして指定しています。

実際には、Android対応する場合はAndroid端末によって動作状況(スレッドの回転状況)が異なってくるため、楽曲をループ再生しても違和感が生じにくい0.15秒ほどの余裕をもってfinish_positionを指定しています。
つまり、

finish_position = 楽曲ファイルの終端の時間 - 楽曲末尾に挿入した空白の時間 - Android端末ごとの動作差異の吸収時間

という風に指定しています。
性能が低い端末だと再生時間のチェックを余裕ですり抜けて、BGMの再生が終了してしまう場合もあるため、
前述したsetOnCompletionListenerによる楽曲終端の検知も保険として残してあります。


より良い方法がありましたら教えていただけると助かります!