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', {