7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-23

はじめに

初投稿です。

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→最初にセットしたデータを使いまわす。

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操作を同期する要件があるので使用していません。

    //前処理
    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」

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

    //前処理
    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()
        }
    }

停止・終了(共通)

    //停止
    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で再生のたびに停止→再生することで開始のタイミングがずれないようにしています。

おわりに

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

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?