mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
playback: add dynamic ReplayGain mode
Use album gain for items whose queue entry references an album and track gain for playlists, singles and other mixed contexts, including shuffled album queues. Carry the resolved context on each media item so the audio thread does not need to inspect the queue. Keep a track moved within an album as an album range so it stays in album context; moves out of playlists, artists, genres and other mixed contexts remain pinned song entries.
This commit is contained in:
@@ -225,6 +225,9 @@ class UserPreferencesTest {
|
||||
assertEquals(ReplayGainMode.Album, prefs.replayGainMode().first())
|
||||
assertEquals(3.5f, prefs.replayGainPreampDb().first(), 0f)
|
||||
|
||||
prefs.setReplayGainMode(ReplayGainMode.Dynamic)
|
||||
assertEquals(ReplayGainMode.Dynamic, prefs.replayGainMode().first())
|
||||
|
||||
prefs.setReplayGainPreampDb(99f)
|
||||
assertEquals(MAX_REPLAY_GAIN_PREAMP_DB, prefs.replayGainPreampDb().first(), 0f)
|
||||
|
||||
|
||||
@@ -485,6 +485,43 @@ class QueueRepositoryTest {
|
||||
repository.move(repository.snapshot(), from = 2, to = 1)
|
||||
|
||||
assertEquals(listOf("s1", "s5", "s4", "s2", "s3"), resolveAll(repository.snapshot()))
|
||||
val (entry, _) = repository.snapshot().locate(1)!!
|
||||
assertEquals(QueueKind.Song, entry.kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingAQueuedAlbumTrackKeepsItsAlbumEntry() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
val snapshot = repository.snapshotAfter(listOf(repository.albumEntry(1, "al1")))
|
||||
repository.addUpNext(snapshot, currentPosition = 0, entry = repository.albumEntry(1, "al2"), playNext = false)
|
||||
|
||||
repository.move(repository.snapshot(), from = 2, to = 1)
|
||||
|
||||
val (entry, _) = repository.snapshot().locate(1)!!
|
||||
assertEquals(QueueKind.Album, entry.kind)
|
||||
assertEquals("al2", entry.refId)
|
||||
assertEquals(1L, entry.rangeStart)
|
||||
assertEquals(1L, entry.rangeEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingAPlaylistTrackKeepsItAPinnedSong() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
db.libraryDao().upsertPlaylistSongs(
|
||||
listOf(
|
||||
PlaylistSong(1, "p1", "s1", 0),
|
||||
PlaylistSong(1, "p1", "s4", 1),
|
||||
),
|
||||
)
|
||||
repository.snapshotAfter(listOf(repository.playlistEntry(1, "p1")))
|
||||
|
||||
repository.move(repository.snapshot(), from = 0, to = 1)
|
||||
|
||||
val (entry, _) = repository.snapshot().locate(1)!!
|
||||
assertEquals(QueueKind.Song, entry.kind)
|
||||
assertEquals("s1", entry.refId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -853,11 +890,30 @@ class QueueRepositoryTest {
|
||||
repository.move(repository.snapshot(), from = 0, to = 2)
|
||||
|
||||
val (entry, _) = repository.snapshot().locate(2)!!
|
||||
assertEquals(QueueKind.Song, entry.kind)
|
||||
assertEquals("s1", entry.refId)
|
||||
assertEquals(QueueKind.Album, entry.kind)
|
||||
assertEquals("al1", entry.refId)
|
||||
assertEquals(0L, entry.rangeStart)
|
||||
assertEquals(0L, entry.rangeEnd)
|
||||
assertEquals("First Album", repository.sourceName(entry.kind, entry.sourceId, entry.refId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingAnAlbumTrackBackCompactsTheAlbumEntry() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
repository.replace(listOf(repository.albumEntry(1, "al1")))
|
||||
|
||||
repository.move(repository.snapshot(), from = 0, to = 2)
|
||||
repository.move(repository.snapshot(), from = 2, to = 0)
|
||||
|
||||
val snapshot = repository.snapshot()
|
||||
assertEquals(listOf("s1", "s2", "s3"), resolveAll(snapshot))
|
||||
val entry = snapshot.entries.single().entry
|
||||
assertEquals(QueueKind.Album, entry.kind)
|
||||
assertEquals(0L, entry.rangeStart)
|
||||
assertEquals(2L, entry.rangeEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAlbumSplitByRemovalIsStillLabeledByItsAlbum() =
|
||||
runTest {
|
||||
|
||||
@@ -70,6 +70,20 @@ class MediaItemTest {
|
||||
assertEquals(gain, item.replayGain())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theItemCarriesItsGainContext() {
|
||||
val item =
|
||||
queueMediaItem(
|
||||
song.copy(albumContext = true),
|
||||
local = null,
|
||||
stream = "http://server/stream?s=s1",
|
||||
transcode = false,
|
||||
)
|
||||
|
||||
assertEquals(true, item.albumContext())
|
||||
assertEquals(false, queueMediaItem(song, local = null, stream = "http://server/stream?s=s1", transcode = false).albumContext())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anItemWithoutGainReadsBackAsNoGain() {
|
||||
val item = queueMediaItem(song, local = null, stream = "http://server/stream?s=s1", transcode = false)
|
||||
|
||||
@@ -479,6 +479,67 @@ class PlaybackControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumTracksAndQueuedSinglesCarryDifferentGainContexts() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
seedSong("x1", "al2")
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.items.any { it.id == "s1" } }
|
||||
assertTrue(handle.items.first { it.id == "s1" }.albumContext)
|
||||
|
||||
controller.playNext(1, QueueKind.Song, "x1")
|
||||
await { handle.items.any { it.id == "x1" } }
|
||||
|
||||
assertFalse(handle.items.first { it.id == "x1" }.albumContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun queuedAlbumsCarryAlbumGain() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
seedSong("x1", "al2")
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.items.any { it.id == "s1" } }
|
||||
|
||||
controller.playNext(1, QueueKind.Album, "al2")
|
||||
await { handle.items.any { it.id == "x1" } }
|
||||
|
||||
assertTrue(handle.items.first { it.id == "x1" }.albumContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingAnAlbumTrackKeepsItsAlbumGainContext() {
|
||||
seedAlbum(4, sourceId = 1)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.items.any { it.id == "s1" } }
|
||||
|
||||
runBlocking { controller.move(0, 2) }
|
||||
await { controller.state.value.position == 2L }
|
||||
|
||||
assertTrue(handle.items.first { it.id == "s1" }.albumContext)
|
||||
val (entry, _) = runBlocking { queues.snapshot().locate(2)!! }
|
||||
assertEquals(QueueKind.Album, entry.kind)
|
||||
assertEquals("al1", entry.refId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun undoingARemovalRestoresTheTracksAlbumGainContext() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { handle.items.any { it.id == "s2" } }
|
||||
|
||||
runBlocking { controller.removeAt(1) }
|
||||
await { handle.items.none { it.id == "s2" } }
|
||||
|
||||
runBlocking { controller.undo() }
|
||||
await { handle.items.any { it.id == "s2" } }
|
||||
|
||||
assertTrue(handle.items.first { it.id == "s2" }.albumContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingAContextTrackWhileAQueuedTrackPlaysKeepsTheQueue() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
@@ -1268,6 +1329,25 @@ class PlaybackControllerTest {
|
||||
@Test
|
||||
fun movingThePlayingContextTrackStillResolvesAnAlbumTitle() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
runBlocking {
|
||||
db.libraryDao().upsertAlbums(
|
||||
listOf(
|
||||
Album(
|
||||
sourceId = 1,
|
||||
id = "al1",
|
||||
artistId = "ar1",
|
||||
name = "Album",
|
||||
albumArtist = "Artist",
|
||||
created = 0,
|
||||
coverArt = null,
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 3,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
controller.playAlbum(1, "al1", 1)
|
||||
await {
|
||||
|
||||
@@ -35,6 +35,28 @@ class QueueItemTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun carriesTheAlbumsGainContextWhenAsked() {
|
||||
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,
|
||||
)
|
||||
|
||||
val item = AlbumSongItem(song, coverArt = null).toQueueItem(albumContext = true)
|
||||
|
||||
assertEquals(true, item.albumContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSongWithoutGainCarriesNoGain() {
|
||||
val item =
|
||||
|
||||
@@ -38,6 +38,17 @@ class ReplayGainControllerTest {
|
||||
assertEquals(1f, applied.last(), TOLERANCE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dynamicModeFollowsTheItemsGainContext() {
|
||||
controller.setSettings(ReplayGainSettings(ReplayGainMode.Dynamic, 0f))
|
||||
|
||||
controller.applyItem(item("a", trackGain = -6.0206f, albumGain = -1f))
|
||||
assertEquals(0.5f, applied.last(), TOLERANCE)
|
||||
|
||||
controller.applyItem(item("b", trackGain = -1f, albumGain = -6.0206f, albumContext = true))
|
||||
assertEquals(0.5f, applied.last(), TOLERANCE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anItemWithoutGainPlaysAtUnity() {
|
||||
controller.setSettings(ReplayGainSettings(ReplayGainMode.Track, 0f))
|
||||
@@ -65,6 +76,8 @@ class ReplayGainControllerTest {
|
||||
id: String,
|
||||
trackGain: Float?,
|
||||
trackPeak: Float? = null,
|
||||
albumGain: Float? = null,
|
||||
albumContext: Boolean = false,
|
||||
) = queueMediaItem(
|
||||
QueueItem(
|
||||
id = id,
|
||||
@@ -72,7 +85,8 @@ class ReplayGainControllerTest {
|
||||
artist = null,
|
||||
album = null,
|
||||
coverArtId = null,
|
||||
replayGain = ReplayGain(trackGain = trackGain, trackPeak = trackPeak),
|
||||
replayGain = ReplayGain(trackGain = trackGain, albumGain = albumGain, trackPeak = trackPeak),
|
||||
albumContext = albumContext,
|
||||
),
|
||||
local = null,
|
||||
stream = "http://server/$id",
|
||||
|
||||
@@ -53,6 +53,40 @@ class ReplayGainTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dynamicModeAppliesTheAlbumGainInAnAlbumContext() {
|
||||
assertEquals(
|
||||
0.5f,
|
||||
replayGainLinearGain(
|
||||
ReplayGain(trackGain = -1f, albumGain = -6.0206f, trackPeak = 1f, albumPeak = 1f),
|
||||
ReplayGainSettings(ReplayGainMode.Dynamic, 0f),
|
||||
albumContext = true,
|
||||
),
|
||||
TOLERANCE,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dynamicModeAppliesTheTrackGainInAMixedContext() {
|
||||
assertEquals(
|
||||
0.5f,
|
||||
replayGainLinearGain(
|
||||
ReplayGain(trackGain = -6.0206f, albumGain = -1f, trackPeak = 1f, albumPeak = 1f),
|
||||
ReplayGainSettings(ReplayGainMode.Dynamic, 0f),
|
||||
albumContext = false,
|
||||
),
|
||||
TOLERANCE,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dynamicModeFallsBackInEitherContext() {
|
||||
val settings = ReplayGainSettings(ReplayGainMode.Dynamic, 0f)
|
||||
|
||||
assertEquals(0.5f, replayGainLinearGain(ReplayGain(trackGain = -6.0206f), settings, albumContext = true), TOLERANCE)
|
||||
assertEquals(0.5f, replayGainLinearGain(ReplayGain(albumGain = -6.0206f), settings, albumContext = false), TOLERANCE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offAppliesUnityGain() {
|
||||
assertEquals(
|
||||
|
||||
@@ -19,6 +19,7 @@ class ReplayGainControlsTest {
|
||||
assertEquals("Off", replayGainModeLabel(resources, ReplayGainMode.Off))
|
||||
assertEquals("Track gain", replayGainModeLabel(resources, ReplayGainMode.Track))
|
||||
assertEquals("Album gain", replayGainModeLabel(resources, ReplayGainMode.Album))
|
||||
assertEquals("Dynamic", replayGainModeLabel(resources, ReplayGainMode.Dynamic))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user