diff --git a/ui/src/utils/calculateReplayGain.js b/ui/src/utils/calculateReplayGain.js index 0ce69fa4b..e69e27b15 100644 --- a/ui/src/utils/calculateReplayGain.js +++ b/ui/src/utils/calculateReplayGain.js @@ -1,16 +1,25 @@ 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': { + // Singles and untagged albums have no album gain, so use the track gain. + if (song.rgAlbumGain == null) { + 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..a74c44ab3 --- /dev/null +++ b/ui/src/utils/calculateReplayGain.test.js @@ -0,0 +1,90 @@ +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 nativeTrackOnlySong = { + rgAlbumGain: null, + rgAlbumPeak: null, + 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) + expect(result).toBeCloseTo(10 ** (-3 / 20)) + }) + + 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) + }) + + 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)) + }) + + 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) + }) +})