ReplayGain support + audio normalization (web player) (#1988)

* ReplayGain support

- extract ReplayGain tags from files, expose via native api
- use metadata to normalize audio in web player

* make pre-push happy

* remove unnecessary prints

* remove another unnecessary print

* add tooltips, see metadata

* address comments, use settings instead

* remove console.log

* use better language for gain modes
This commit is contained in:
Kendall Garner
2023-01-17 20:52:00 +00:00
committed by Deluan
parent 9ae156dd82
commit 1324a16fc5
24 changed files with 411 additions and 56 deletions
+87 -3
View File
@@ -24,6 +24,16 @@ import locale from './locale'
import { keyMap } from '../hotkeys'
import keyHandlers from './keyHandlers'
function calculateReplayGain(preAmp, gain, peak) {
if (gain === undefined || peak === undefined) {
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 Player = () => {
const theme = useCurrentTheme()
const translate = useTranslate()
@@ -50,6 +60,70 @@ const Player = () => {
const showNotifications = useSelector(
(state) => state.settings.notifications || false
)
const gainInfo = useSelector((state) => state.replayGain)
const [context, setContext] = useState(null)
const [gainNode, setGainNode] = useState(null)
useEffect(() => {
if (
context === null &&
audioInstance &&
config.enableReplayGain &&
'AudioContext' in window
) {
const ctx = new AudioContext()
// we need this to support radios in firefox
audioInstance.crossOrigin = 'anonymous'
const source = ctx.createMediaElementSource(audioInstance)
const gain = ctx.createGain()
source.connect(gain)
gain.connect(ctx.destination)
setContext(ctx)
setGainNode(gain)
}
}, [audioInstance, context])
useEffect(() => {
if (gainNode) {
const current = playerState.current || {}
const song = current.song || {}
let numericGain
switch (gainInfo.gainMode) {
case 'album': {
numericGain = calculateReplayGain(
gainInfo.preAmp,
song.rgAlbumGain,
song.rgAlbumPeak
)
break
}
case 'track': {
numericGain = calculateReplayGain(
gainInfo.preAmp,
song.rgTrackGain,
song.rgTrackPeak
)
break
}
default: {
numericGain = 1
}
}
gainNode.gain.setValueAtTime(numericGain, context.currentTime)
}
}, [
audioInstance,
context,
gainNode,
gainInfo.gainMode,
gainInfo.preAmp,
playerState,
])
const defaultOptions = useMemo(
() => ({
@@ -75,11 +149,15 @@ const Player = () => {
},
volumeFade: { fadeIn: 200, fadeOut: 200 },
renderAudioTitle: (audioInfo, isMobile) => (
<AudioTitle audioInfo={audioInfo} isMobile={isMobile} />
<AudioTitle
audioInfo={audioInfo}
gainInfo={gainInfo}
isMobile={isMobile}
/>
),
locale: locale(translate),
}),
[isDesktop, playerTheme, translate]
[gainInfo, isDesktop, playerTheme, translate]
)
const options = useMemo(() => {
@@ -151,6 +229,12 @@ const Player = () => {
const onAudioPlay = useCallback(
(info) => {
// Do this to start the context; on chrome-based browsers, the context
// will start paused since it is created prior to user interaction
if (context && context.state !== 'running') {
context.resume()
}
dispatch(currentPlaying(info))
if (startTime === null) {
setStartTime(Date.now())
@@ -178,7 +262,7 @@ const Player = () => {
}
}
},
[dispatch, showNotifications, startTime]
[context, dispatch, showNotifications, startTime]
)
const onAudioPlayTrackChange = useCallback(() => {