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:
parent
2d7dbd19cd
commit
519de0e1d5
12 changed files with 77 additions and 61 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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) {}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue