replaygain: redocument
Redocument the replaygain module.
This commit is contained in:
parent
9d283fc6e4
commit
9cf8d54353
6 changed files with 75 additions and 47 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue