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.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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