playback: refactor audio focus

Move all replaygain functionality into AudioReactor, alongside moving
all volume management into the class. This allows volume state to stay
sane throughout.
This commit is contained in:
OxygenCobalt 2022-01-06 12:20:25 -07:00
parent 357184dd8d
commit 8ad44d898c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 146 additions and 141 deletions

View file

@ -20,36 +20,142 @@ package org.oxycblt.auxio.playback.system
import android.content.Context import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import androidx.core.math.MathUtils
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat import androidx.media.AudioManagerCompat
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.flac.VorbisComment
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import kotlin.math.pow
/** /**
* Object that manages the AudioFocus state. * Object that manages the AudioFocus state.
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe) * Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AudioReactor( class AudioReactor(context: Context) : AudioManager.OnAudioFocusChangeListener {
context: Context, private data class Gain(val track: Float, val album: Float)
private val player: ExoPlayer
) : AudioManager.OnAudioFocusChangeListener {
private val playbackManager = PlaybackStateManager.maybeGetInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val audioManager = context.getSystemServiceSafe(AudioManager::class) private val audioManager = context.getSystemServiceSafe(AudioManager::class)
private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setWillPauseWhenDucked(true) .setWillPauseWhenDucked(false)
.setAudioAttributes(
AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.build()
)
.setOnAudioFocusChangeListener(this) .setOnAudioFocusChangeListener(this)
.build() .build()
private var previousVolume = player.volume private var multiplier = 1f
private var pauseWasTransient = false private var pauseWasTransient = false
var volume = 0f
get() = field * multiplier
private set
/**
* Updates the rough volume adjustment for [Metadata] with ReplayGain tags.
* This is based off Vanilla Music's implementation.
*/
fun applyReplayGain(metadata: Metadata?) {
if (metadata == null) {
logD("No parsable ReplayGain tags, returning volume to 1.")
volume = 1f
return
}
val gain = parseReplayGain(metadata)
// Currently we consider both the album and the track gain. One might want to add
// configuration to handle more cases.
var adjust = 0f
if (gain != null) {
adjust = if (gain.album != 0f) {
gain.album
} else {
gain.track
}
}
// Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
volume = MathUtils.clamp((10f.pow((adjust / 20f))), 0f, 1f)
logD("Applied ReplayGain adjustment: $volume")
}
private fun parseReplayGain(metadata: Metadata): Gain? {
data class GainTag(val key: String, val value: Float)
var trackGain = 0f
var albumGain = 0f
var found = false
val tags = mutableListOf<GainTag>()
for (i in 0 until metadata.length()) {
val entry = metadata.get(i)
// Sometimes the ReplayGain keys will be lowercase, so make them uppercase.
if (entry is TextInformationFrame && entry.description?.uppercase() in replayGainTags) {
tags.add(GainTag(entry.description!!.uppercase(), parseReplayGainFloat(entry.value)))
continue
}
if (entry is VorbisComment && entry.key.uppercase() in replayGainTags) {
tags.add(GainTag(entry.key.uppercase(), parseReplayGainFloat(entry.value)))
}
}
// Case 1: Normal ReplayGain, most commonly found on MPEG files.
tags.findLast { tag -> tag.key == RG_TRACK }?.let { tag ->
trackGain = tag.value
found = true
}
tags.findLast { tag -> tag.key == RG_ALBUM }?.let { tag ->
albumGain = tag.value
found = true
}
// Case 2: R128 ReplayGain, most commonly found on FLAC files.
// While technically there is the R128 base gain in Opus files, ExoPlayer doesn't
// have metadata parsing functionality for those, so we just ignore it.
tags.findLast { tag -> tag.key == R128_TRACK }?.let { tag ->
trackGain += tag.value / 256f
found = true
}
tags.findLast { tag -> tag.key == R128_ALBUM }?.let { tag ->
albumGain += tag.value / 256f
found = true
}
return if (found) {
Gain(trackGain, albumGain)
} else {
null
}
}
private fun parseReplayGainFloat(raw: String): Float {
return try {
raw.replace(Regex("[^0-9.-]"), "").toFloat()
} catch (e: Exception) {
0f
}
}
/** /**
* Request the android system for audio focus * Request the android system for audio focus
*/ */
@ -66,7 +172,7 @@ class AudioReactor(
override fun onAudioFocusChange(focusChange: Int) { override fun onAudioFocusChange(focusChange: Int) {
if (!settingsManager.doAudioFocus) { if (!settingsManager.doAudioFocus) {
// Dont do audio focus if its not enabled // Don't do audio focus if its not enabled
return return
} }
@ -79,7 +185,7 @@ class AudioReactor(
} }
private fun onGain() { private fun onGain() {
if (player.volume == VOLUME_DUCK) { if (multiplier == MULTIPLIER_DUCK) {
unduck() unduck()
} else if (pauseWasTransient) { } else if (pauseWasTransient) {
logD("Gained focus after transient loss") logD("Gained focus after transient loss")
@ -94,7 +200,6 @@ class AudioReactor(
// Since this loss is only temporary, mark it as such if we had to pause playback. // Since this loss is only temporary, mark it as such if we had to pause playback.
if (playbackManager.isPlaying) { if (playbackManager.isPlaying) {
logD("Pausing for transient loss") logD("Pausing for transient loss")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
pauseWasTransient = true pauseWasTransient = true
} }
@ -102,22 +207,32 @@ class AudioReactor(
private fun onLossPermanent() { private fun onLossPermanent() {
logD("Pausing for permanent loss") logD("Pausing for permanent loss")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
} }
private fun onDuck() { private fun onDuck() {
previousVolume = player.volume multiplier = MULTIPLIER_DUCK
player.volume = VOLUME_DUCK logD("Ducked volume, now $volume")
logD("Ducked volume to ${player.volume} [previous: $previousVolume]")
} }
private fun unduck() { private fun unduck() {
player.volume = previousVolume multiplier = 1f
logD("Unducked volume to ${player.volume}") logD("Unducked volume, now $volume")
} }
companion object { companion object {
private const val VOLUME_DUCK = 0.2f private const val MULTIPLIER_DUCK = 0.2f
const val RG_TRACK = "REPLAYGAIN_TRACK_GAIN"
const val RG_ALBUM = "REPLAYGAIN_ALBUM_GAIN"
const val R128_TRACK = "R128_TRACK_GAIN"
const val R128_ALBUM = "R128_ALBUM_GAIN"
val replayGainTags = arrayOf(
RG_TRACK,
RG_ALBUM,
R128_ALBUM,
R128_TRACK
)
} }
} }

View file

@ -129,7 +129,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// --- SYSTEM SETUP --- // --- SYSTEM SETUP ---
audioReactor = AudioReactor(this, player) audioReactor = AudioReactor(this)
widgets = WidgetController(this) widgets = WidgetController(this)
// Set up the media button callbacks // Set up the media button callbacks
@ -210,7 +210,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
when (state) { when (state) {
Player.STATE_READY -> startPollingPosition() Player.STATE_READY -> startPolling()
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
if (playbackManager.loopMode == LoopMode.TRACK) { if (playbackManager.loopMode == LoopMode.TRACK) {
@ -247,9 +247,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
val metadata = info.trackGroup.getFormat(i).metadata val metadata = info.trackGroup.getFormat(i).metadata
if (metadata != null) { if (metadata != null) {
audioReactor.applyReplayGain(metadata)
consumed = true consumed = true
player.volume = calculateReplayGain(metadata)
logD("Applied ReplayGain adjustment: ${player.volume}")
} }
break break
@ -262,8 +261,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
if (!consumed) { if (!consumed) {
// Sadly we couldn't parse any ReplayGain tags. Revert to normal volume. // Sadly we couldn't parse any ReplayGain tags. Revert to normal volume.
player.volume = 1f audioReactor.applyReplayGain(null)
logD("No parsable ReplayGain tags, returning volume to 1.")
} }
} }
@ -294,7 +292,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
if (isPlaying && !player.isPlaying) { if (isPlaying && !player.isPlaying) {
player.play() player.play()
audioReactor.requestFocus() audioReactor.requestFocus()
startPollingPosition() startPolling()
} else { } else {
player.pause() player.pause()
} }
@ -400,17 +398,21 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
/** /**
* Start polling the position on a coroutine. * Start polling the position on a coroutine.
*/ */
private fun startPollingPosition() { private fun startPolling() {
data class Poll(val pos: Long, val multiplier: Float)
val pollFlow = flow { val pollFlow = flow {
while (true) { while (true) {
emit(player.currentPosition) emit(Poll(player.currentPosition, audioReactor.volume))
delay(POS_POLL_INTERVAL) delay(POS_POLL_INTERVAL)
} }
}.conflate() }.conflate()
serviceScope.launch { serviceScope.launch {
pollFlow.takeWhile { player.isPlaying }.collect { pos -> pollFlow.takeWhile { player.isPlaying }.collect { poll ->
playbackManager.setPosition(pos) playbackManager.setPosition(poll.pos)
player.volume = audioReactor.volume
logD(player.volume)
} }
} }
} }
@ -542,10 +544,5 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
const val ACTION_PLAY_PAUSE = BuildConfig.APPLICATION_ID + ".action.PLAY_PAUSE" const val ACTION_PLAY_PAUSE = BuildConfig.APPLICATION_ID + ".action.PLAY_PAUSE"
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT" const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT" const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
const val RG_TRACK = "REPLAYGAIN_TRACK_GAIN"
const val RG_ALBUM = "REPLAYGAIN_ALBUM_GAIN"
const val R128_TRACK = "R128_TRACK_GAIN"
const val R128_ALBUM = "R128_ALBUM_GAIN"
} }
} }

View file

@ -1,107 +0,0 @@
package org.oxycblt.auxio.playback.system
import androidx.core.math.MathUtils
import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.flac.VorbisComment
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import kotlin.math.pow
const val RG_TRACK = "REPLAYGAIN_TRACK_GAIN"
const val RG_ALBUM = "REPLAYGAIN_ALBUM_GAIN"
const val R128_TRACK = "R128_TRACK_GAIN"
const val R128_ALBUM = "R128_ALBUM_GAIN"
val replayGainTags = arrayOf(
RG_TRACK,
RG_ALBUM,
R128_ALBUM,
R128_TRACK
)
data class Gain(val track: Float, val album: Float)
/**
* Calculates the rough volume adjustment for [Metadata] with ReplayGain tags.
* This is based off Vanilla Music's implementation.
*/
fun calculateReplayGain(metadata: Metadata): Float {
val gain = parseReplayGain(metadata)
// Currently we consider both the album and the track gain. One might want to add
// configuration to handle more cases.
var adjust = 0f
if (gain != null) {
adjust = if (gain.album != 0f) {
gain.album
} else {
gain.track
}
}
// Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
return MathUtils.clamp((10f.pow((adjust / 20f))), 0f, 1f)
}
private fun parseReplayGain(metadata: Metadata): Gain? {
data class GainTag(val key: String, val value: Float)
var trackGain = 0f
var albumGain = 0f
var found = false
val tags = mutableListOf<GainTag>()
for (i in 0 until metadata.length()) {
val entry = metadata.get(i)
// Sometimes the ReplayGain keys will be lowercase, so make them uppercase.
if (entry is TextInformationFrame && entry.description?.uppercase() in replayGainTags) {
tags.add(GainTag(entry.description!!.uppercase(), parseReplayGainFloat(entry.value)))
continue
}
if (entry is VorbisComment && entry.key.uppercase() in replayGainTags) {
tags.add(GainTag(entry.key.uppercase(), parseReplayGainFloat(entry.value)))
}
}
// Case 1: Normal ReplayGain, most commonly found on MPEG files.
tags.findLast { tag -> tag.key == RG_TRACK }?.let { tag ->
trackGain = tag.value
found = true
}
tags.findLast { tag -> tag.key == RG_ALBUM }?.let { tag ->
albumGain = tag.value
found = true
}
// Case 2: R128 ReplayGain, most commonly found on FLAC files.
// While technically there is the R128 base gain in Opus files, ExoPlayer doesn't
// have metadata parsing functionality for those, so we just ignore it.
tags.findLast { tag -> tag.key == R128_TRACK }?.let { tag ->
trackGain += tag.value / 256f
found = true
}
tags.findLast { tag -> tag.key == R128_ALBUM }?.let { tag ->
albumGain += tag.value / 256f
found = true
}
return if (found) {
Gain(trackGain, albumGain)
} else {
null
}
}
private fun parseReplayGainFloat(raw: String): Float {
return try {
raw.replace(Regex("[^0-9.-]"), "").toFloat()
} catch (e: Exception) {
0f
}
}