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
_shouldRecreateTabs.value = true
}

View file

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

View file

@ -15,7 +15,7 @@
* 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
@ -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
* BroadcastReceiver in the manifest that actually does nothing. Any broadcast by apps should be
* routed by the media session when the service exists.
* @author OxygenCobalt
*/
class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {}

View file

@ -52,6 +52,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
init {
player.addListener(this)
playbackManager.addCallback(this)
settingsManager.addCallback(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)
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
.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
// 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 ---
override fun onShowCoverUpdate(showCovers: Boolean) {
updateMediaMetadata(playbackManager.song)
}
override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
override fun onCoverSettingsChanged() {
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.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
/**
* 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
* inconsistency when the foreground state is started.
* inconsistency derived from callback order.
* @author OxygenCobalt
*/
@SuppressLint("RestrictedApi")
@ -108,6 +109,7 @@ class NotificationComponent(
object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) {
setLargeIcon(bitmap)
setLargeIcon(null)
callback.onNotificationChanged(this@NotificationComponent)
}
})

View file

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

View file

@ -61,7 +61,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
preferenceManager.onDisplayPreferenceDialogListener = this
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 {
clipToPadding = false

View file

@ -24,7 +24,8 @@ 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.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.Sort
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.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) */
val songPlaybackMode: PlaybackMode
get() =
@ -186,6 +201,7 @@ class SettingsManager private constructor(context: Context) :
Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: Sort.ByDisc(true)
// Correct legacy album sort modes to Disc
if (sort is Sort.ByName) {
sort = Sort.ByDisc(sort.isAscending)
}
@ -239,12 +255,11 @@ class SettingsManager private constructor(context: Context) :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
KEY_USE_ALT_NOTIFICATION_ACTION ->
callbacks.forEach { it.onNotifActionUpdate(useAltNotifAction) }
KEY_SHOW_COVERS -> callbacks.forEach { it.onShowCoverUpdate(showCovers) }
KEY_QUALITY_COVERS -> callbacks.forEach { it.onQualityCoverUpdate(useQualityCovers) }
KEY_LIB_TABS -> callbacks.forEach { it.onLibTabsUpdate(libTabs) }
KEY_REPLAY_GAIN -> callbacks.forEach { it.onReplayGainUpdate(replayGainMode) }
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 ->
callbacks.forEach { it.onReplayGainSettingsChanged() }
}
}
@ -254,11 +269,10 @@ class SettingsManager private constructor(context: Context) :
* context.
*/
interface Callback {
fun onLibTabsUpdate(libTabs: Array<Tab>) {}
fun onNotifActionUpdate(useAltAction: Boolean) {}
fun onShowCoverUpdate(showCovers: Boolean) {}
fun onQualityCoverUpdate(doQualityCovers: Boolean) {}
fun onReplayGainUpdate(mode: ReplayGainMode) {}
fun onLibraryChanged() {}
fun onNotifSettingsChanged() {}
fun onCoverSettingsChanged() {}
fun onReplayGainSettingsChanged() {}
}
companion object {
@ -276,6 +290,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_SONG_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
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:
* - 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.
* - Addition of an indicator, which is a dot that can denote when a button is active. This is
* also useful for the playback buttons, as at times highlighting them is not enough to
* differentiate them.
* - Addition of an indicator, which is a dot that can denote when a button is active. This is also
* useful for the playback buttons, as at times highlighting them is not enough to differentiate
* them.
* @author OxygenCobalt
*/
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
* images.
*
* Default behavior includes the addition of a tonal background, automatic sizing of icons to
* half of the view size, and corner radius application depending on user preference.
* Default behavior includes the addition of a tonal background, automatic sizing of icons to half
* of the view size, and corner radius application depending on user preference.
* @author OxygenCobalt
*/
class StyledImageView

View file

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