playback: add dynamic replaygain mode [#7]

Add a "Dynamic" replaygain mode inspired by the FooBar2000 plugin. This
will automatically determine whether the playback is in an album or not
and use the album gain or track gain accordingly.
This commit is contained in:
OxygenCobalt 2022-01-13 20:15:22 -07:00
parent 5359c819bd
commit ddf2757cb6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 85 additions and 28 deletions

View file

@ -63,9 +63,9 @@ I primarily built Auxio for myself, but you can use it too, I guess.
## Building
Auxio relies on a local version of ExoPlayer that enables some extra features. So, the build process is as follows:
Auxio relies on a custom version of ExoPlayer that enables some extra features. So, the build process is as follows:
1. Change into the project directory
1. Enter into the project directory
2. Run `python3 prebuild.py`, which installs ExoPlayer and it's extensions.
3. Build the project normally in Android Studio.

View file

@ -27,6 +27,7 @@ import androidx.media.AudioManagerCompat
import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe
@ -88,30 +89,54 @@ class AudioReactor(
* This is based off Vanilla Music's implementation.
*/
fun applyReplayGain(metadata: Metadata?) {
if (settingsManager.replayGainMode == ReplayGainMode.OFF || metadata == null) {
logD("ReplayGain is disabled or cannot be determined for this track, resetting volume.")
if (metadata == null) {
logD("No metadata.")
volume = 1f
return
}
// ReplayGain is configurable, so determine what to do based off of the mode.
val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) {
ReplayGainMode.OFF -> {
logD("ReplayGain is off.")
volume = 1f
return
}
// User wants track gain to be preferred
ReplayGainMode.TRACK ->
{ gain ->
gain.track == 0f
}
ReplayGainMode.ALBUM ->
{ gain ->
gain.album != 0f
}
ReplayGainMode.DYNAMIC ->
{ _ ->
playbackManager.parent is Album &&
playbackManager.song?.album == playbackManager.parent
}
}
val gain = parseReplayGain(metadata)
// Currently we consider both the album and the track gain.
var adjust = 0f
if (gain != null) {
// Allow the user to configure a preferred mode for ReplayGain.
adjust = if (settingsManager.replayGainMode == ReplayGainMode.TRACK) {
if (gain.track != 0f) gain.track else gain.album
val adjust = if (gain != null) {
if (useAlbumGain(gain)) {
logD("Using album gain.")
gain.album
} else {
if (gain.album != 0f) gain.album else gain.track
logD("Using track gain.")
gain.track
}
} else {
0f
}
// Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
volume = MathUtils.clamp((10f.pow((adjust / 20f))), 0f, 1f)
logD("Applied ReplayGain adjustment: $volume")
}
private fun parseReplayGain(metadata: Metadata): Gain? {
@ -126,14 +151,25 @@ class AudioReactor(
for (i in 0 until metadata.length()) {
val entry = metadata.get(i)
// Sometimes the ReplayGain keys will be lowercase, so make them uppercase.
if (entry is TextInformationFrame && entry.description?.uppercase() in REPLAY_GAIN_TAGS) {
tags.add(GainTag(entry.description!!.uppercase(), parseReplayGainFloat(entry.value)))
continue
val key: String?
val value: String
when (entry) {
is TextInformationFrame -> {
key = entry.description?.uppercase()
value = entry.value
}
is VorbisComment -> {
key = entry.key
value = entry.value
}
else -> continue
}
if (entry is VorbisComment && entry.key.uppercase() in REPLAY_GAIN_TAGS) {
tags.add(GainTag(entry.key.uppercase(), parseReplayGainFloat(entry.value)))
if (key in REPLAY_GAIN_TAGS) {
tags.add(GainTag(key!!, parseReplayGainFloat(value)))
}
}

View file

@ -3,13 +3,15 @@ package org.oxycblt.auxio.playback.system
enum class ReplayGainMode {
OFF,
TRACK,
ALBUM;
ALBUM,
DYNAMIC;
fun toInt(): Int {
return when (this) {
OFF -> INT_OFF
TRACK -> INT_TRACK
ALBUM -> INT_ALBUM
DYNAMIC -> INT_DYNAMIC
}
}
@ -17,12 +19,14 @@ enum class ReplayGainMode {
private const val INT_OFF = 0xA110
private const val INT_TRACK = 0xA111
private const val INT_ALBUM = 0xA112
private const val INT_DYNAMIC = 0xA113
fun fromInt(value: Int): ReplayGainMode? {
return when (value) {
INT_OFF -> OFF
INT_TRACK -> TRACK
INT_ALBUM -> ALBUM
INT_DYNAMIC -> DYNAMIC
else -> null
}
}

View file

@ -36,12 +36,14 @@
<item>@string/set_replay_gain_off</item>
<item>@string/set_replay_gain_track</item>
<item>@string/set_replay_gain_album</item>
<item>@string/set_replay_gain_dynamic</item>
</array>
<string-array name="values_replay_gain">
<item>@integer/replay_gain_off</item>
<item>@integer/replay_gain_track</item>
<item>@integer/replay_gain_album</item>
<item>@integer/replay_gain_dynamic</item>
</string-array>
<integer name="theme_auto">-1</integer>
@ -56,4 +58,5 @@
<integer name="replay_gain_off">0xA110</integer>
<integer name="replay_gain_track">0xA111</integer>
<integer name="replay_gain_album">0xA112</integer>
<integer name="replay_gain_dynamic">0xA113</integer>
</resources>

View file

@ -85,10 +85,11 @@
<string name="set_focus_desc">Pause when other audio plays (ex. Calls)</string>
<string name="set_plug_mgt">Headset focus</string>
<string name="set_plug_mgt_desc">Play/Pause when the headset connection changes</string>
<string name="set_replay_gain">ReplayGain (MP3/FLAC Only)</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_replay_gain_off">Off</string>
<string name="set_replay_gain_track">Prefer track</string>
<string name="set_replay_gain_album">Prefer album</string>
<string name="set_replay_gain_dynamic">Dynamic</string>
<string name="set_behavior">Behavior</string>
<string name="set_song_mode">When a song is selected</string>

View file

@ -30,6 +30,19 @@ As per the [Supported ExoPlayer Formats](https://exoplayer.dev/supported-formats
MP4, MP3, MKA, OGG, WAV, MPEG, AAC on all versions of Android. Auxio also supports FLAC on all versions
of Android through the use of the ExoPlayer FLAC extension.
#### ReplayGain isn't working on my music!
This is for a couple reason:
- Auxio doesn't extract ReplayGain tags for your format. This is the case with MP4 files since there's no
defined ReplayGain standard for those.
- Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
an unrecognized name.
#### What is dynamic ReplayGain?
Dynamic ReplayGain is a quirk based off the FooBar2000 plugin that dynamically switches from track gain to album
gain depending on if the current playback is from an album or not.
#### Why are accents lighter/less saturated in dark mode?
As per the [Material Design Guidelines](https://material.io/design/color/dark-theme.html), accents should be less

View file

@ -33,7 +33,7 @@ def sh(cmd):
exoplayer_path = os.path.join(os.path.abspath(os.curdir), "deps", "exoplayer")
if os.path.exists(exoplayer_path):
reinstall = input(INFO + "info:" + NC + " ExoPlayer is already installed. Would you like to reinstall it? [y/n] ")
reinstall = input(INFO + "info:" + NC + " exoplayer is already installed. would you like to reinstall it? [y/n] ")
if not re.match("[yY][eE][sS]|[yY]", reinstall):
sys.exit(0)
@ -55,31 +55,31 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk_build")):
candidates.append(entry.path)
if len(candidates) > 0:
print(WARN + "warn:" + NC + " NDK_PATH was not set or invalid. Multiple candidates were found however:")
print(WARN + "warn:" + NC + " NDK_PATH was not set or invalid. multiple candidates were found however:")
for i, candidate in enumerate(candidates):
print("[" + str(i) + "] " + candidate)
try:
ndk_path = candidates[int(input("Enter the NDK to use [Default 0]: "))]
ndk_path = candidates[int(input("Enter the ndk to use [Default 0]: "))]
except:
ndk_path = candidates[0]
else:
print(FATAL + "fatal:" + NC + " NDK_PATH is either invalid, or the Android NDK was not installed at a recognized location.")
print(FATAL + "fatal:" + NC + " NDK_PATH is either invalid, or the android ndk was not installed at a recognized location.")
system.exit(1)
# Now try to install ExoPlayer.
sh("rm -rf deps")
print(INFO + "info:" + NC + " Cloning ExoPlayer...")
print(INFO + "info:" + NC + " cloning ExoPlayer...")
sh("git clone https://github.com/oxygencobalt/ExoPlayer.git " + exoplayer_path)
os.chdir(exoplayer_path)
sh("git checkout release-v2")
flac_ext_jni_path = os.path.join("extensions", "flac", "src", "main", "jni")
print(INFO + "info:" + NC + " Installing FLAC extension...")
print(INFO + "info:" + NC + " installing FLAC extension...")
os.chdir(flac_ext_jni_path)
sh('curl "https://ftp.osuosl.org/pub/xiph/releases/flac/flac-' + FLAC_VERSION + '.tar.xz" | tar xJ && mv "flac-' + FLAC_VERSION + '" flac')
sh(ndk_path + "/ndk-build APP_ABI=all -j4")
print(OK + "success:" + NC + " Completed pre-build.")
print(OK + "success:" + NC + " completed pre-build.")