diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt index b1c3ce66d..e5a3c239f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt @@ -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() + + 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 + ) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 63fd9cd2c..a1f4ce9c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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" } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGain.kt deleted file mode 100644 index 488d5c022..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGain.kt +++ /dev/null @@ -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() - - 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 - } -}