playback: add framework for handling pre-amp

Implement an internal setting for a ReplayGain pre-amp setting.

Pre-amp is a lot like above reference volume regarding Auxio's
ReplayGain implementation, where I want to implement it in order
to allow ReplayGain to graduate from being labeled "experimental".

No UI frontend has been implemented just yet.
This commit is contained in:
OxygenCobalt 2022-05-21 16:34:20 -06:00
parent 2d7dbd19cd
commit 519de0e1d5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 77 additions and 61 deletions

View file

@ -145,7 +145,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
} }
} }
override fun onLibTabsUpdate(libTabs: Array<Tab>) { override fun onLibraryChanged() {
tabs = visibleTabs tabs = visibleTabs
_shouldRecreateTabs.value = true _shouldRecreateTabs.value = true
} }

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.system package org.oxycblt.auxio.playback.replaygain
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.audio.AudioProcessor import com.google.android.exoplayer2.audio.AudioProcessor
@ -23,6 +23,7 @@ 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.vorbis.VorbisComment import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.pow import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -39,8 +40,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* manipulates the bitstream itself to modify the volume, which allows the use of positive * manipulates the bitstream itself to modify the volume, which allows the use of positive
* ReplayGain values. * ReplayGain values.
* *
* TODO: Pre-amp values
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ReplayGainAudioProcessor : BaseAudioProcessor() { class ReplayGainAudioProcessor : BaseAudioProcessor() {
@ -64,23 +63,21 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
* Vanilla Music's implementation. * Vanilla Music's implementation.
*/ */
fun applyReplayGain(metadata: Metadata?) { fun applyReplayGain(metadata: Metadata?) {
if (metadata == null || settingsManager.replayGainMode == ReplayGainMode.OFF) { if (settingsManager.replayGainMode == ReplayGainMode.OFF) {
logW( logW("ReplayGain not enabled")
"Not applying replaygain [" +
"metadata: ${metadata != null}, " +
"enabled: ${settingsManager.replayGainMode == ReplayGainMode.OFF}]")
volume = 1f volume = 1f
return return
} }
val gain = parseReplayGain(metadata) val gain = metadata?.let(::parseReplayGain)
val preAmp = settingsManager.replayGainPreAmp
val adjust = val adjust =
if (gain != null) { if (gain != null) {
// ReplayGain is configurable, so determine what to do based off of the mode. // ReplayGain is configurable, so determine what to do based off of the mode.
val useAlbumGain = val useAlbumGain =
when (settingsManager.replayGainMode) { when (settingsManager.replayGainMode) {
ReplayGainMode.OFF -> error("Unreachable") ReplayGainMode.OFF -> throw UnsupportedOperationException()
// User wants track gain to be preferred. Default to album gain only if // User wants track gain to be preferred. Default to album gain only if
// there is no track gain. // there is no track gain.
@ -96,18 +93,25 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
playbackManager.song?.album?.id == playbackManager.parent?.id playbackManager.song?.album?.id == playbackManager.parent?.id
} }
if (useAlbumGain) { val resolvedGain =
logD("Using album gain") if (useAlbumGain) {
gain.album logD("Using album gain")
} else { gain.album
logD("Using track gain") } else {
gain.track logD("Using track gain")
} gain.track
}
// Apply the "With tags" adjustment
resolvedGain + preAmp.with
} else { } else {
// No gain tags were present // No gain tags were present, just apply the adjustment without tags.
0f logD("No ReplayGain tags present ")
preAmp.without
} }
logD("Applying ReplayGain adjustment ${adjust}db")
// Final adjustment along the volume curve. // Final adjustment along the volume curve.
volume = 10f.pow(adjust / 20f) volume = 10f.pow(adjust / 20f)
} }

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.system package org.oxycblt.auxio.playback.replaygain
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
@ -42,3 +42,9 @@ enum class ReplayGainMode {
} }
} }
} }
/** Represents the ReplayGain pre-amp */
data class ReplayGainPreAmp(
val with: Float,
val without: Float,
)

View file

@ -29,7 +29,6 @@ import android.content.Intent
* KitKat don't break! To prevent Auxio from not showing up at all in these apps, we declare a * KitKat don't break! To prevent Auxio from not showing up at all in these apps, we declare a
* BroadcastReceiver in the manifest that actually does nothing. Any broadcast by apps should be * BroadcastReceiver in the manifest that actually does nothing. Any broadcast by apps should be
* routed by the media session when the service exists. * routed by the media session when the service exists.
* @author OxygenCobalt
*/ */
class MediaButtonReceiver : BroadcastReceiver() { class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {} override fun onReceive(context: Context, intent: Intent) {}

View file

@ -52,6 +52,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
init { init {
player.addListener(this) player.addListener(this)
playbackManager.addCallback(this) playbackManager.addCallback(this)
settingsManager.addCallback(this)
mediaSession.setCallback(this) mediaSession.setCallback(this)
} }
@ -101,9 +102,6 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L)
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString()) .putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
.putText(
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
song.album.albumCoverUri.toString())
// Normally, android expects one to provide a URI to the metadata instance instead of // Normally, android expects one to provide a URI to the metadata instance instead of
// a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot // a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot
@ -145,11 +143,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
// --- SETTINGSMANAGER CALLBACKS --- // --- SETTINGSMANAGER CALLBACKS ---
override fun onShowCoverUpdate(showCovers: Boolean) { override fun onCoverSettingsChanged() {
updateMediaMetadata(playbackManager.song)
}
override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
updateMediaMetadata(playbackManager.song) updateMediaMetadata(playbackManager.song)
} }

View file

@ -35,13 +35,14 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newBroadcastIntent
import org.oxycblt.auxio.util.newMainIntent import org.oxycblt.auxio.util.newMainIntent
/** /**
* The unified notification for [PlaybackService]. Due to the nature of how this notification is * The unified notification for [PlaybackService]. Due to the nature of how this notification is
* used, it is *not self-sufficient*. Updates have to be delivered manually, as to prevent state * used, it is *not self-sufficient*. Updates have to be delivered manually, as to prevent state
* inconsistency when the foreground state is started. * inconsistency derived from callback order.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@ -108,6 +109,7 @@ class NotificationComponent(
object : BitmapProvider.Target { object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) { override fun onCompleted(bitmap: Bitmap?) {
setLargeIcon(bitmap) setLargeIcon(bitmap)
setLargeIcon(null)
callback.onNotificationChanged(this@NotificationComponent) callback.onNotificationChanged(this@NotificationComponent)
} }
}) })

View file

@ -47,6 +47,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
@ -291,24 +292,18 @@ class PlaybackService :
// --- SETTINGSMANAGER OVERRIDES --- // --- SETTINGSMANAGER OVERRIDES ---
override fun onReplayGainUpdate(mode: ReplayGainMode) { override fun onReplayGainSettingsChanged() {
onTracksInfoChanged(player.currentTracksInfo) onTracksInfoChanged(player.currentTracksInfo)
} }
override fun onShowCoverUpdate(showCovers: Boolean) { override fun onCoverSettingsChanged() {
playbackManager.song?.let { song -> playbackManager.song?.let { song ->
notificationComponent.updateMetadata(song, playbackManager.parent) notificationComponent.updateMetadata(song, playbackManager.parent)
} }
} }
override fun onQualityCoverUpdate(doQualityCovers: Boolean) { override fun onNotifSettingsChanged() {
playbackManager.song?.let { song -> if (settingsManager.useAltNotifAction) {
notificationComponent.updateMetadata(song, playbackManager.parent)
}
}
override fun onNotifActionUpdate(useAltAction: Boolean) {
if (useAltAction) {
onShuffledChanged(playbackManager.isShuffled) onShuffledChanged(playbackManager.isShuffled)
} else { } else {
onRepeatChanged(playbackManager.repeatMode) onRepeatChanged(playbackManager.repeatMode)

View file

@ -61,7 +61,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
preferenceManager.onDisplayPreferenceDialogListener = this preferenceManager.onDisplayPreferenceDialogListener = this
preferenceScreen.children.forEach(::recursivelyHandlePreference) preferenceScreen.children.forEach(::recursivelyHandlePreference)
// Make the RecycleBiew edge-to-edge capable // Make the RecycleView edge-to-edge capable
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply { view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
clipToPadding = false clipToPadding = false

View file

@ -24,7 +24,8 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.system.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.ui.accent.Accent
@ -104,6 +105,20 @@ class SettingsManager private constructor(context: Context) :
ReplayGainMode.fromIntCode(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE)) ReplayGainMode.fromIntCode(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE))
?: ReplayGainMode.OFF ?: ReplayGainMode.OFF
/** The current ReplayGain pre-amp configuration */
var replayGainPreAmp: ReplayGainPreAmp
get() =
ReplayGainPreAmp(
prefs.getFloat(KEY_REPLAY_GAIN_PRE_AMP_WITH, 0f),
prefs.getFloat(KEY_REPLAY_GAIN_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)
apply()
}
}
/** What queue to create when a song is selected (ex. From All Songs or Search) */ /** What queue to create when a song is selected (ex. From All Songs or Search) */
val songPlaybackMode: PlaybackMode val songPlaybackMode: PlaybackMode
get() = get() =
@ -186,6 +201,7 @@ class SettingsManager private constructor(context: Context) :
Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: Sort.ByDisc(true) ?: Sort.ByDisc(true)
// Correct legacy album sort modes to Disc
if (sort is Sort.ByName) { if (sort is Sort.ByName) {
sort = Sort.ByDisc(sort.isAscending) sort = Sort.ByDisc(sort.isAscending)
} }
@ -239,12 +255,11 @@ class SettingsManager private constructor(context: Context) :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
KEY_USE_ALT_NOTIFICATION_ACTION -> KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { it.onNotifSettingsChanged() }
callbacks.forEach { it.onNotifActionUpdate(useAltNotifAction) } KEY_SHOW_COVERS, KEY_QUALITY_COVERS -> callbacks.forEach { it.onCoverSettingsChanged() }
KEY_SHOW_COVERS -> callbacks.forEach { it.onShowCoverUpdate(showCovers) } KEY_LIB_TABS -> callbacks.forEach { it.onLibraryChanged() }
KEY_QUALITY_COVERS -> callbacks.forEach { it.onQualityCoverUpdate(useQualityCovers) } KEY_REPLAY_GAIN, KEY_REPLAY_GAIN_PRE_AMP_WITH, KEY_REPLAY_GAIN_PRE_AMP_WITHOUT ->
KEY_LIB_TABS -> callbacks.forEach { it.onLibTabsUpdate(libTabs) } callbacks.forEach { it.onReplayGainSettingsChanged() }
KEY_REPLAY_GAIN -> callbacks.forEach { it.onReplayGainUpdate(replayGainMode) }
} }
} }
@ -254,11 +269,10 @@ class SettingsManager private constructor(context: Context) :
* context. * context.
*/ */
interface Callback { interface Callback {
fun onLibTabsUpdate(libTabs: Array<Tab>) {} fun onLibraryChanged() {}
fun onNotifActionUpdate(useAltAction: Boolean) {} fun onNotifSettingsChanged() {}
fun onShowCoverUpdate(showCovers: Boolean) {} fun onCoverSettingsChanged() {}
fun onQualityCoverUpdate(doQualityCovers: Boolean) {} fun onReplayGainSettingsChanged() {}
fun onReplayGainUpdate(mode: ReplayGainMode) {}
} }
companion object { companion object {
@ -276,6 +290,9 @@ class SettingsManager private constructor(context: Context) :
const val KEY_HEADSET_AUTOPLAY = "auxio_headset_autoplay" const val KEY_HEADSET_AUTOPLAY = "auxio_headset_autoplay"
const val KEY_REPLAY_GAIN = "auxio_replay_gain" 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_SONG_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" const val KEY_SONG_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
const val KEY_KEEP_SHUFFLE = "KEY_KEEP_SHUFFLE" const val KEY_KEEP_SHUFFLE = "KEY_KEEP_SHUFFLE"

View file

@ -35,9 +35,9 @@ import org.oxycblt.auxio.util.getDrawableSafe
* More specifically, this class add two features: * More specifically, this class add two features:
* - Specification of the icon size. This is to accommodate the playback buttons, which tend to be * - Specification of the icon size. This is to accommodate the playback buttons, which tend to be
* larger as by default the playback icons look terrible with the gobs of whitespace everywhere. * larger as by default the playback icons look terrible with the gobs of whitespace everywhere.
* - Addition of an indicator, which is a dot that can denote when a button is active. This is * - Addition of an indicator, which is a dot that can denote when a button is active. This is also
* also useful for the playback buttons, as at times highlighting them is not enough to * useful for the playback buttons, as at times highlighting them is not enough to differentiate
* differentiate them. * them.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class StyledImageButton class StyledImageButton

View file

@ -45,8 +45,8 @@ import org.oxycblt.auxio.util.getDrawableSafe
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding * An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
* images. * images.
* *
* Default behavior includes the addition of a tonal background, automatic sizing of icons to * Default behavior includes the addition of a tonal background, automatic sizing of icons to half
* half of the view size, and corner radius application depending on user preference. * of the view size, and corner radius application depending on user preference.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class StyledImageView class StyledImageView

View file

@ -131,8 +131,7 @@ class WidgetComponent(private val context: Context) :
override fun onPlayingChanged(isPlaying: Boolean) = update() override fun onPlayingChanged(isPlaying: Boolean) = update()
override fun onShuffledChanged(isShuffled: Boolean) = update() override fun onShuffledChanged(isShuffled: Boolean) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onShowCoverUpdate(showCovers: Boolean) = update() override fun onCoverSettingsChanged() = update()
override fun onQualityCoverUpdate(doQualityCovers: Boolean) = update()
/* /*
* An immutable condensed variant of the current playback state, used so that PlaybackStateManager * An immutable condensed variant of the current playback state, used so that PlaybackStateManager