diff --git a/src/locales/de.ts b/src/locales/de.ts index 904be52f..80c9aaa1 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -716,6 +716,8 @@ export const deTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Lautstärke mit ReplayGain-Metadaten normalisieren', replayGainMode: 'Modus', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Wählt Album-Gain, wenn benachbarte Tracks in der Warteschlange vom selben Album sind, sonst Track-Gain.', replayGainTrack: 'Track', replayGainAlbum: 'Album', replayGainPreGain: 'Pre-Gain (getaggte Dateien)', diff --git a/src/locales/en.ts b/src/locales/en.ts index 3ac27695..824a7185 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -718,6 +718,8 @@ export const enTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Normalize track volume using ReplayGain metadata', replayGainMode: 'Mode', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Picks album gain when neighbouring queue tracks are from the same album, otherwise track gain.', replayGainTrack: 'Track', replayGainAlbum: 'Album', replayGainPreGain: 'Pre-Gain (tagged files)', diff --git a/src/locales/es.ts b/src/locales/es.ts index c07cc962..57c59e77 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -709,6 +709,8 @@ export const esTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Normalizar volumen de pistas usando metadatos ReplayGain', replayGainMode: 'Modo', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Usa la ganancia del álbum cuando las pistas adyacentes en la cola son del mismo álbum, si no la ganancia de pista.', replayGainTrack: 'Pista', replayGainAlbum: 'Álbum', replayGainPreGain: 'Pre-Ganancia (archivos etiquetados)', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 375b08a8..582696ee 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -704,6 +704,8 @@ export const frTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Normaliser le volume des pistes avec les métadonnées ReplayGain', replayGainMode: 'Mode', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Utilise le gain d\'album quand les pistes voisines de la file proviennent du même album, sinon le gain de piste.', replayGainTrack: 'Piste', replayGainAlbum: 'Album', replayGainPreGain: 'Pré-Gain (fichiers taggés)', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 22fff46a..21c03df5 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -703,6 +703,8 @@ export const nbTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Normaliser sporvolumet ved hjelp av ReplayGain-metadata', replayGainMode: 'Modus', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Bruker albumforsterkning når nabospor i køen er fra samme album, ellers sporforsterkning.', replayGainTrack: 'Spor', replayGainAlbum: 'Album', replayGainPreGain: 'Pre-Gain (taggede filer)', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 9d857322..c4ad696a 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -703,6 +703,8 @@ export const nlTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Nummervolume normaliseren met ReplayGain-metadata', replayGainMode: 'Modus', + replayGainAuto: 'Auto', + replayGainAutoDesc: 'Kiest album-gain wanneer aangrenzende nummers in de wachtrij van hetzelfde album zijn, anders nummer-gain.', replayGainTrack: 'Nummer', replayGainAlbum: 'Album', replayGainPreGain: 'Pre-Gain (getagde bestanden)', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 633dcb14..886407eb 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -733,6 +733,8 @@ export const ruTranslation = { replayGain: 'Replay Gain', replayGainDesc: 'Выравнивание громкости по метаданным ReplayGain', replayGainMode: 'Режим', + replayGainAuto: 'Авто', + replayGainAutoDesc: 'Использует громкость альбома, когда соседние треки в очереди из того же альбома, иначе громкость трека.', replayGainTrack: 'По треку', replayGainAlbum: 'По альбому', replayGainPreGain: 'Предусиление (файлы с тегами)', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index fa0d3009..f8d87dc2 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -699,6 +699,8 @@ export const zhTranslation = { replayGain: '回放增益', replayGainDesc: '使用 ReplayGain 元数据标准化曲目音量', replayGainMode: '模式', + replayGainAuto: '自动', + replayGainAutoDesc: '当队列中相邻曲目来自同一专辑时使用专辑增益,否则使用曲目增益。', replayGainTrack: '曲目', replayGainAlbum: '专辑', replayGainPreGain: '预增益(有标签文件)', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index ed2a6f2c..7ba7c4a2 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1314,22 +1314,36 @@ export default function Settings() { {auth.replayGainEnabled && ( -
- {t('settings.replayGainMode')}: - - +
+
+ {t('settings.replayGainMode')}: + + + +
+ {auth.replayGainMode === 'auto' && ( +
+ {t('settings.replayGainAutoDesc')} +
+ )}
)} {auth.replayGainEnabled && ( diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 9484138e..89dcea11 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -49,7 +49,7 @@ interface AuthState { excludeAudiobooks: boolean; customGenreBlacklist: string[]; replayGainEnabled: boolean; - replayGainMode: 'track' | 'album'; + replayGainMode: 'track' | 'album' | 'auto'; replayGainPreGainDb: number; // added to RG gain for tagged files (0…+6 dB) replayGainFallbackDb: number; // gain for untagged files / radio (-6…0 dB) crossfadeEnabled: boolean; @@ -200,7 +200,7 @@ interface AuthState { setExcludeAudiobooks: (v: boolean) => void; setCustomGenreBlacklist: (v: string[]) => void; setReplayGainEnabled: (v: boolean) => void; - setReplayGainMode: (v: 'track' | 'album') => void; + setReplayGainMode: (v: 'track' | 'album' | 'auto') => void; setReplayGainPreGainDb: (v: number) => void; setReplayGainFallbackDb: (v: number) => void; setCrossfadeEnabled: (v: boolean) => void; @@ -308,7 +308,7 @@ export const useAuthStore = create()( excludeAudiobooks: false, customGenreBlacklist: [], replayGainEnabled: false, - replayGainMode: 'track', + replayGainMode: 'auto', replayGainPreGainDb: 0, replayGainFallbackDb: 0, crossfadeEnabled: false, diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index e01903af..27757c63 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -64,6 +64,37 @@ export function songToTrack(song: SubsonicSong): Track { }; } +/** + * Resolve the ReplayGain dB value for a track based on the configured mode. + * In 'auto' mode, picks album-gain when an adjacent queue neighbour shares the + * same albumId (i.e. the track is being played as part of an album), otherwise + * track-gain. Falls back to track-gain when album-gain is missing. + */ +export function resolveReplayGainDb( + track: Track, + prevTrack: Track | null | undefined, + nextTrack: Track | null | undefined, + enabled: boolean, + mode: 'track' | 'album' | 'auto', +): number | null { + if (!enabled) return null; + let useAlbum: boolean; + if (mode === 'album') { + useAlbum = true; + } else if (mode === 'track') { + useAlbum = false; + } else { + const albumId = track.albumId; + useAlbum = !!albumId && ( + prevTrack?.albumId === albumId || nextTrack?.albumId === albumId + ); + } + const value = useAlbum + ? (track.replayGainAlbumDb ?? track.replayGainTrackDb) + : track.replayGainTrackDb; + return value ?? null; +} + export function shuffleArray(items: T[]): T[] { const arr = [...items]; for (let i = arr.length - 1; i > 0; i--) { @@ -590,11 +621,15 @@ function handleAudioProgress(current_time: number, duration: number) { if (shouldChainGapless && nextTrack.id !== gaplessPreloadingId) { gaplessPreloadingId = nextTrack.id; const authState = useAuthStore.getState(); - const replayGainDb = authState.replayGainEnabled - ? (authState.replayGainMode === 'album' - ? (nextTrack.replayGainAlbumDb ?? nextTrack.replayGainTrackDb) - : nextTrack.replayGainTrackDb) ?? null - : null; + // Auto-mode neighbours for the *next* track: current track on its left, + // queue[nextIdx+1] on its right. + const nextNeighbour = nextIdx + 1 < queue.length + ? queue[nextIdx + 1] + : (repeatMode === 'all' && queue.length > 0 ? queue[0] : null); + const replayGainDb = resolveReplayGainDb( + nextTrack, track, nextNeighbour, + authState.replayGainEnabled, authState.replayGainMode, + ); const replayGainPeak = authState.replayGainEnabled ? (nextTrack.replayGainPeak ?? null) : null; @@ -1210,9 +1245,12 @@ export const usePlayerStore = create()( ); } setDeferHotCachePrefetch(true); - const replayGainDb = authState.replayGainEnabled - ? (authState.replayGainMode === 'album' ? (track.replayGainAlbumDb ?? track.replayGainTrackDb) : track.replayGainTrackDb) ?? null - : null; + const playIdx = idx >= 0 ? idx : 0; + const nextNeighbour = playIdx + 1 < newQueue.length ? newQueue[playIdx + 1] : null; + const replayGainDb = resolveReplayGainDb( + track, prevTrack, nextNeighbour, + authState.replayGainEnabled, authState.replayGainMode, + ); const replayGainPeak = authState.replayGainEnabled ? (track.replayGainPeak ?? null) : null; invoke('audio_play', { url, @@ -1296,8 +1334,10 @@ export const usePlayerStore = create()( set({ isPlaying: true }); return; } - const { currentTrack, queue, currentTime } = get(); + const { currentTrack, queue, queueIndex, currentTime } = get(); if (!currentTrack) return; + const coldPrev = queueIndex > 0 ? queue[queueIndex - 1] : null; + const coldNext = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null; if (isAudioPaused) { // Rust engine has audio loaded but paused — just resume it. @@ -1317,9 +1357,10 @@ export const usePlayerStore = create()( // Update store with fresh track data if available if (freshSong) set({ currentTrack: trackToPlay }); const authStateCold = useAuthStore.getState(); - const replayGainDbCold = authStateCold.replayGainEnabled - ? (authStateCold.replayGainMode === 'album' ? (trackToPlay.replayGainAlbumDb ?? trackToPlay.replayGainTrackDb) : trackToPlay.replayGainTrackDb) ?? null - : null; + const replayGainDbCold = resolveReplayGainDb( + trackToPlay, coldPrev, coldNext, + authStateCold.replayGainEnabled, authStateCold.replayGainMode, + ); const replayGainPeakCold = authStateCold.replayGainEnabled ? (trackToPlay.replayGainPeak ?? null) : null; const coldServerId = useAuthStore.getState().activeServerId ?? ''; setDeferHotCachePrefetch(true); @@ -1350,9 +1391,10 @@ export const usePlayerStore = create()( if (playGeneration !== gen) return; // Fallback to currentTrack if fetch fails const authStateCold = useAuthStore.getState(); - const replayGainDbCold = authStateCold.replayGainEnabled - ? (authStateCold.replayGainMode === 'album' ? currentTrack.replayGainAlbumDb : currentTrack.replayGainTrackDb) ?? null - : null; + const replayGainDbCold = resolveReplayGainDb( + currentTrack, coldPrev, coldNext, + authStateCold.replayGainEnabled, authStateCold.replayGainMode, + ); const replayGainPeakCold = authStateCold.replayGainEnabled ? (currentTrack.replayGainPeak ?? null) : null; const coldServerId = useAuthStore.getState().activeServerId ?? ''; setDeferHotCachePrefetch(true); @@ -1732,16 +1774,17 @@ export const usePlayerStore = create()( }, updateReplayGainForCurrentTrack: () => { - const { currentTrack, volume } = get(); + const { currentTrack, queue, queueIndex, volume } = get(); if (!currentTrack || !currentTrack.id) return; const authState = useAuthStore.getState(); - const replayGainDb = authState.replayGainEnabled - ? (authState.replayGainMode === 'album' - ? (currentTrack.replayGainAlbumDb ?? currentTrack.replayGainTrackDb) - : currentTrack.replayGainTrackDb) ?? null - : null; - const replayGainPeak = authState.replayGainEnabled - ? (currentTrack.replayGainPeak ?? null) + const prev = queueIndex > 0 ? queue[queueIndex - 1] : null; + const next = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null; + const replayGainDb = resolveReplayGainDb( + currentTrack, prev, next, + authState.replayGainEnabled, authState.replayGainMode, + ); + const replayGainPeak = authState.replayGainEnabled + ? (currentTrack.replayGainPeak ?? null) : null; invoke('audio_update_replay_gain', {