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:
@@ -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 ? (
|
||||
|
||||
@@ -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§ion=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(() => {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user