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.
* @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<Tab>? {
fun fromIntCode(intCode: Int): Array<Tab>? {
val tabs = mutableListOf<Tab>()
// 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

View file

@ -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) {

View file

@ -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) {

View file

@ -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<DialogPreAmpBinding>() {

View file

@ -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
)

View file

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