AndroidのAudioTrackで繰り返し再生する

## はじめに
初投稿です。

Androidでメトロノームアプリを作りたいと思い、
AudioTrackで実装を進めていましたが、かなりハマったので備忘のために残します。

## 環境
Windows10
AndroidStudio
Kotlin
minSdkVersion:23
targetSdkVersion:28

## AudioTrack
MediaPlayer、SoundPool、AudioTrackがありますが、
繰り返し同じリズムを刻むにはAudioTrackが良いようでした。

## 初期化
AudioTrack.Builder()を使います。

・setUsage：USAGE_MEDIA=音楽など
・setContentType：CONTENT_TYPE_MUSIC=音楽など
　他にも音声やビープ音などもありますが、変更しても何も変わりませんでした。

・setEncoding：ENCODING_PCM_8BIT
・setSampleRate：44100
・setChannelMask：CHANNEL_OUT_MONO
　再生するデータに合わせます。

・setBufferSizeInBytes：バッファサイズ
　再生するデータに合わせます。
　MODE_STREAMの場合は余裕がないと途切れます。2～4倍くらい?

・setTransferMode：MODE_STREAMまたはMODE_STATIC
　MODE_STREAM→バッファにデータを追加して、消費していくイメージ。
　MODE_STATIC→最初にセットしたデータを使いまわす。


```kotlin:
var audioTrack: AudioTrack? = null

val SMPL = 44100
val bufSize = SMPL * 2
val mode = AudioTrack.MODE_STATIC

fun init() {
        audioTrack = AudioTrack.Builder()
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build()
            )
            .setAudioFormat(
                AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_8BIT)
                    .setSampleRate(SMPL)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build()
            )
            .setBufferSizeInBytes(bufSize)
            .setTransferMode(mode)
            .build()
}
```
## 主要メソッド
各メソッドのイメージがつかみにくかったので、
自分なりに解釈した内容です。
これが分かるまで、呼び出す順番が良くわかりませんでした。

音が出るタイミングは、直観的にはplayだと思い込んでいたのですが、
writeのタイミングでした。

ここでかなりハマりました。

|メソッド|目的|
|:-:|:-:|
|play|playStateを再生状態(PLAYSTATE_PLAYING)にする。|
|stop|playStateを停止状態(PLAYSTATE_STOPPED)にする。|
|write|データを再生バッファに書き込む。|
|flush|再生バッファのデータをクリアする。|
|release|AudioTrackオブジェクトを破棄する。|

## 音を再生する

呼び出し元ではinit→readyの後、playを繰り返します。

### MODE_STATICの場合

前処理でデータをセットして、
あとはreloadStaticData()で繰り返し再利用する感じです。

同じパターンを繰り返し再生するメトロノームは、
こちらが適しているようでした。

毎回stop()していますが、stop()なしでも連続再生できます。
今回の要件的に必要なので入れています。

ループ再生もできますが、
再生タイミングとUI操作を同期する要件があるので使用していません。

```kotlin:
    //前処理
    fun ready(arr: ByteArray) {
        //バッファにあらかじめデータをセットしておく
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)
    }

    //再生
    fun play(arr: ByteArray) {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            //再生バッファをクリアする
            audioTrack?.flush()
        }

        //読み込み済データを再度読み出す
        audioTrack?.reloadStaticData()
        //再生バッファにデータを書き込む
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)

        //再生中にする
        audioTrack?.play()
    }
```

### MODE_STREAMの場合

再生完了時にいったんstop()しないと音が出なくなります。

また、バッファの中身が枯渇すると以下のエラーが出て、1回分の再生がスキップされます。
「releaseBuffer() track %p name=%s disabled due to previous underrun, restarting」

前処理でバッファを埋めておくことで一応解決しましたが、
この方法で良いのかどうかわかりません。。

```kotlin:
    //前処理
    fun ready(arr: ByteArray) {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            audioTrack?.flush()
        }
           
        if (audioTrack != null) {
            //バッファを埋めておく
            val loopCount = bufSize / arr.count()
            for (i in 0 until loopCount) {
                audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)
            }
        }
    }

    //再生
    fun play(arr: ByteArray) {
        //再生バッファにデータを書き込む
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)

        //1回分の再生終了を検知して停止する
        audioTrack?.setPlaybackPositionUpdateListener(
            object : AudioTrack.OnPlaybackPositionUpdateListener {
                override fun onPeriodicNotification(track: AudioTrack){
                }
                override fun onMarkerReached(track: AudioTrack) {
                    //再生完了
                    if(track.playState == AudioTrack.PLAYSTATE_PLAYING){
                        //停止
                        track.stop()
                    }
                }
            }
        )

        //再生終了検知するためのNotificationをセット
        audioTrack?.setNotificationMarkerPosition(arr.count())

        //再生状態にする
        if (audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) {
            audioTrack?.play()
        }
    }
```

### 停止・終了(共通)

```kotlin:
    //停止
    fun stop() {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            //再生バッファをクリアする
            audioTrack?.flush()
        }
    }

    //終了時にオブジェクトを破棄する
    fun release() {
        try {
            this.stop()

            audioTrack?.release()
            audioTrack = null

            Logger.set(LogKind.INFO, "sound released")
        } catch (e: Exception) {
            Logger.set(LogKind.ERROR, e.toString())
        }
    }
```

## WRITE_BLOCKINGとWRITE_NON_BLOCKING

writeメソッドでWRITE_BLOCKINGとWRITE_NON_BLOCKINGが指定できます。

WRITE_BLOCKINGは前の再生が終わるまで次の再生を待ち、
WRITE_NON_BLOCKINGは前の再生が終わってなくても次の再生をする感じでした。

WRITE_NON_BLOCKINGにすると、繰り返す際に
最後の音と最初の音がかぶって再生されることがありました。

今回の要件ではBPMを保ち、一定の間隔で繰り返し再生する必要があるため、
WRITE_BLOCKINGで再生のたびに停止→再生することで開始のタイミングがずれないようにしています。

## おわりに
情報が少ない&古いものが多く、手探りでの実装となりましたので、
より良い方法をご存知の方は是非、教えていただけると幸いです。