diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt index 7c45d96ce..0c5c668e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt @@ -44,8 +44,6 @@ import kotlin.math.min * of state and view magic. I tried my best to document it, but it's probably not the most friendly * or extendable. You have been warned. * - * TODO: Add the queue view into this layout. - * * @author OxygenCobalt (With help from Umano and Hai Zhang) */ class PlaybackLayout @JvmOverloads constructor( 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 218a0c41f..2710b7db3 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 @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.RenderersFactory +import com.google.android.exoplayer2.TracksInfo import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory @@ -234,6 +235,29 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac } } + override fun onTracksInfoChanged(tracksInfo: TracksInfo) { + super.onTracksInfoChanged(tracksInfo) + + for (info in tracksInfo.trackGroupInfos) { + if (info.isSelected) { + for (i in 0 until info.trackGroup.length) { + if (info.isTrackSelected(i)) { + val metadata = info.trackGroup.getFormat(i).metadata + + if (metadata != null) { + player.volume = calculateReplayGain(metadata) + logD("Applied ReplayGain adjustment: ${player.volume}") + } + + break + } + } + + break + } + } + } + // --- PLAYBACK STATE CALLBACK OVERRIDES --- override fun onSongUpdate(song: Song?) { @@ -501,7 +525,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac companion object { private const val DISCONNECTED = 0 private const val CONNECTED = 1 - private const val WAKELOCK_TIME = 25000L private const val POS_POLL_INTERVAL = 500L const val ACTION_LOOP = BuildConfig.APPLICATION_ID + ".action.LOOP" @@ -510,5 +533,10 @@ 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 new file mode 100644 index 000000000..488d5c022 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGain.kt @@ -0,0 +1,107 @@ +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 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt index ab3ffda01..db720180d 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -59,7 +59,7 @@ private fun Any.getName(): String = "Auxio.${this::class.simpleName ?: "Anonymou * I know that this will not stop you, but consider what you are doing with your life, copycats. * Do you want to live a fulfilling existence on this planet? Or do you want to spend your life * taking work others did and making it objectively worse so you could arbitrage a fraction of a - * penny on every AdMob impression you get. You could do so many great things if you simply had + * penny on every AdMob impression you get? You could do so many great things if you simply had * the courage to come up with an idea of your own. Be better. */ private fun basedCopyleftNotice() {