はじめに
初投稿です。
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で再生のたびに停止→再生することで開始のタイミングがずれないようにしています。
おわりに
情報が少ない&古いものが多く、手探りでの実装となりましたので、
より良い方法をご存知の方は是非、教えていただけると幸いです。