From c9363cadf36de82d4f24e2c2d4b5e7a03ebbf42c Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Sun, 9 Aug 2026 14:14:01 +0200 Subject: [PATCH 1/3] fix(ui): fall back to track ReplayGain when album gain is missing Signed-off-by: junkerderprovinz --- ui/src/utils/calculateReplayGain.js | 10 ++++++ ui/src/utils/calculateReplayGain.test.js | 46 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 ui/src/utils/calculateReplayGain.test.js diff --git a/ui/src/utils/calculateReplayGain.js b/ui/src/utils/calculateReplayGain.js index 0ce69fa4b..e73ab5559 100644 --- a/ui/src/utils/calculateReplayGain.js +++ b/ui/src/utils/calculateReplayGain.js @@ -11,6 +11,16 @@ const calculateReplayGain = (preAmp, gain, peak) => { export const calculateGain = (gainInfo, song) => { switch (gainInfo.gainMode) { case 'album': { + // Fall back to track gain when the album gain is missing (singles, or + // tracks without album ReplayGain tags), matching common ReplayGain + // players instead of applying no adjustment at all. + if (song.rgAlbumGain === undefined || song.rgAlbumPeak === undefined) { + return calculateReplayGain( + gainInfo.preAmp, + song.rgTrackGain, + song.rgTrackPeak, + ) + } return calculateReplayGain( gainInfo.preAmp, song.rgAlbumGain, diff --git a/ui/src/utils/calculateReplayGain.test.js b/ui/src/utils/calculateReplayGain.test.js new file mode 100644 index 000000000..dc3e54196 --- /dev/null +++ b/ui/src/utils/calculateReplayGain.test.js @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest' +import { calculateGain } from './calculateReplayGain' + +describe('calculateGain', () => { + const preAmp = 0 + + const albumSong = { + rgAlbumGain: -6, + rgAlbumPeak: 1, + rgTrackGain: -3, + rgTrackPeak: 1, + } + const trackOnlySong = { + rgTrackGain: -3, + rgTrackPeak: 1, + } + const noGainSong = {} + + it('uses album gain in album mode when it is present', () => { + const result = calculateGain({ gainMode: 'album', preAmp }, albumSong) + expect(result).toBeCloseTo(10 ** (-6 / 20)) + }) + + it('falls back to track gain in album mode when the album gain is missing', () => { + const result = calculateGain({ gainMode: 'album', preAmp }, trackOnlySong) + // Without the fallback this returned 1 (no adjustment). It should now use + // the track gain instead. + expect(result).toBeCloseTo(10 ** (-3 / 20)) + expect(result).not.toBe(1) + }) + + it('returns 1 in album mode when neither album nor track gain is present', () => { + const result = calculateGain({ gainMode: 'album', preAmp }, noGainSong) + expect(result).toBe(1) + }) + + it('uses track gain in track mode', () => { + const result = calculateGain({ gainMode: 'track', preAmp }, albumSong) + expect(result).toBeCloseTo(10 ** (-3 / 20)) + }) + + it('returns 1 when gain is disabled', () => { + const result = calculateGain({ gainMode: 'none', preAmp }, albumSong) + expect(result).toBe(1) + }) +}) From df6ec04c44b0ec0b07ad0434a451922afea0483f Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Fri, 9 Oct 2026 20:52:08 +0200 Subject: [PATCH 2/3] fix(ui): treat null ReplayGain values as missing The native API sends absent album gain as null, so the album fallback never ran for library songs. A missing peak no longer drops the gain, which keeps R128 files without peak tags normalized. Signed-off-by: junkerderprovinz --- ui/src/utils/calculateReplayGain.js | 11 +++---- ui/src/utils/calculateReplayGain.test.js | 39 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/ui/src/utils/calculateReplayGain.js b/ui/src/utils/calculateReplayGain.js index e73ab5559..e69e27b15 100644 --- a/ui/src/utils/calculateReplayGain.js +++ b/ui/src/utils/calculateReplayGain.js @@ -1,20 +1,19 @@ const calculateReplayGain = (preAmp, gain, peak) => { - if (gain === undefined || peak === undefined) { + if (gain == null) { return 1 } // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification§ion=19 // Normalized to max gain - return Math.min(10 ** ((gain + preAmp) / 20), 1 / peak) + const replayGain = 10 ** ((gain + preAmp) / 20) + return peak == null ? replayGain : Math.min(replayGain, 1 / peak) } export const calculateGain = (gainInfo, song) => { switch (gainInfo.gainMode) { case 'album': { - // Fall back to track gain when the album gain is missing (singles, or - // tracks without album ReplayGain tags), matching common ReplayGain - // players instead of applying no adjustment at all. - if (song.rgAlbumGain === undefined || song.rgAlbumPeak === undefined) { + // Singles and untagged albums have no album gain, so use the track gain. + if (song.rgAlbumGain == null) { return calculateReplayGain( gainInfo.preAmp, song.rgTrackGain, diff --git a/ui/src/utils/calculateReplayGain.test.js b/ui/src/utils/calculateReplayGain.test.js index dc3e54196..0b257ba48 100644 --- a/ui/src/utils/calculateReplayGain.test.js +++ b/ui/src/utils/calculateReplayGain.test.js @@ -14,6 +14,12 @@ describe('calculateGain', () => { rgTrackGain: -3, rgTrackPeak: 1, } + const nativeTrackOnlySong = { + rgAlbumGain: null, + rgAlbumPeak: null, + rgTrackGain: -3, + rgTrackPeak: 1, + } const noGainSong = {} it('uses album gain in album mode when it is present', () => { @@ -23,12 +29,28 @@ describe('calculateGain', () => { it('falls back to track gain in album mode when the album gain is missing', () => { const result = calculateGain({ gainMode: 'album', preAmp }, trackOnlySong) - // Without the fallback this returned 1 (no adjustment). It should now use - // the track gain instead. expect(result).toBeCloseTo(10 ** (-3 / 20)) - expect(result).not.toBe(1) }) + it('falls back to track gain in album mode when the album gain is null', () => { + const result = calculateGain( + { gainMode: 'album', preAmp }, + nativeTrackOnlySong, + ) + expect(result).toBeCloseTo(10 ** (-3 / 20)) + }) + + it.each([null, undefined])( + 'keeps the album gain in album mode when the album peak is %s', + (rgAlbumPeak) => { + const result = calculateGain( + { gainMode: 'album', preAmp }, + { ...albumSong, rgAlbumPeak }, + ) + expect(result).toBeCloseTo(10 ** (-6 / 20)) + }, + ) + it('returns 1 in album mode when neither album nor track gain is present', () => { const result = calculateGain({ gainMode: 'album', preAmp }, noGainSong) expect(result).toBe(1) @@ -39,6 +61,17 @@ describe('calculateGain', () => { expect(result).toBeCloseTo(10 ** (-3 / 20)) }) + it.each([null, undefined])( + 'keeps the track gain in track mode when the track peak is %s', + (rgTrackPeak) => { + const result = calculateGain( + { gainMode: 'track', preAmp }, + { ...albumSong, rgTrackPeak }, + ) + expect(result).toBeCloseTo(10 ** (-3 / 20)) + }, + ) + it('returns 1 when gain is disabled', () => { const result = calculateGain({ gainMode: 'none', preAmp }, albumSong) expect(result).toBe(1) From f2bdc8c111fff25f05ecbb0a2a0c4f4ba06e6ebc Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Fri, 9 Oct 2026 20:55:45 +0200 Subject: [PATCH 3/3] test(ui): leave songs without ReplayGain tags unaffected by the pre-amp Signed-off-by: junkerderprovinz --- ui/src/utils/calculateReplayGain.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/src/utils/calculateReplayGain.test.js b/ui/src/utils/calculateReplayGain.test.js index 0b257ba48..a74c44ab3 100644 --- a/ui/src/utils/calculateReplayGain.test.js +++ b/ui/src/utils/calculateReplayGain.test.js @@ -56,6 +56,17 @@ describe('calculateGain', () => { expect(result).toBe(1) }) + it.each(['album', 'track'])( + 'does not apply the pre-amp in %s mode to a song without gain tags', + (gainMode) => { + const result = calculateGain( + { gainMode, preAmp: 6 }, + { ...nativeTrackOnlySong, rgTrackGain: null, rgTrackPeak: null }, + ) + expect(result).toBe(1) + }, + ) + it('uses track gain in track mode', () => { const result = calculateGain({ gainMode: 'track', preAmp }, albumSong) expect(result).toBeCloseTo(10 ** (-3 / 20))