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:
parent
357184dd8d
commit
8ad44d898c
3 changed files with 146 additions and 141 deletions
|
@ -20,36 +20,142 @@ package org.oxycblt.auxio.playback.system
|
|||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
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.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Object that manages the AudioFocus state.
|
||||
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AudioReactor(
|
||||
context: Context,
|
||||
private val player: ExoPlayer
|
||||
) : AudioManager.OnAudioFocusChangeListener {
|
||||
class AudioReactor(context: Context) : AudioManager.OnAudioFocusChangeListener {
|
||||
private data class Gain(val track: Float, val album: Float)
|
||||
|
||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
||||
|
||||
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)
|
||||
.build()
|
||||
|
||||
private var previousVolume = player.volume
|
||||
|
||||
private var multiplier = 1f
|
||||
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
|
||||
*/
|
||||
|
@ -66,7 +172,7 @@ class AudioReactor(
|
|||
|
||||
override fun onAudioFocusChange(focusChange: Int) {
|
||||
if (!settingsManager.doAudioFocus) {
|
||||
// Dont do audio focus if its not enabled
|
||||
// Don't do audio focus if its not enabled
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -79,7 +185,7 @@ class AudioReactor(
|
|||
}
|
||||
|
||||
private fun onGain() {
|
||||
if (player.volume == VOLUME_DUCK) {
|
||||
if (multiplier == MULTIPLIER_DUCK) {
|
||||
unduck()
|
||||
} else if (pauseWasTransient) {
|
||||
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.
|
||||
if (playbackManager.isPlaying) {
|
||||
logD("Pausing for transient loss")
|
||||
|
||||
playbackManager.setPlaying(false)
|
||||
pauseWasTransient = true
|
||||
}
|
||||
|
@ -102,22 +207,32 @@ class AudioReactor(
|
|||
|
||||
private fun onLossPermanent() {
|
||||
logD("Pausing for permanent loss")
|
||||
|
||||
playbackManager.setPlaying(false)
|
||||
}
|
||||
|
||||
private fun onDuck() {
|
||||
previousVolume = player.volume
|
||||
player.volume = VOLUME_DUCK
|
||||
logD("Ducked volume to ${player.volume} [previous: $previousVolume]")
|
||||
multiplier = MULTIPLIER_DUCK
|
||||
logD("Ducked volume, now $volume")
|
||||
}
|
||||
|
||||
private fun unduck() {
|
||||
player.volume = previousVolume
|
||||
logD("Unducked volume to ${player.volume}")
|
||||
multiplier = 1f
|
||||
logD("Unducked volume, now $volume")
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
// --- SYSTEM SETUP ---
|
||||
|
||||
audioReactor = AudioReactor(this, player)
|
||||
audioReactor = AudioReactor(this)
|
||||
widgets = WidgetController(this)
|
||||
|
||||
// Set up the media button callbacks
|
||||
|
@ -210,7 +210,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
when (state) {
|
||||
Player.STATE_READY -> startPollingPosition()
|
||||
Player.STATE_READY -> startPolling()
|
||||
|
||||
Player.STATE_ENDED -> {
|
||||
if (playbackManager.loopMode == LoopMode.TRACK) {
|
||||
|
@ -247,9 +247,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
val metadata = info.trackGroup.getFormat(i).metadata
|
||||
|
||||
if (metadata != null) {
|
||||
audioReactor.applyReplayGain(metadata)
|
||||
consumed = true
|
||||
player.volume = calculateReplayGain(metadata)
|
||||
logD("Applied ReplayGain adjustment: ${player.volume}")
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -262,8 +261,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
if (!consumed) {
|
||||
// Sadly we couldn't parse any ReplayGain tags. Revert to normal volume.
|
||||
player.volume = 1f
|
||||
logD("No parsable ReplayGain tags, returning volume to 1.")
|
||||
audioReactor.applyReplayGain(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,7 +292,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
if (isPlaying && !player.isPlaying) {
|
||||
player.play()
|
||||
audioReactor.requestFocus()
|
||||
startPollingPosition()
|
||||
startPolling()
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
|
@ -400,17 +398,21 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
/**
|
||||
* 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 {
|
||||
while (true) {
|
||||
emit(player.currentPosition)
|
||||
emit(Poll(player.currentPosition, audioReactor.volume))
|
||||
delay(POS_POLL_INTERVAL)
|
||||
}
|
||||
}.conflate()
|
||||
|
||||
serviceScope.launch {
|
||||
pollFlow.takeWhile { player.isPlaying }.collect { pos ->
|
||||
playbackManager.setPosition(pos)
|
||||
pollFlow.takeWhile { player.isPlaying }.collect { poll ->
|
||||
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_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue