diff --git a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt
index 236d6a6e..a63a0958 100644
--- a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt
+++ b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt
@@ -57,7 +57,7 @@ const val MAX_REPLAY_GAIN_PREAMP_DB = 15f
enum class SyncMode { Off, WifiOnly, Always }
-enum class ReplayGainMode { Off, Track, Album, Dynamic }
+enum class ReplayGainMode { Off, Track, Album, Auto }
data class ArtworkSeedValue(
val cacheKey: String,
diff --git a/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt b/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt
index d69798c0..a801044d 100644
--- a/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt
+++ b/app/src/main/kotlin/com/subtracks/playback/ReplayGain.kt
@@ -37,7 +37,7 @@ internal fun replayGainLinearGain(
// headroom without a peak, so a peakless track is only ever attenuated.
val trackPeak = gain.trackPeak?.takeIf { it.isFinite() && it > 0f }
val albumPeak = gain.albumPeak?.takeIf { it.isFinite() && it > 0f }
- val album = settings.mode == ReplayGainMode.Album || (settings.mode == ReplayGainMode.Dynamic && albumContext)
+ val album = settings.mode == ReplayGainMode.Album || (settings.mode == ReplayGainMode.Auto && albumContext)
val (requestedDb, peak) =
if (album) {
(gain.albumGain ?: gain.trackGain) to (albumPeak ?: trackPeak)
diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/ReplayGainControls.kt b/app/src/main/kotlin/com/subtracks/ui/settings/ReplayGainControls.kt
index bbbc8eec..434be698 100644
--- a/app/src/main/kotlin/com/subtracks/ui/settings/ReplayGainControls.kt
+++ b/app/src/main/kotlin/com/subtracks/ui/settings/ReplayGainControls.kt
@@ -32,7 +32,7 @@ internal fun replayGainModeLabel(
ReplayGainMode.Off -> resources.getString(R.string.settings_replay_gain_mode_off)
ReplayGainMode.Track -> resources.getString(R.string.settings_replay_gain_mode_track)
ReplayGainMode.Album -> resources.getString(R.string.settings_replay_gain_mode_album)
- ReplayGainMode.Dynamic -> resources.getString(R.string.settings_replay_gain_mode_dynamic)
+ ReplayGainMode.Auto -> resources.getString(R.string.settings_replay_gain_mode_auto)
}
internal fun replayGainPreampLabel(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fd59d375..9dccf365 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -83,7 +83,7 @@
Send now playing and play counts
Scrobble to server
Album gain
- Dynamic
+ Auto
Off
Track gain
Lower preamp
diff --git a/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt b/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt
index a1c846d6..6c5bc159 100644
--- a/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt
+++ b/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt
@@ -225,8 +225,8 @@ 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.setReplayGainMode(ReplayGainMode.Auto)
+ assertEquals(ReplayGainMode.Auto, prefs.replayGainMode().first())
prefs.setReplayGainPreampDb(99f)
assertEquals(MAX_REPLAY_GAIN_PREAMP_DB, prefs.replayGainPreampDb().first(), 0f)
diff --git a/app/src/test/kotlin/com/subtracks/playback/ReplayGainControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/ReplayGainControllerTest.kt
index 2c164376..d36bfdd2 100644
--- a/app/src/test/kotlin/com/subtracks/playback/ReplayGainControllerTest.kt
+++ b/app/src/test/kotlin/com/subtracks/playback/ReplayGainControllerTest.kt
@@ -39,8 +39,8 @@ class ReplayGainControllerTest {
}
@Test
- fun dynamicModeFollowsTheItemsGainContext() {
- controller.setSettings(ReplayGainSettings(ReplayGainMode.Dynamic, 0f))
+ fun autoModeFollowsTheItemsGainContext() {
+ controller.setSettings(ReplayGainSettings(ReplayGainMode.Auto, 0f))
controller.applyItem(item("a", trackGain = -6.0206f, albumGain = -1f))
assertEquals(0.5f, applied.last(), TOLERANCE)
diff --git a/app/src/test/kotlin/com/subtracks/playback/ReplayGainTest.kt b/app/src/test/kotlin/com/subtracks/playback/ReplayGainTest.kt
index 4fdafc99..f076b3d1 100644
--- a/app/src/test/kotlin/com/subtracks/playback/ReplayGainTest.kt
+++ b/app/src/test/kotlin/com/subtracks/playback/ReplayGainTest.kt
@@ -54,12 +54,12 @@ class ReplayGainTest {
}
@Test
- fun dynamicModeAppliesTheAlbumGainInAnAlbumContext() {
+ fun autoModeAppliesTheAlbumGainInAnAlbumContext() {
assertEquals(
0.5f,
replayGainLinearGain(
ReplayGain(trackGain = -1f, albumGain = -6.0206f, trackPeak = 1f, albumPeak = 1f),
- ReplayGainSettings(ReplayGainMode.Dynamic, 0f),
+ ReplayGainSettings(ReplayGainMode.Auto, 0f),
albumContext = true,
),
TOLERANCE,
@@ -67,12 +67,12 @@ class ReplayGainTest {
}
@Test
- fun dynamicModeAppliesTheTrackGainInAMixedContext() {
+ fun autoModeAppliesTheTrackGainInAMixedContext() {
assertEquals(
0.5f,
replayGainLinearGain(
ReplayGain(trackGain = -6.0206f, albumGain = -1f, trackPeak = 1f, albumPeak = 1f),
- ReplayGainSettings(ReplayGainMode.Dynamic, 0f),
+ ReplayGainSettings(ReplayGainMode.Auto, 0f),
albumContext = false,
),
TOLERANCE,
@@ -80,8 +80,8 @@ class ReplayGainTest {
}
@Test
- fun dynamicModeFallsBackInEitherContext() {
- val settings = ReplayGainSettings(ReplayGainMode.Dynamic, 0f)
+ fun autoModeFallsBackInEitherContext() {
+ val settings = ReplayGainSettings(ReplayGainMode.Auto, 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)
diff --git a/app/src/test/kotlin/com/subtracks/ui/settings/ReplayGainControlsTest.kt b/app/src/test/kotlin/com/subtracks/ui/settings/ReplayGainControlsTest.kt
index 5210d9b5..676e16b6 100644
--- a/app/src/test/kotlin/com/subtracks/ui/settings/ReplayGainControlsTest.kt
+++ b/app/src/test/kotlin/com/subtracks/ui/settings/ReplayGainControlsTest.kt
@@ -19,7 +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))
+ assertEquals("Auto", replayGainModeLabel(resources, ReplayGainMode.Auto))
}
@Test
diff --git a/docs/architecture.md b/docs/architecture.md
index 135f7d80..9ad34bcc 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -98,7 +98,7 @@ Server writes — now-playing, scrobbles and stars — go through a `ServerActio
The media notification carries a star button for the playing track. `PlaybackService` adds a `com.subtracks.STAR` session command and a `CommandButton` to the `MediaSession`, and drives its filled/unfilled icon from the playing song's Room row. That path — `MediaSession.Callback`, `SessionCommand`, `CommandButton`, `SessionResult`/`SessionError` and `MediaSession.setMediaButtonPreferences` — is annotated `@UnstableApi` in Media3 1.11.1 and is therefore opted into explicitly: when bumping Media3, re-check those signatures and the notification's button resolution. The stable `setCustomLayout` is not a substitute here, because the notification manager only refreshes on `onMediaButtonPreferencesChanged`, so the icon would not update mid-playback.
-ReplayGain is applied at the audio sink. The gain the server reports per song (`trackGain`/`albumGain` and their peaks; `baseGain` and `fallbackGain` are ignored) is synced into the song row, carried onto the media item, and applied by a `ReplayGainProcessor` in `DefaultAudioSink`'s processor chain, installed through a `RenderersFactory`. The gain is set from `AudioProcessor.flush(StreamMetadata)`, which Media3 1.11 populates with the playback `Timeline` and the current period id (`androidx/media#418`) and calls on every sink configuration — including a media-item transition at an unchanged format — so a new item's gain is in place before any of its buffers are processed. That timing is the point: `Player.Listener.onMediaItemTransition` arrives on the app thread after the playback thread has already rendered, which is too late. When bumping Media3, re-check that `flush` still carries the timeline and period, and that the chain is reached at all: float output and audio offload both bypass it, which is why float output stays off. The mode (track/album/dynamic/off) and a preamp are DataStore preferences read by `PlaybackService`, and the anti-clip cap uses the peak when the server sends one: without a peak there is no headroom to check, so a peakless track is only ever attenuated. Dynamic mode selects album gain for items whose queue entry references an album and track gain for playlists, singles and other mixed contexts, including shuffled album queues; the choice is carried on each media item because the audio thread cannot query the queue.
+ReplayGain is applied at the audio sink. The gain the server reports per song (`trackGain`/`albumGain` and their peaks; `baseGain` and `fallbackGain` are ignored) is synced into the song row, carried onto the media item, and applied by a `ReplayGainProcessor` in `DefaultAudioSink`'s processor chain, installed through a `RenderersFactory`. The gain is set from `AudioProcessor.flush(StreamMetadata)`, which Media3 1.11 populates with the playback `Timeline` and the current period id (`androidx/media#418`) and calls on every sink configuration — including a media-item transition at an unchanged format — so a new item's gain is in place before any of its buffers are processed. That timing is the point: `Player.Listener.onMediaItemTransition` arrives on the app thread after the playback thread has already rendered, which is too late. When bumping Media3, re-check that `flush` still carries the timeline and period, and that the chain is reached at all: float output and audio offload both bypass it, which is why float output stays off. The mode (track/album/auto/off) and a preamp are DataStore preferences read by `PlaybackService`, and the anti-clip cap uses the peak when the server sends one: without a peak there is no headroom to check, so a peakless track is only ever attenuated. Auto mode selects album gain for items whose queue entry references an album and track gain for playlists, singles and other mixed contexts, including shuffled album queues; the choice is carried on each media item because the audio thread cannot query the queue.
## Data model