From d81efe168ed056912609ea95ae9efbf0ecf8ee96 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:13:49 +0000 Subject: [PATCH] playback: make Auto the default ReplayGain mode Use Auto for the preference fallback, the settings initial state and the controller's pre-preference default. Existing stored modes are unchanged. --- .../main/kotlin/com/subtracks/data/prefs/UserPreferences.kt | 2 +- .../kotlin/com/subtracks/playback/ReplayGainController.kt | 2 +- .../main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt | 2 +- .../kotlin/com/subtracks/ui/settings/SettingsViewModel.kt | 2 +- .../kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt | 4 ++-- docs/architecture.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) 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 a63a0958..5846925c 100644 --- a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt +++ b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt @@ -154,7 +154,7 @@ class UserPreferences( fun replayGainMode(): Flow = store.data.map { prefs -> - prefs[replayGainModeKey]?.let { stored -> ReplayGainMode.entries.firstOrNull { it.name == stored } } ?: ReplayGainMode.Track + prefs[replayGainModeKey]?.let { stored -> ReplayGainMode.entries.firstOrNull { it.name == stored } } ?: ReplayGainMode.Auto } suspend fun setReplayGainMode(mode: ReplayGainMode) { diff --git a/app/src/main/kotlin/com/subtracks/playback/ReplayGainController.kt b/app/src/main/kotlin/com/subtracks/playback/ReplayGainController.kt index 0cacd474..ee6352d7 100644 --- a/app/src/main/kotlin/com/subtracks/playback/ReplayGainController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/ReplayGainController.kt @@ -8,7 +8,7 @@ internal class ReplayGainController( ) { private var track = ReplayGain() private var albumContext = false - private var settings = ReplayGainSettings(ReplayGainMode.Track, 0f) + private var settings = ReplayGainSettings(ReplayGainMode.Auto, 0f) @Synchronized fun applyItem(item: MediaItem) { diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt index 65e9c21e..588518bc 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt @@ -203,7 +203,7 @@ fun SettingsScreen( verboseLogging: Boolean = false, syncMode: SyncMode = SyncMode.WifiOnly, syncInterval: Int = DEFAULT_SYNC_INTERVAL_MINUTES, - replayGainMode: ReplayGainMode = ReplayGainMode.Track, + replayGainMode: ReplayGainMode = ReplayGainMode.Auto, replayGainPreamp: Float = 0f, onSelectSource: (Long) -> Unit, onEditServer: (Long) -> Unit, diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt index 16caac8f..d9fbbbba 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt @@ -69,7 +69,7 @@ class SettingsViewModel( val replayGainMode: StateFlow = userPreferences .replayGainMode() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ReplayGainMode.Track) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ReplayGainMode.Auto) val replayGainPreamp: StateFlow = userPreferences 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 6c5bc159..a0dff94f 100644 --- a/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt @@ -210,13 +210,13 @@ class UserPreferencesTest { } @Test - fun replayGainDefaultsToTrackModeAndRoundTrips() = + fun replayGainDefaultsToAutoModeAndRoundTrips() = runTest { val file = File.createTempFile("user-prefs", ".preferences_pb").apply { delete() } val store = PreferenceDataStoreFactory.create(scope = backgroundScope) { file } val prefs = UserPreferences(store) - assertEquals(ReplayGainMode.Track, prefs.replayGainMode().first()) + assertEquals(ReplayGainMode.Auto, prefs.replayGainMode().first()) assertEquals(0f, prefs.replayGainPreampDb().first(), 0f) prefs.setReplayGainMode(ReplayGainMode.Album) diff --git a/docs/architecture.md b/docs/architecture.md index 9ad34bcc..d0b4e92e 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/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. +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 is the default and 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