diff --git a/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt b/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt index 0fe590f0..abc7b0b5 100644 --- a/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt +++ b/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt @@ -234,11 +234,37 @@ class Media3PlayerHandle( mediaItem.mediaMetadata.extras ?.takeIf { it.containsKey(EXTRA_DURATION_MS) } ?.getLong(EXTRA_DURATION_MS), + replayGain = mediaItem.replayGain(), ) } private const val EXTRA_COVER_ART_ID = "coverArtId" private const val EXTRA_DURATION_MS = "durationMs" +private const val EXTRA_REPLAY_GAIN_TRACK_GAIN = "replayGainTrackGain" +private const val EXTRA_REPLAY_GAIN_ALBUM_GAIN = "replayGainAlbumGain" +private const val EXTRA_REPLAY_GAIN_TRACK_PEAK = "replayGainTrackPeak" +private const val EXTRA_REPLAY_GAIN_ALBUM_PEAK = "replayGainAlbumPeak" + +// The gain rides on the MediaItem so PlaybackService can apply it the moment the item becomes +// current, rather than after an asynchronous round trip to the library. +internal fun MediaItem?.replayGain(): ReplayGain { + val extras = this?.mediaMetadata?.extras ?: return ReplayGain() + return ReplayGain( + trackGain = extras.gain(EXTRA_REPLAY_GAIN_TRACK_GAIN), + albumGain = extras.gain(EXTRA_REPLAY_GAIN_ALBUM_GAIN), + trackPeak = extras.gain(EXTRA_REPLAY_GAIN_TRACK_PEAK), + albumPeak = extras.gain(EXTRA_REPLAY_GAIN_ALBUM_PEAK), + ) +} + +private fun Bundle.putGain( + key: String, + value: Float?, +) { + if (value != null) putFloat(key, value) +} + +private fun Bundle.gain(key: String): Float? = if (containsKey(key)) getFloat(key) else null internal fun queueMediaItem( item: QueueItem, @@ -260,6 +286,10 @@ internal fun queueMediaItem( Bundle().apply { putString(EXTRA_COVER_ART_ID, item.coverArtId) item.durationMs?.let { putLong(EXTRA_DURATION_MS, it) } + putGain(EXTRA_REPLAY_GAIN_TRACK_GAIN, item.replayGain.trackGain) + putGain(EXTRA_REPLAY_GAIN_ALBUM_GAIN, item.replayGain.albumGain) + putGain(EXTRA_REPLAY_GAIN_TRACK_PEAK, item.replayGain.trackPeak) + putGain(EXTRA_REPLAY_GAIN_ALBUM_PEAK, item.replayGain.albumPeak) }, ).build() val builder = diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index 12b0ef98..e306f8ef 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -34,6 +34,7 @@ data class QueueItem( val album: String?, val coverArtId: String?, val durationMs: Long? = null, + val replayGain: ReplayGain = ReplayGain(), ) data class QueueContext( diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt index ab0f6f00..12fcf237 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt @@ -140,6 +140,7 @@ class PlaybackService : MediaSessionService() { reason: Int, ) { observeCurrentSong(mediaItem?.mediaId) + replayGain.setTrack(mediaItem?.replayGain()) } }, ) @@ -181,7 +182,6 @@ class PlaybackService : MediaSessionService() { val library = library ?: return if (songId == null) { session?.setMediaButtonPreferences(emptyList()) - replayGain.setTrack(null) return } session?.setMediaButtonPreferences(listOf(starButton(false))) @@ -196,7 +196,6 @@ class PlaybackService : MediaSessionService() { if (currentSongId != songId) return@collect starred = song?.starred != null session?.setMediaButtonPreferences(listOf(starButton(starred))) - replayGain.setTrack(song?.replayGain()) } } } diff --git a/app/src/main/kotlin/com/subtracks/playback/QueueItem.kt b/app/src/main/kotlin/com/subtracks/playback/QueueItem.kt index 4691f83c..b72fba25 100644 --- a/app/src/main/kotlin/com/subtracks/playback/QueueItem.kt +++ b/app/src/main/kotlin/com/subtracks/playback/QueueItem.kt @@ -10,4 +10,5 @@ fun SongItem.toQueueItem() = album = song.album, coverArtId = coverArt, durationMs = song.duration?.let { it * 1000 }, + replayGain = song.replayGain(), ) diff --git a/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt b/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt index c8139f5b..cbb6d780 100644 --- a/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt +++ b/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt @@ -6,7 +6,7 @@ import kotlin.math.log10 import kotlin.math.min import kotlin.math.pow -internal data class ReplayGain( +data class ReplayGain( val trackGain: Float? = null, val albumGain: Float? = null, val trackPeak: Float? = null, diff --git a/app/src/test/kotlin/com/subtracks/playback/MediaItemTest.kt b/app/src/test/kotlin/com/subtracks/playback/MediaItemTest.kt index 585e059f..731bf948 100644 --- a/app/src/test/kotlin/com/subtracks/playback/MediaItemTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/MediaItemTest.kt @@ -54,4 +54,26 @@ class MediaItemTest { assertEquals("Song", item.mediaMetadata.title.toString()) assertEquals(CoverArtArtwork.uri("al1"), item.mediaMetadata.artworkUri) } + + @Test + fun theItemCarriesTheReplayGain() { + val gain = ReplayGain(trackGain = -3.2f, albumGain = -2.1f, trackPeak = 0.98f, albumPeak = 0.99f) + + val item = + queueMediaItem( + song.copy(replayGain = gain), + local = null, + stream = "http://server/stream?s=s1", + transcode = false, + ) + + assertEquals(gain, item.replayGain()) + } + + @Test + fun anItemWithoutGainReadsBackAsNoGain() { + val item = queueMediaItem(song, local = null, stream = "http://server/stream?s=s1", transcode = false) + + assertEquals(ReplayGain(), item.replayGain()) + } } diff --git a/app/src/test/kotlin/com/subtracks/playback/QueueItemTest.kt b/app/src/test/kotlin/com/subtracks/playback/QueueItemTest.kt new file mode 100644 index 00000000..7f2e96a0 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/playback/QueueItemTest.kt @@ -0,0 +1,60 @@ +package com.subtracks.playback + +import com.subtracks.data.model.AlbumSongItem +import com.subtracks.data.model.Song +import org.junit.Assert.assertEquals +import org.junit.Test + +class QueueItemTest { + @Test + fun carriesTheSongsReplayGain() { + val song = + Song( + sourceId = 1, + id = "s1", + albumId = "al1", + artistId = "ar1", + title = "Song", + album = "Album", + artist = "Artist", + duration = 100, + track = 1, + disc = 1, + starred = null, + trackGain = -3.2f, + albumGain = -2.1f, + trackPeak = 0.98f, + albumPeak = 0.99f, + ) + + val item = AlbumSongItem(song, coverArt = "al1").toQueueItem() + + assertEquals( + ReplayGain(trackGain = -3.2f, albumGain = -2.1f, trackPeak = 0.98f, albumPeak = 0.99f), + item.replayGain, + ) + } + + @Test + fun aSongWithoutGainCarriesNoGain() { + val item = + AlbumSongItem( + Song( + sourceId = 1, + id = "s1", + albumId = "al1", + artistId = "ar1", + title = "Song", + album = "Album", + artist = "Artist", + duration = 100, + track = 1, + disc = 1, + starred = null, + ), + coverArt = null, + ).toQueueItem() + + assertEquals(ReplayGain(), item.replayGain) + } +}