diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 6675e8967..76f7cf95d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -93,16 +93,16 @@ sealed class Tab(open val mode: MusicMode) { /** * Convert a [Tab] integer representation into it's corresponding array of [Tab]s. - * @param sequence The integer representation of the [Tab]s. + * @param intCode The integer representation of the [Tab]s. * @return An array of [Tab]s corresponding to the sequence. */ - fun fromIntCode(sequence: Int): Array? { + fun fromIntCode(intCode: Int): Array? { val tabs = mutableListOf() // Try to parse a mode for each chunk in the sequence. // If we can't parse one, just skip it. for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) { - val chunk = sequence.shr(shift) and 0b1111 + val chunk = intCode.shr(shift) and 0b1111 val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt index 35354c51a..d1a656f4c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt @@ -48,7 +48,7 @@ enum class CoverMode { * Convert a [CoverMode] integer representation into an instance. * @param intCode An integer representation of a [CoverMode] * @return The corresponding [CoverMode], or null if the [CoverMode] is invalid. - * @see intCode + * @see CoverMode.intCode */ fun fromIntCode(intCode: Int) = when (intCode) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt index d2c3802a5..42a86f25a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt @@ -51,7 +51,7 @@ enum class MusicMode { * Convert a [MusicMode] integer representation into an instance. * @param intCode An integer representation of a [MusicMode] * @return The corresponding [MusicMode], or null if the [MusicMode] is invalid. - * @see intCode + * @see MusicMode.intCode */ fun fromIntCode(intCode: Int) = when (intCode) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 18861025d..3c08e6b47 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context /** - * The dialog for customizing the ReplayGain pre-amp values. + * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. * @author Alexander Capehart (OxygenCobalt) */ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt index df8a1694e..def0cce19 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt @@ -19,34 +19,41 @@ package org.oxycblt.auxio.playback.replaygain import org.oxycblt.auxio.IntegerTable -/** Represents the current setting for ReplayGain. */ +/** + * The current ReplayGain configuration. + * @author Alexander Capehart (OxygenCobalt) + */ enum class ReplayGainMode { /** Apply the track gain, falling back to the album gain if the track gain is not found. */ TRACK, - /** Apply the album gain, falling back to the track gain if the album gain is not found. */ ALBUM, - /** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */ DYNAMIC; companion object { - /** Convert an int [code] into an instance, or null if it isn't valid. */ - fun fromIntCode(code: Int): ReplayGainMode? { - return when (code) { + /** + * Convert a [ReplayGainMode] integer representation into an instance. + * @param intCode An integer representation of a [ReplayGainMode] + * @return The corresponding [ReplayGainMode], or null if the [ReplayGainMode] is invalid. + */ + fun fromIntCode(intCode: Int) = + when (intCode) { IntegerTable.REPLAY_GAIN_MODE_TRACK -> TRACK IntegerTable.REPLAY_GAIN_MODE_ALBUM -> ALBUM IntegerTable.REPLAY_GAIN_MODE_DYNAMIC -> DYNAMIC else -> null } - } } } -/** Represents the ReplayGain pre-amp values. */ +/** + * The current ReplayGain pre-amp configuration. + * @param with The pre-amp (in dB) to use when ReplayGain tags are present. + * @param without The pre-amp (in dB) to use when ReplayGain tags are not present. + * @author Alexander Capehart (OxygenCobalt) + */ data class ReplayGainPreAmp( - /** The value to use when ReplayGain tags are present. */ val with: Float, - /** The value to use when ReplayGain tags are not present. */ val without: Float ) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 07f9b45c5..fe16134ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -38,17 +38,11 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * Instead of leveraging the volume attribute like other implementations, this system manipulates * the bitstream itself to modify the volume, which allows the use of positive ReplayGain values. * - * Note that you must still give it a [Metadata] instance for it to function, which should be done - * when the active track changes. + * Note: This instance must be updated with a new [Metadata] every time the active track chamges. * * @author Alexander Capehart (OxygenCobalt) - * - * TODO: Convert to a low-level audio processor capable of handling any kind of PCM data. */ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { - private data class Gain(val track: Float, val album: Float) - private data class GainTag(val key: String, val value: Float) - private val playbackManager = PlaybackStateManager.getInstance() private val settings = Settings(context) @@ -62,10 +56,12 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { // --- REPLAYGAIN PARSING --- /** - * Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is tangentially - * based off Vanilla Music's implementation, but has diverged to a significant extent. + * Updates the volume adjustment based on the given [Metadata]. + * @param metadata The [Metadata] of the currently playing track, or null if the track + * has no [Metadata]. */ fun applyReplayGain(metadata: Metadata?) { + // TODO: Allow this to automatically obtain it's own [Metadata]. val gain = metadata?.let(::parseReplayGain) val preAmp = settings.replayGainPreAmp @@ -112,7 +108,14 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { volume = 10f.pow(adjust / 20f) } + /** + * Parse ReplayGain information from the given [Metadata]. + * @param metadata The [Metadata] to parse. + * @return A [Gain] adjustment, or null if there was no adjustments to parse. + */ private fun parseReplayGain(metadata: Metadata): Gain? { + // TODO: Unify this parser with the music parser? They both grok Metadata. + var trackGain = 0f var albumGain = 0f var found = false @@ -149,20 +152,29 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { } if (key in REPLAY_GAIN_TAGS) { - tags.add(GainTag(unlikelyToBeNull(key), parseReplayGainFloat(value))) + // Grok a float from a ReplayGain tag by removing everything that is not 0-9, , + // or -. + // Derived from vanilla music: https://github.com/vanilla-music/vanilla + val gainValue = try { + value.replace(Regex("[^\\d.-]"), "").toFloat() + } catch (e: Exception) { + 0f + } + + tags.add(GainTag(unlikelyToBeNull(key), gainValue)) } } // Case 1: Normal ReplayGain, most commonly found on MPEG files. tags - .findLast { tag -> tag.key.equals(RG_TRACK, ignoreCase = true) } + .findLast { tag -> tag.key.equals(TAG_RG_TRACK, ignoreCase = true) } ?.let { tag -> trackGain = tag.value found = true } tags - .findLast { tag -> tag.key.equals(RG_ALBUM, ignoreCase = true) } + .findLast { tag -> tag.key.equals(TAG_RG_ALBUM, ignoreCase = true) } ?.let { tag -> albumGain = tag.value found = true @@ -194,15 +206,6 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { null } } - - private fun parseReplayGainFloat(raw: String) = - // Grok a float from a ReplayGain tag by removing everything that is not 0-9, , or -. - try { - raw.replace(Regex("[^\\d.-]"), "").toFloat() - } catch (e: Exception) { - 0f - } - // --- AUDIO PROCESSOR IMPLEMENTATION --- override fun onConfigure( @@ -211,6 +214,8 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { if (inputAudioFormat.encoding == C.ENCODING_PCM_16BIT) { // AudioProcessor is only provided 16-bit PCM audio data, so that's the only // encoding we need to check for. + // TODO: Convert to a low-level audio processor capable of handling any kind of + // PCM data, once ExoPlayer can support it. return inputAudioFormat } @@ -244,27 +249,43 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { buffer.flip() } - // Normally, ByteBuffer endianness is determined by object state, which is possibly - // the most java thing I have ever heard. Instead of mutating that state and accidentally - // breaking downstream parsers of audio data, we have our own methods to always parse a - // little-endian value. - - /** Always get a little-endian short value from a [ByteBuffer] */ + /** + * Always read a little-endian [Short] from the [ByteBuffer] at the given index. + * @param at The index to read the [Short] from. + */ private fun ByteBuffer.getLeShort(at: Int) = get(at + 1).toInt().shl(8).or(get(at).toInt().and(0xFF)).toShort() - /** Always place a little-endian short value into a [ByteBuffer]. */ + /** + * Always write a little-endian [Short] at the end of the [ByteBuffer]. + * @param short The [Short] to write. + */ private fun ByteBuffer.putLeShort(short: Short) { put(short.toByte()) put(short.toInt().shr(8).toByte()) } + /** + * The resolved ReplayGain adjustment for a file. + * @param track The track adjustment (in dB), or 0 if it is not present. + * @param album The album adjustment (in dB), or 0 if it is not present. + */ + private data class Gain(val track: Float, val album: Float) + + /** + * A raw ReplayGain adjustment. + * @param key The tag's key. + * @param value The tag's adjustment, in dB. + * TODO: Try to phasse this out. + */ + private data class GainTag(val key: String, val value: Float) + companion object { - private const val RG_TRACK = "replaygain_track_gain" - private const val RG_ALBUM = "replaygain_album_gain" + private const val TAG_RG_TRACK = "replaygain_track_gain" + private const val TAG_RG_ALBUM = "replaygain_album_gain" private const val R128_TRACK = "r128_track_gain" private const val R128_ALBUM = "r128_album_gain" - private val REPLAY_GAIN_TAGS = arrayOf(RG_TRACK, RG_ALBUM, R128_ALBUM, R128_TRACK) + private val REPLAY_GAIN_TAGS = arrayOf(TAG_RG_TRACK, TAG_RG_ALBUM, R128_ALBUM, R128_TRACK) } }