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 <jdp@braethoria.com>
This commit is contained in:
junkerderprovinz 2026-10-09 20:52:08 +02:00
parent c9363cadf3
commit df6ec04c44
2 changed files with 41 additions and 9 deletions

View File

@ -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&section=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,

View File

@ -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)