playback: add support for mp4 replaygain

Add support for MP4 ReplayGain tags. These are usually under a `----`
atom with an iTunes domain and ReplayGain description. These are
mapped to an ID3v2 internal frame within ExoPlayer, which is why
Auxio did not support them, as it only expected Vorbis comments and
ID3v2 TXXX frames.

Resolves #292.
This commit is contained in:
Alexander Capehart 2022-12-15 21:01:41 -07:00
parent 873f15ff40
commit 900a64bc02
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 18 additions and 10 deletions

View file

@ -16,6 +16,7 @@
- Music loader now caches parsed metadata for faster load times - Music loader now caches parsed metadata for faster load times
- Redesigned icon - Redesigned icon
- Added animated splash screen on Android 12+ - Added animated splash screen on Android 12+
- Added support for MP4 ReplayGain (`----`) atoms
#### What's Improved #### What's Improved
- Sorting now takes accented characters into account - Sorting now takes accented characters into account

View file

@ -51,7 +51,7 @@ I primarily built Auxio for myself, but you can use it too, I guess.
precise/original dates, sort tags, and more precise/original dates, sort tags, and more
- SD Card-aware folder management - SD Card-aware folder management
- Reliable playback state persistence - Reliable playback state persistence
- Full ReplayGain support (On MP3, FLAC, OGG, OPUS, and (some) MP4 files) - Full ReplayGain support (On MP3, FLAC, OGG, OPUS, and MP4 files)
- External equalizer support (ex. Wavelet) - External equalizer support (ex. Wavelet)
- Edge-to-edge - Edge-to-edge
- Embedded covers support - Embedded covers support

View file

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.audio.AudioProcessor
import com.google.android.exoplayer2.audio.BaseAudioProcessor import com.google.android.exoplayer2.audio.BaseAudioProcessor
import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.id3.InternalFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.pow import kotlin.math.pow
@ -129,9 +130,15 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// (like "replaygain_track_gain"), but can also be uppercase. Make sure that // (like "replaygain_track_gain"), but can also be uppercase. Make sure that
// capitalization is consistent before continuing. // capitalization is consistent before continuing.
is TextInformationFrame -> { is TextInformationFrame -> {
key = entry.description?.uppercase() key = entry.description
value = entry.values[0] value = entry.values[0]
} }
// Internal Frame. This is actually MP4's "----" atom, but mapped to an ID3v2
// frame by ExoPlayer (presumably to reduce duplication).
is InternalFrame -> {
key = entry.description
value = entry.text
}
// Vorbis comment. These are nearly always uppercase, so a check for such is // Vorbis comment. These are nearly always uppercase, so a check for such is
// skipped. // skipped.
is VorbisComment -> { is VorbisComment -> {
@ -148,14 +155,14 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// 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 == RG_TRACK } .findLast { tag -> tag.key.equals(RG_TRACK, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
trackGain = tag.value trackGain = tag.value
found = true found = true
} }
tags tags
.findLast { tag -> tag.key == RG_ALBUM } .findLast { tag -> tag.key.equals(RG_ALBUM, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
albumGain = tag.value albumGain = tag.value
found = true found = true
@ -168,14 +175,14 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// want to read it is to zero previous ReplayGain values for being invalid, however there // want to read it is to zero previous ReplayGain values for being invalid, however there
// is no demand to fix that edge case right now. // is no demand to fix that edge case right now.
tags tags
.findLast { tag -> tag.key == R128_TRACK } .findLast { tag -> tag.key.equals(R128_TRACK, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
trackGain += tag.value / 256f trackGain += tag.value / 256f
found = true found = true
} }
tags tags
.findLast { tag -> tag.key == R128_ALBUM } .findLast { tag -> tag.key.equals(R128_ALBUM, ignoreCase = true) }
?.let { tag -> ?.let { tag ->
albumGain += tag.value / 256f albumGain += tag.value / 256f
found = true found = true
@ -253,10 +260,10 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
} }
companion object { companion object {
private const val RG_TRACK = "REPLAYGAIN_TRACK_GAIN" private const val RG_TRACK = "replaygain_track_gain"
private const val RG_ALBUM = "REPLAYGAIN_ALBUM_GAIN" private const val 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(RG_TRACK, RG_ALBUM, R128_ALBUM, R128_TRACK)
} }