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
+12 -3
View File
@@ -5,7 +5,7 @@ import clsx from 'clsx'
import { QualityInfo } from '../common'
import useStyle from './styles'
const AudioTitle = React.memo(({ audioInfo, isMobile }) => {
const AudioTitle = React.memo(({ audioInfo, gainInfo, isMobile }) => {
const classes = useStyle()
const className = classes.audioTitle
const isDesktop = useMediaQuery('(min-width:810px)')
@@ -15,7 +15,12 @@ const AudioTitle = React.memo(({ audioInfo, isMobile }) => {
}
const song = audioInfo.song
const qi = { suffix: song.suffix, bitRate: song.bitRate }
const qi = {
suffix: song.suffix,
bitRate: song.bitRate,
albumGain: song.rgAlbumGain,
trackGain: song.rgTrackGain,
}
return (
<Link
@@ -31,7 +36,11 @@ const AudioTitle = React.memo(({ audioInfo, isMobile }) => {
{song.title}
</span>
{isDesktop && (
<QualityInfo record={qi} className={classes.qualityInfo} />
<QualityInfo
record={qi}
className={classes.qualityInfo}
{...gainInfo}
/>
)}
</span>
{isMobile ? (
+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(() => {
+8 -2
View File
@@ -5,8 +5,13 @@ import { LoveButton, useToggleLove } from '../common'
import { keyMap } from '../hotkeys'
import config from '../config'
const Placeholder = () =>
config.enableFavourites && <LoveButton disabled={true} resource={'song'} />
const Placeholder = () => (
<>
{config.enableFavourites && (
<LoveButton disabled={true} resource={'song'} />
)}
</>
)
const Toolbar = ({ id }) => {
const { data, loading } = useGetOne('song', id)
@@ -15,6 +20,7 @@ const Toolbar = ({ id }) => {
const handlers = {
TOGGLE_LOVE: useCallback(() => toggleLove(), [toggleLove]),
}
return (
<>
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges />