diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainModels.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/Models.kt
similarity index 90%
rename from app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainModels.kt
rename to app/src/main/java/org/oxycblt/auxio/playback/replaygain/Models.kt
index 387d7cea4..87ffc95dd 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainModels.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/Models.kt
@@ -43,8 +43,10 @@ enum class ReplayGainMode {
}
}
-/** Represents the ReplayGain pre-amp */
+/** Represents the ReplayGain pre-amp values. */
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,
)
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
index 8339530ab..c4780d81d 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
@@ -59,8 +59,9 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
// --- REPLAYGAIN PARSING ---
/**
- * Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is based off
- * Vanilla Music's implementation.
+ * 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.
*/
fun applyReplayGain(metadata: Metadata?) {
if (settingsManager.replayGainMode == ReplayGainMode.OFF) {
@@ -102,10 +103,11 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
gain.track
}
- // Apply the "With tags" adjustment
+ // Apply the adjustment specified when there is ReplayGain tags.
resolvedGain + preAmp.with
} else {
- // No gain tags were present, just apply the adjustment without tags.
+ // No ReplayGain tags existed, or no tags were parsable, or there was no metadata
+ // in the first place. Return the gain to use when there is no ReplayGain value.
logD("No ReplayGain tags present ")
preAmp.without
}
@@ -157,7 +159,8 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
found = true
}
- // Case 2: R128 ReplayGain, most commonly found on FLAC files.
+ // Case 2: R128 ReplayGain, most commonly found on FLAC files and other lossless
+ // encodings to increase precision in volume adjustments.
// While technically there is the R128 base gain in Opus files, that is automatically
// applied by the media framework [which ExoPlayer relies on]. The only reason we would
// want to read it is to zero previous ReplayGain values for being invalid, however there
@@ -215,8 +218,8 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
} else {
for (i in position until limit step 2) {
// Ensure we clamp the values to the minimum and maximum values possible
- // for the encoding. This prevents issues where samples amplified beyond
- // 1 << 16 will end up becoming truncated during the conversion to a short,
+ // for the encoding. This prevents issues where samples amplified beyond
+ // 1 << 16 will end up becoming truncated during the conversion to a short,
// resulting in popping.
var sample = inputBuffer.getLeShort(i)
sample =
@@ -232,10 +235,17 @@ class ReplayGainAudioProcessor : 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] */
private fun ByteBuffer.getLeShort(at: Int): Short {
return get(at + 1).toInt().shl(8).or(get(at).toInt().and(0xFF)).toShort()
}
+ /** Always place a little-endian short value into a [ByteBuffer]. */
private fun ByteBuffer.putLeShort(short: Short) {
put(short.toByte())
put(short.toInt().shr(8).toByte())
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainDialog.kt
new file mode 100644
index 000000000..d7439e0bc
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainDialog.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.playback.replaygain
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import kotlin.math.abs
+import org.oxycblt.auxio.BuildConfig
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.databinding.DialogPreAmpBinding
+import org.oxycblt.auxio.settings.SettingsManager
+import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.util.textSafe
+
+/**
+ * The dialog for customizing the ReplayGain pre-amp values.
+ * @author OxygenCobalt
+ */
+class ReplayGainDialog : ViewBindingDialogFragment() {
+ private val settingsManager = SettingsManager.getInstance()
+
+ override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater)
+
+ override fun onConfigDialog(builder: AlertDialog.Builder) {
+ builder
+ .setTitle(R.string.set_pre_amp)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ val binding = requireBinding()
+ settingsManager.replayGainPreAmp =
+ ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ }
+
+ override fun onBindingCreated(binding: DialogPreAmpBinding, savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
+ // First initialization, we need to supply the sliders with the values from
+ // settings. After this, the sliders save their own state, so we do not need to
+ // do any restore behavior.
+ val preAmp = settingsManager.replayGainPreAmp
+ binding.withTagsSlider.value = preAmp.with
+ binding.withoutTagsSlider.value = preAmp.without
+ }
+
+ // The listener always fires when the Slider restores it's own state, *except* when
+ // it's at it's default value. Then it doesn't. Just initialize the ticker ourselves.
+ updateTicker(binding.withTagsTicker, binding.withTagsSlider.value)
+ binding.withTagsSlider.addOnChangeListener { _, value, _ ->
+ updateTicker(binding.withTagsTicker, value)
+ }
+
+ updateTicker(binding.withoutTagsTicker, binding.withoutTagsSlider.value)
+ binding.withoutTagsSlider.addOnChangeListener { _, value, _ ->
+ updateTicker(binding.withoutTagsTicker, value)
+ }
+ }
+
+ private fun updateTicker(ticker: TextView, valueDb: Float) {
+ // It is more clear to prepend a +/- before the pre-amp value to make it easier to
+ // gauge how much it may be increasing the volume, however android does not add +
+ // to positive float values when formatting them in a string. Instead, add it ourselves.
+ ticker.textSafe =
+ if (valueDb >= 0) {
+ getString(R.string.fmt_db_pos, valueDb)
+ } else {
+ getString(R.string.fmt_db_neg, abs(valueDb))
+ }
+ }
+
+ companion object {
+ const val TAG = BuildConfig.APPLICATION_ID + ".tag.PRE_AMP_CUSTOMIZE"
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
index 568f0e472..4a5e19f46 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
@@ -35,7 +35,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.getSystemServiceSafe
-import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newBroadcastIntent
import org.oxycblt.auxio.util.newMainIntent
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt
index fb552911f..782af507a 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt
@@ -33,6 +33,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.excluded.ExcludedDialog
import org.oxycblt.auxio.playback.PlaybackViewModel
+import org.oxycblt.auxio.playback.replaygain.ReplayGainDialog
+import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.settings.pref.IntListPreference
import org.oxycblt.auxio.settings.pref.IntListPreferenceDialog
import org.oxycblt.auxio.ui.accent.AccentCustomizeDialog
@@ -88,6 +90,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
// to adequately supply a "target fragment" (otherwise we will crash since the dialog
// requires one), and then we need to actually show the dialog, making sure we use
// the parent FragmentManager as again, it will crash if we don't.
+ //
// Fragments were a mistake.
val dialog = IntListPreferenceDialog.from(preference)
dialog.setTargetFragment(this, 0)
@@ -150,7 +153,22 @@ class SettingsListFragment : PreferenceFragmentCompat() {
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() }
-
+ true
+ }
+ }
+ SettingsManager.KEY_REPLAY_GAIN -> {
+ notifyDependencyChange(settingsManager.replayGainMode == ReplayGainMode.OFF)
+ onPreferenceChangeListener =
+ Preference.OnPreferenceChangeListener { _, value ->
+ notifyDependencyChange(
+ ReplayGainMode.fromIntCode(value as Int) == ReplayGainMode.OFF)
+ true
+ }
+ }
+ SettingsManager.KEY_PRE_AMP -> {
+ onPreferenceClickListener =
+ Preference.OnPreferenceClickListener {
+ ReplayGainDialog().show(childFragmentManager, ReplayGainDialog.TAG)
true
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt
index b785017cf..11ed1ae4e 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt
@@ -23,9 +23,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.oxycblt.auxio.home.tabs.Tab
-import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
+import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.accent.Accent
@@ -109,12 +109,11 @@ class SettingsManager private constructor(context: Context) :
var replayGainPreAmp: ReplayGainPreAmp
get() =
ReplayGainPreAmp(
- prefs.getFloat(KEY_REPLAY_GAIN_PRE_AMP_WITH, 0f),
- prefs.getFloat(KEY_REPLAY_GAIN_PRE_AMP_WITHOUT, 0f))
+ prefs.getFloat(KEY_PRE_AMP_WITH, 0f), prefs.getFloat(KEY_PRE_AMP_WITHOUT, 0f))
set(value) {
prefs.edit {
- putFloat(KEY_REPLAY_GAIN_PRE_AMP_WITH, value.with)
- putFloat(KEY_REPLAY_GAIN_PRE_AMP_WITHOUT, value.without)
+ putFloat(KEY_PRE_AMP_WITH, value.with)
+ putFloat(KEY_PRE_AMP_WITHOUT, value.without)
apply()
}
}
@@ -258,7 +257,7 @@ class SettingsManager private constructor(context: Context) :
KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { it.onNotifSettingsChanged() }
KEY_SHOW_COVERS, KEY_QUALITY_COVERS -> callbacks.forEach { it.onCoverSettingsChanged() }
KEY_LIB_TABS -> callbacks.forEach { it.onLibraryChanged() }
- KEY_REPLAY_GAIN, KEY_REPLAY_GAIN_PRE_AMP_WITH, KEY_REPLAY_GAIN_PRE_AMP_WITHOUT ->
+ KEY_REPLAY_GAIN, KEY_PRE_AMP_WITH, KEY_PRE_AMP_WITHOUT ->
callbacks.forEach { it.onReplayGainSettingsChanged() }
}
}
@@ -290,9 +289,9 @@ class SettingsManager private constructor(context: Context) :
const val KEY_HEADSET_AUTOPLAY = "auxio_headset_autoplay"
const val KEY_REPLAY_GAIN = "auxio_replay_gain"
- const val KEY_REPLAY_GAIN_PRE_AMP = "auxio_pre_amp"
- const val KEY_REPLAY_GAIN_PRE_AMP_WITH = "auxio_pre_amp_with"
- const val KEY_REPLAY_GAIN_PRE_AMP_WITHOUT = "auxio_pre_amp_without"
+ const val KEY_PRE_AMP = "auxio_pre_amp"
+ const val KEY_PRE_AMP_WITH = "auxio_pre_amp_with"
+ const val KEY_PRE_AMP_WITHOUT = "auxio_pre_amp_without"
const val KEY_SONG_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
const val KEY_KEEP_SHUFFLE = "KEY_KEEP_SHUFFLE"
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt
index db945a60b..fe93acc81 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt
@@ -40,20 +40,20 @@ class AccentCustomizeDialog :
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) {
- builder.setTitle(R.string.set_accent)
+ builder
+ .setTitle(R.string.set_accent)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ if (accentAdapter.selectedAccent != settingsManager.accent) {
+ logD("Applying new accent")
+ settingsManager.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
+ requireActivity().recreate()
+ }
- builder.setPositiveButton(android.R.string.ok) { _, _ ->
- if (accentAdapter.selectedAccent != settingsManager.accent) {
- logD("Applying new accent")
- settingsManager.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
- requireActivity().recreate()
+ dismiss()
}
- dismiss()
- }
-
- // Negative button just dismisses, no need for a listener.
- builder.setNegativeButton(android.R.string.cancel, null)
+ // Negative button just dismisses, no need for a listener.
+ .setNegativeButton(android.R.string.cancel, null)
}
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
diff --git a/app/src/main/res/layout/dialog_excluded.xml b/app/src/main/res/layout/dialog_excluded.xml
index 9067fda64..f4bc48212 100644
--- a/app/src/main/res/layout/dialog_excluded.xml
+++ b/app/src/main/res/layout/dialog_excluded.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- android:paddingTop="@dimen/spacing_small">
+ android:paddingTop="@dimen/spacing_medium">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_tabs.xml b/app/src/main/res/layout/dialog_tabs.xml
index fc292412b..4377b262d 100644
--- a/app/src/main/res/layout/dialog_tabs.xml
+++ b/app/src/main/res/layout/dialog_tabs.xml
@@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
- android:paddingTop="@dimen/spacing_small"
+ android:paddingTop="@dimen/spacing_medium"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
app:layout_constraintTop_toBottomOf="@+id/accent_header"
diff --git a/app/src/main/res/layout/item_excluded_dir.xml b/app/src/main/res/layout/item_excluded_dir.xml
index f0fe034bf..14bca0d07 100644
--- a/app/src/main/res/layout/item_excluded_dir.xml
+++ b/app/src/main/res/layout/item_excluded_dir.xml
@@ -3,11 +3,8 @@
تفضيل نشاط الخلط
صوتيات
- صخب الصوت (تجريبي)
+ صخب الصوت
اطفاء
تفضيل المقطع
تفضيل الالبوم
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 36464d1bc..0c375acfe 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -73,7 +73,7 @@
Audio
Kopfhörer automatische Wiedergabe
Beginne die Wiedergabe immer, wenn Kopfhörer verbunden sind (funktioniert nicht auf allen Geräten)
- ReplayGain (Experimentell)
+ ReplayGain
Aus
Titel bevorzugen
Album bevorzugen
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index bd821b884..b9e43f475 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -81,7 +81,7 @@
Preferir acción de mezcla
Sonido
- ReplayGain (Experimental)
+ ReplayGain
Desactivado
Por pista
Por álbum
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index a92f09b70..2ace338c8 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -83,7 +83,7 @@
Audio
Autoplay cuffie
Comincia la riproduzione ogni volta che le cuffie sono inserite (potrebbe non funzionare su tutti i dispositivi)
- Replay Gain (Sperimentale)
+ Replay Gain
Preferisci traccia
Preferisci disco
Dinamico
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 11c8b2362..9cef75174 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -83,7 +83,7 @@
Звук
Воспроизводить при подключении
Всегда начинать воспроизведение при подключении наушников (может работать не на всех устройствах)
- ReplayGain (экспериментально)
+ ReplayGain
Выкл.
По треку
По альбому
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a3f4c9a69..690398bff 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -82,7 +82,7 @@
音频
自动播放
连接至耳机时总是自动播放(并非在所有设备上都有用)
- 回放增益(实验性)
+ 回放增益
偏好曲目
偏好专辑
动态
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index baa70df53..0db474ae6 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -42,8 +42,6 @@
6dp
16dp
- 6dp
- 10dp
88dp
128dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1040d8501..8849ccffb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -87,10 +87,15 @@
Audio
Headset autoplay
Always start playing when a headset is connected (may not work on all devices)
- ReplayGain (Experimental)
+ ReplayGain
Prefer track
Prefer album
Dynamic
+ ReplayGain pre-amp
+ The pre-amp is applied to the existing adjustment during playback
+ Adjustment with tags
+ Adjustment without tags
+ Warning: Changing the pre-amp to a high positive value may result in peaking on some audio tracks.
Behavior
When a song is selected
@@ -109,6 +114,7 @@
Excluded folders
The content of excluded folders is hidden from your library
+
Off
@@ -173,6 +179,10 @@
Disc %d
+
+ +%.1f dB
+ -%.1f dB
+
Songs loaded: %d
Albums loaded: %d
Artists loaded: %d
diff --git a/app/src/main/res/values/styles_android.xml b/app/src/main/res/values/styles_android.xml
index d592f2350..023ca1d36 100644
--- a/app/src/main/res/values/styles_android.xml
+++ b/app/src/main/res/values/styles_android.xml
@@ -6,7 +6,7 @@
A dialog theme that doesn't suck. This is the only non-Material3 usage in the entire
project since the Material3 dialogs [and especially the button panel] are unusable.
-->
-