playback: add pre-amp customization

Implement a UI frontend for customizing the ReplayGain pre-amp value.

This finally completes Auxio's ReplayGain implementation. Not only
that, it also shows how Auxio can use positive ReplayGain values,
unlike other apps.

As a side-note, this also fiddles with the dialog style somewhat.
I got carried away.

Resolves #114.
This commit is contained in:
OxygenCobalt 2022-05-22 09:32:23 -06:00
parent 519de0e1d5
commit 21fccf1f31
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 294 additions and 49 deletions

View file

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

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DialogPreAmpBinding>() {
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"
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/excluded_recycler"
@ -22,7 +22,10 @@
android:id="@+id/excluded_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_medium"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_medium"
android:text="@string/lbl_no_dirs"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Auxio.TitleMidLarge"

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/with_tags_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginStart="@dimen/spacing_mid_large"
android:text="@string/set_pre_amp_with"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.slider.Slider
android:id="@+id/with_tags_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:stepSize="0.1"
android:valueFrom="-15.0"
android:valueTo="15.0"
app:labelBehavior="gone"
app:thumbRadius="@dimen/slider_thumb_radius"
app:haloRadius="@dimen/slider_halo_radius"
app:tickVisible="false"
android:layout_marginStart="@dimen/spacing_small"
app:layout_constraintEnd_toStartOf="@+id/with_tags_ticker"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/with_tags_header"
tools:value="0.0" />
<TextView
android:id="@+id/with_tags_ticker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minWidth="@dimen/size_btn_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:gravity="center"
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/with_tags_slider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/with_tags_slider"
tools:text="+1.6 dB" />
<TextView
android:id="@+id/without_tags_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginStart="@dimen/spacing_mid_large"
android:text="@string/set_pre_amp_without"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/with_tags_slider" />
<com.google.android.material.slider.Slider
android:id="@+id/without_tags_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:stepSize="0.1"
android:valueFrom="-15.0"
android:valueTo="15.0"
app:thumbRadius="@dimen/slider_thumb_radius"
app:haloRadius="@dimen/slider_halo_radius"
android:layout_marginStart="@dimen/spacing_small"
app:tickVisible="false"
app:labelBehavior="gone"
app:layout_constraintEnd_toStartOf="@+id/without_tags_ticker"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/without_tags_header"
tools:value="0.0" />
<TextView
android:id="@+id/without_tags_ticker"
android:layout_width="0dp"
android:gravity="center"
android:minWidth="@dimen/size_btn_large"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
android:layout_marginEnd="@dimen/spacing_mid_large"
app:layout_constraintBottom_toBottomOf="@+id/without_tags_slider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/without_tags_slider"
tools:text="+1.6 dB" />
<TextView
android:id="@+id/pre_amp_notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:text="@string/set_pre_amp_warning"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:textAppearance="@style/TextAppearance.Auxio.BodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/without_tags_slider" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

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

View file

@ -3,11 +3,8 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Auxio.ItemLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:padding="0dp">
<TextView
@ -15,7 +12,10 @@
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_medium"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small"
android:gravity="center"
android:maxLines="@null"
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
@ -29,11 +29,15 @@
android:id="@+id/excluded_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_small"
android:background="@drawable/ui_unbounded_ripple"
android:contentDescription="@string/desc_blacklist_delete"
android:minWidth="@dimen/size_btn_small"
android:minHeight="@dimen/size_btn_small"
android:paddingTop="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small"
android:paddingEnd="@dimen/spacing_medium"
android:paddingStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_small"
android:src="@drawable/ic_clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -14,9 +14,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="false"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginBottom="@dimen/spacing_small"
android:clickable="false"
android:focusable="false"
android:paddingStart="@dimen/spacing_medium"
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
@ -39,6 +39,7 @@
android:minHeight="@dimen/size_btn_small"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_small"
android:scaleType="center"
android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/tab_icon"

View file

@ -81,7 +81,7 @@
<string name="set_alt_shuffle">تفضيل نشاط الخلط</string>
<string name="set_audio">صوتيات</string>
<string name="set_replay_gain">صخب الصوت (تجريبي)</string>
<string name="set_replay_gain">صخب الصوت</string>
<string name="set_off">اطفاء</string>
<string name="set_replay_gain_track">تفضيل المقطع</string>
<string name="set_replay_gain_album">تفضيل الالبوم</string>

View file

@ -73,7 +73,7 @@
<string name="set_audio">Audio</string>
<string name="set_headset_autoplay">Kopfhörer automatische Wiedergabe</string>
<string name="set_headset_autoplay_desc">Beginne die Wiedergabe immer, wenn Kopfhörer verbunden sind (funktioniert nicht auf allen Geräten)</string>
<string name="set_replay_gain">ReplayGain (Experimentell)</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_off">Aus</string>
<string name="set_replay_gain_track">Titel bevorzugen</string>
<string name="set_replay_gain_album">Album bevorzugen</string>

View file

@ -81,7 +81,7 @@
<string name="set_alt_shuffle">Preferir acción de mezcla</string>
<string name="set_audio">Sonido</string>
<string name="set_replay_gain">ReplayGain (Experimental)</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_off">Desactivado</string>
<string name="set_replay_gain_track">Por pista</string>
<string name="set_replay_gain_album">Por álbum</string>

View file

@ -83,7 +83,7 @@
<string name="set_audio">Audio</string>
<string name="set_headset_autoplay">Autoplay cuffie</string>
<string name="set_headset_autoplay_desc">Comincia la riproduzione ogni volta che le cuffie sono inserite (potrebbe non funzionare su tutti i dispositivi)</string>
<string name="set_replay_gain">Replay Gain (Sperimentale)</string>
<string name="set_replay_gain">Replay Gain</string>
<string name="set_replay_gain_track">Preferisci traccia</string>
<string name="set_replay_gain_album">Preferisci disco</string>
<string name="set_replay_gain_dynamic">Dinamico</string>

View file

@ -83,7 +83,7 @@
<string name="set_audio">Звук</string>
<string name="set_headset_autoplay">Воспроизводить при подключении</string>
<string name="set_headset_autoplay_desc">Всегда начинать воспроизведение при подключении наушников (может работать не на всех устройствах)</string>
<string name="set_replay_gain">ReplayGain (экспериментально)</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_off">Выкл.</string>
<string name="set_replay_gain_track">По треку</string>
<string name="set_replay_gain_album">По альбому</string>

View file

@ -82,7 +82,7 @@
<string name="set_audio">音频</string>
<string name="set_headset_autoplay">自动播放</string>
<string name="set_headset_autoplay_desc">连接至耳机时总是自动播放(并非在所有设备上都有用)</string>
<string name="set_replay_gain">回放增益(实验性)</string>
<string name="set_replay_gain">回放增益</string>
<string name="set_replay_gain_track">偏好曲目</string>
<string name="set_replay_gain_album">偏好专辑</string>
<string name="set_replay_gain_dynamic">动态</string>

View file

@ -42,8 +42,6 @@
<dimen name="slider_thumb_radius">6dp</dimen>
<dimen name="slider_halo_radius">16dp</dimen>
<dimen name="slider_thumb_radius_collapsed">6dp</dimen>
<dimen name="slider_thumb_radius_expanded">10dp</dimen>
<dimen name="recycler_fab_space_normal">88dp</dimen>
<dimen name="recycler_fab_space_large">128dp</dimen>

View file

@ -87,10 +87,15 @@
<string name="set_audio">Audio</string>
<string name="set_headset_autoplay">Headset autoplay</string>
<string name="set_headset_autoplay_desc">Always start playing when a headset is connected (may not work on all devices)</string>
<string name="set_replay_gain">ReplayGain (Experimental)</string>
<string name="set_replay_gain">ReplayGain</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_pre_amp">ReplayGain pre-amp</string>
<string name="set_pre_amp_desc">The pre-amp is applied to the existing adjustment during playback</string>
<string name="set_pre_amp_with">Adjustment with tags</string>
<string name="set_pre_amp_without">Adjustment without tags</string>
<string name="set_pre_amp_warning">Warning: Changing the pre-amp to a high positive value may result in peaking on some audio tracks.</string>
<string name="set_behavior">Behavior</string>
<string name="set_song_mode">When a song is selected</string>
@ -109,6 +114,7 @@
<string name="set_excluded">Excluded folders</string>
<string name="set_excluded_desc">The content of excluded folders is hidden from your library</string>
<!-- TODO: Phase out android strings for in-house strings -->
<string name="set_off">Off</string>
<!-- Error Namespace | Error Labels -->
@ -173,6 +179,10 @@
<!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_disc_no">Disc %d</string>
<string name="fmt_db_pos">+%.1f dB</string>
<string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_songs_loaded">Songs loaded: %d</string>
<string name="fmt_albums_loaded">Albums loaded: %d</string>
<string name="fmt_artists_loaded">Artists loaded: %d</string>

View file

@ -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.
-->
<style name="Theme.Auxio.Dialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<style name="Theme.Auxio.Dialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item>
<item name="materialAlertDialogTitleTextStyle">@style/Widget.Auxio.Dialog.TextView</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog

View file

@ -94,6 +94,14 @@
app:key="auxio_replay_gain"
app:title="@string/set_replay_gain" />
<Preference
app:iconSpaceReserved="false"
app:key="auxio_pre_amp"
app:allowDividerBelow="false"
app:title="@string/set_pre_amp"
app:dependency="auxio_replay_gain"
app:summary="@string/set_pre_amp_desc"/>
</PreferenceCategory>
<PreferenceCategory