replaygain: redocument

Redocument the replaygain module.
This commit is contained in:
Alexander Capehart 2022-12-25 20:05:32 -07:00
parent 9d283fc6e4
commit 9cf8d54353
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 75 additions and 47 deletions

View file

@ -93,16 +93,16 @@ sealed class Tab(open val mode: MusicMode) {
/** /**
* Convert a [Tab] integer representation into it's corresponding array of [Tab]s. * 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. * @return An array of [Tab]s corresponding to the sequence.
*/ */
fun fromIntCode(sequence: Int): Array<Tab>? { fun fromIntCode(intCode: Int): Array<Tab>? {
val tabs = mutableListOf<Tab>() val tabs = mutableListOf<Tab>()
// Try to parse a mode for each chunk in the sequence. // Try to parse a mode for each chunk in the sequence.
// If we can't parse one, just skip it. // If we can't parse one, just skip it.
for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) { 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 val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue

View file

@ -48,7 +48,7 @@ enum class CoverMode {
* Convert a [CoverMode] integer representation into an instance. * Convert a [CoverMode] integer representation into an instance.
* @param intCode An integer representation of a [CoverMode] * @param intCode An integer representation of a [CoverMode]
* @return The corresponding [CoverMode], or null if the [CoverMode] is invalid. * @return The corresponding [CoverMode], or null if the [CoverMode] is invalid.
* @see intCode * @see CoverMode.intCode
*/ */
fun fromIntCode(intCode: Int) = fun fromIntCode(intCode: Int) =
when (intCode) { when (intCode) {

View file

@ -51,7 +51,7 @@ enum class MusicMode {
* Convert a [MusicMode] integer representation into an instance. * Convert a [MusicMode] integer representation into an instance.
* @param intCode An integer representation of a [MusicMode] * @param intCode An integer representation of a [MusicMode]
* @return The corresponding [MusicMode], or null if the [MusicMode] is invalid. * @return The corresponding [MusicMode], or null if the [MusicMode] is invalid.
* @see intCode * @see MusicMode.intCode
*/ */
fun fromIntCode(intCode: Int) = fun fromIntCode(intCode: Int) =
when (intCode) { when (intCode) {

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() { class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {

View file

@ -19,34 +19,41 @@ package org.oxycblt.auxio.playback.replaygain
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
/** Represents the current setting for ReplayGain. */ /**
* The current ReplayGain configuration.
* @author Alexander Capehart (OxygenCobalt)
*/
enum class ReplayGainMode { enum class ReplayGainMode {
/** Apply the track gain, falling back to the album gain if the track gain is not found. */ /** Apply the track gain, falling back to the album gain if the track gain is not found. */
TRACK, TRACK,
/** Apply the album gain, falling back to the track gain if the album gain is not found. */ /** Apply the album gain, falling back to the track gain if the album gain is not found. */
ALBUM, ALBUM,
/** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */ /** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */
DYNAMIC; DYNAMIC;
companion object { companion object {
/** Convert an int [code] into an instance, or null if it isn't valid. */ /**
fun fromIntCode(code: Int): ReplayGainMode? { * Convert a [ReplayGainMode] integer representation into an instance.
return when (code) { * @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_TRACK -> TRACK
IntegerTable.REPLAY_GAIN_MODE_ALBUM -> ALBUM IntegerTable.REPLAY_GAIN_MODE_ALBUM -> ALBUM
IntegerTable.REPLAY_GAIN_MODE_DYNAMIC -> DYNAMIC IntegerTable.REPLAY_GAIN_MODE_DYNAMIC -> DYNAMIC
else -> null 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( data class ReplayGainPreAmp(
/** The value to use when ReplayGain tags are present. */
val with: Float, val with: Float,
/** The value to use when ReplayGain tags are not present. */
val without: Float val without: Float
) )

View file

@ -38,17 +38,11 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* Instead of leveraging the volume attribute like other implementations, this system manipulates * 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. * 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 * Note: This instance must be updated with a new [Metadata] every time the active track chamges.
* when the active track changes.
* *
* @author Alexander Capehart (OxygenCobalt) * @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() { 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 playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context) private val settings = Settings(context)
@ -62,10 +56,12 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// --- REPLAYGAIN PARSING --- // --- REPLAYGAIN PARSING ---
/** /**
* Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is tangentially * Updates the volume adjustment based on the given [Metadata].
* based off Vanilla Music's implementation, but has diverged to a significant extent. * @param metadata The [Metadata] of the currently playing track, or null if the track
* has no [Metadata].
*/ */
fun applyReplayGain(metadata: Metadata?) { fun applyReplayGain(metadata: Metadata?) {
// TODO: Allow this to automatically obtain it's own [Metadata].
val gain = metadata?.let(::parseReplayGain) val gain = metadata?.let(::parseReplayGain)
val preAmp = settings.replayGainPreAmp val preAmp = settings.replayGainPreAmp
@ -112,7 +108,14 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
volume = 10f.pow(adjust / 20f) 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? { private fun parseReplayGain(metadata: Metadata): Gain? {
// TODO: Unify this parser with the music parser? They both grok Metadata.
var trackGain = 0f var trackGain = 0f
var albumGain = 0f var albumGain = 0f
var found = false var found = false
@ -149,20 +152,29 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
} }
if (key in REPLAY_GAIN_TAGS) { 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. // Case 1: Normal ReplayGain, most commonly found on MPEG files.
tags tags
.findLast { tag -> tag.key.equals(RG_TRACK, ignoreCase = true) } .findLast { tag -> tag.key.equals(TAG_RG_TRACK, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
trackGain = tag.value trackGain = tag.value
found = true found = true
} }
tags tags
.findLast { tag -> tag.key.equals(RG_ALBUM, ignoreCase = true) } .findLast { tag -> tag.key.equals(TAG_RG_ALBUM, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
albumGain = tag.value albumGain = tag.value
found = true found = true
@ -194,15 +206,6 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
null 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 --- // --- AUDIO PROCESSOR IMPLEMENTATION ---
override fun onConfigure( override fun onConfigure(
@ -211,6 +214,8 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
if (inputAudioFormat.encoding == C.ENCODING_PCM_16BIT) { if (inputAudioFormat.encoding == C.ENCODING_PCM_16BIT) {
// AudioProcessor is only provided 16-bit PCM audio data, so that's the only // AudioProcessor is only provided 16-bit PCM audio data, so that's the only
// encoding we need to check for. // 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 return inputAudioFormat
} }
@ -244,27 +249,43 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
buffer.flip() 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 * Always read a little-endian [Short] from the [ByteBuffer] at the given index.
// breaking downstream parsers of audio data, we have our own methods to always parse a * @param at The index to read the [Short] from.
// little-endian value. */
/** Always get a little-endian short value from a [ByteBuffer] */
private fun ByteBuffer.getLeShort(at: Int) = private fun ByteBuffer.getLeShort(at: Int) =
get(at + 1).toInt().shl(8).or(get(at).toInt().and(0xFF)).toShort() 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) { private fun ByteBuffer.putLeShort(short: Short) {
put(short.toByte()) put(short.toByte())
put(short.toInt().shr(8).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 { companion object {
private const val RG_TRACK = "replaygain_track_gain" private const val TAG_RG_TRACK = "replaygain_track_gain"
private const val RG_ALBUM = "replaygain_album_gain" private const val TAG_RG_ALBUM = "replaygain_album_gain"
private const val R128_TRACK = "r128_track_gain" private const val R128_TRACK = "r128_track_gain"
private const val R128_ALBUM = "r128_album_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)
} }
} }