Browse Source

split gapless from fade, add additional fade types

- add logic for gapless playback in redux store
master
jeffvli 3 years ago
parent
commit
21a0704ac1
  1. 116
      src/components/player/Player.tsx
  2. 24
      src/components/player/PlayerBar.tsx
  3. 66
      src/components/settings/Config.tsx
  4. 15
      src/redux/playQueueSlice.ts

116
src/components/player/Player.tsx

@ -26,6 +26,45 @@ import { setCurrentSeek } from '../../redux/playerSlice';
import cacheSong from '../shared/cacheSong';
import { getSongCachePath, isCached } from '../../shared/utils';
const gaplessListenHandler = (
currentPlayerRef: any,
nextPlayerRef: any,
playQueue: any,
currentPlayer: number,
dispatch: any,
pollingInterval: number
) => {
const seek =
Math.round(currentPlayerRef.current.audioEl.current.currentTime * 100) /
100;
const duration =
Math.round(currentPlayerRef.current.audioEl.current.duration * 100) / 100;
const seekable =
currentPlayerRef.current.audioEl.current.seekable.length >= 1
? currentPlayerRef.current.audioEl.current.seekable.end(
currentPlayerRef.current.audioEl.current.seekable.length - 1
)
: 0;
if (playQueue.currentPlayer === currentPlayer) {
dispatch(
setCurrentSeek({
seek,
seekable,
})
);
}
// Add a bit of leeway for the second track to start since the
// seek value doesn't always reach the duration
const durationPadding =
pollingInterval <= 10 ? 0.13 : pollingInterval <= 20 ? 0.14 : 0.15;
if (seek + durationPadding >= duration) {
nextPlayerRef.current.audioEl.current.play();
}
};
const listenHandler = (
currentPlayerRef: any,
nextPlayerRef: any,
@ -65,6 +104,7 @@ const listenHandler = (
let currentPlayerVolumeCalculation;
let nextPlayerVolumeCalculation;
let percentageOfFadeLeft;
let n;
switch (fadeType) {
case 'equalPower':
// https://dsp.stackexchange.com/a/14755
@ -88,6 +128,30 @@ const listenHandler = (
nextPlayerVolumeCalculation =
(percentageOfFadeLeft - 1) ** 2 * playQueue.volume;
break;
case fadeType.match(/constantPower.*/)?.input:
// https://math.stackexchange.com/a/26159
n =
fadeType === 'constantPower'
? 0
: fadeType === 'constantPowerSlowFade'
? 1
: fadeType === 'constantPowerSlowCut'
? 3
: 10;
percentageOfFadeLeft = timeLeft / fadeDuration;
currentPlayerVolumeCalculation =
Math.cos(
(Math.PI / 4) *
((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)
) * playQueue.volume;
nextPlayerVolumeCalculation =
Math.cos(
(Math.PI / 4) *
((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)
) * playQueue.volume;
break;
default:
currentPlayerVolumeCalculation =
(timeLeft / fadeDuration) * playQueue.volume;
@ -164,13 +228,14 @@ const listenHandler = (
}
};
const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
const Player = ({ currentEntryList, children }: any, ref: any) => {
const player1Ref = useRef<any>();
const player2Ref = useRef<any>();
const dispatch = useAppDispatch();
const playQueue = useAppSelector((state) => state.playQueue);
const player = useAppSelector((state) => state.player);
const cacheSongs = settings.getSync('cacheSongs');
const [debug, setDebug] = useState(playQueue.showDebugWindow);
const [title, setTitle] = useState('');
const [cachePath] = useState(path.join(getSongCachePath(), '/'));
const [fadeDuration, setFadeDuration] = useState(playQueue.fadeDuration);
@ -228,6 +293,7 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
useEffect(() => {
// Update playback settings when changed in redux store
setDebug(playQueue.showDebugWindow);
setFadeDuration(playQueue.fadeDuration);
setFadeType(playQueue.fadeType);
setVolumeFade(playQueue.volumeFade);
@ -236,6 +302,7 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
playQueue.fadeDuration,
playQueue.fadeType,
playQueue.pollingInterval,
playQueue.showDebugWindow,
playQueue.volumeFade,
]);
@ -302,9 +369,12 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
) {
dispatch(setCurrentPlayer(2));
dispatch(incrementPlayerIndex(1));
dispatch(setPlayerVolume({ player: 1, volume: 0 }));
dispatch(setPlayerVolume({ player: 2, volume: playQueue.volume }));
dispatch(setIsFading(false));
if (fadeDuration !== 0) {
dispatch(setPlayerVolume({ player: 1, volume: 0 }));
dispatch(setPlayerVolume({ player: 2, volume: playQueue.volume }));
dispatch(setIsFading(false));
}
dispatch(setAutoIncremented(false));
}
}
@ -343,9 +413,11 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
) {
dispatch(setCurrentPlayer(1));
dispatch(incrementPlayerIndex(2));
dispatch(setPlayerVolume({ player: 1, volume: playQueue.volume }));
dispatch(setPlayerVolume({ player: 2, volume: 0 }));
dispatch(setIsFading(false));
if (fadeDuration !== 0) {
dispatch(setPlayerVolume({ player: 1, volume: playQueue.volume }));
dispatch(setPlayerVolume({ player: 2, volume: 0 }));
dispatch(setIsFading(false));
}
dispatch(setAutoIncremented(false));
}
}
@ -374,6 +446,28 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
setTitle(`${playStatus} ${songTitle}`);
}, [currentEntryList, playQueue, playQueue.currentIndex, player.status]);
const handleGaplessPlayer1 = () => {
gaplessListenHandler(
player1Ref,
player2Ref,
playQueue,
1,
dispatch,
pollingInterval
);
};
const handleGaplessPlayer2 = () => {
gaplessListenHandler(
player2Ref,
player1Ref,
playQueue,
2,
dispatch,
pollingInterval
);
};
return (
<>
<Helmet>
@ -395,7 +489,9 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
}
listenInterval={pollingInterval}
preload="auto"
onListen={handleListenPlayer1}
onListen={
fadeDuration === 0 ? handleGaplessPlayer1 : handleListenPlayer1
}
onEnded={handleOnEndedPlayer1}
volume={playQueue.player1.volume}
autoPlay={
@ -419,7 +515,9 @@ const Player = ({ currentEntryList, debug, children }: any, ref: any) => {
}
listenInterval={pollingInterval}
preload="auto"
onListen={handleListenPlayer2}
onListen={
fadeDuration === 0 ? handleGaplessPlayer2 : handleListenPlayer2
}
onEnded={handleOnEndedPlayer2}
volume={playQueue.player2.volume}
autoPlay={

24
src/components/player/PlayerBar.tsx

@ -56,7 +56,6 @@ const PlayerBar = () => {
const [seekBackwardInterval] = useState<number>(
Number(settings.getSync('seekBackwardInterval')) || 5
);
const [debug] = useState(Boolean(settings.getSync('showDebugWindow')));
const playersRef = useRef<any>();
const history = useHistory();
@ -79,13 +78,27 @@ const PlayerBar = () => {
volume: localVolume,
})
);
if (playQueue.fadeDuration === 0) {
dispatch(
setPlayerVolume({
player: playQueue.currentPlayer === 1 ? 2 : 1,
volume: localVolume,
})
);
}
settings.setSync('volume', localVolume);
}
setIsDragging(false);
}, 10);
return () => clearTimeout(debounce);
}, [dispatch, isDraggingVolume, localVolume, playQueue.currentPlayer]);
}, [
dispatch,
isDraggingVolume,
localVolume,
playQueue.currentPlayer,
playQueue.fadeDuration,
]);
useEffect(() => {
setSeek(player.currentSeek);
@ -309,8 +322,10 @@ const PlayerBar = () => {
};
return (
<Player ref={playersRef} currentEntryList={currentEntryList} debug={debug}>
{debug && <DebugWindow currentEntryList={currentEntryList} />}
<Player ref={playersRef} currentEntryList={currentEntryList}>
{playQueue.showDebugWindow && (
<DebugWindow currentEntryList={currentEntryList} />
)}
<PlayerContainer>
<FlexboxGrid align="middle" style={{ height: '100%' }}>
<FlexboxGrid.Item
@ -480,7 +495,6 @@ const PlayerBar = () => {
}}
/>
</CustomTooltip>
{/* Next Song Button */}
<CustomTooltip text="Next track">
<PlayerControlIcon

66
src/components/settings/Config.tsx

@ -35,13 +35,17 @@ import {
import { getImageCachePath, getSongCachePath } from '../../shared/utils';
import setDefaultSettings from '../shared/setDefaultSettings';
import { HeaderButton } from '../shared/styled';
import { useAppDispatch } from '../../redux/hooks';
import { setPlaybackSetting } from '../../redux/playQueueSlice';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
setPlaybackSetting,
setPlayerVolume,
} from '../../redux/playQueueSlice';
const fsUtils = require('nodejs-fs-utils');
const Config = () => {
const dispatch = useAppDispatch();
const playQueue = useAppSelector((state) => state.playQueue);
const [isScanning, setIsScanning] = useState(false);
const [scanProgress, setScanProgress] = useState(0);
const [imgCacheSize, setImgCacheSize] = useState(0);
@ -50,7 +54,7 @@ const Config = () => {
const [isEditingCachePath, setIsEditingCachePath] = useState(false);
const [newCachePath, setNewCachePath] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [requiresReload, setRequiresReload] = useState(false);
const [requiresReload] = useState(false);
const songCols: any = settings.getSync('songListColumns');
const albumCols: any = settings.getSync('albumListColumns');
@ -178,18 +182,25 @@ const Config = () => {
Fading works by polling the audio player on an interval to determine
when to start fading to the next track. Due to this, you may notice
the fade timing may not be 100% perfect. Lowering the player polling
interval may increase the accuracy of the fade, but may also decrease
application performance. You may find that lowering the polling
interval between <code>1</code> and <code>50</code> can cause
application instabilities, so use at your own risk.
interval can increase the accuracy of the fade, but may also decrease
application performance as calculations are running for the fade.
</p>
<p>
If volume fade is disabled, then the fading-in track will start at the
specified crossfade duration at full volume. This is to tentatively
support <strong>gapless playback without fade</strong>, but due to
tiny inconsistencies with the audio polling interval, you may find the
fade better. Experiment with these settings to find your sweet spot.
specified crossfade duration at full volume.
</p>
<p>
Setting the crossfade duration to <code>0</code> will enable{' '}
<strong>gapless playback</strong>. All other playback settings except
the polling interval will be ignored. It is recommended that you use a
polling interval of <code>1 - 20</code> for increased transition
accuracy.
</p>
<p style={{ fontSize: 'smaller' }}>
*Enable the debug window if you want to view the differences between
each fade type
</p>
<div style={{ width: '300px', paddingTop: '20px' }}>
@ -200,10 +211,22 @@ const Config = () => {
min={0}
max={100}
onChange={(e) => {
settings.setSync('fadeDuration', e);
settings.setSync('fadeDuration', Number(e));
dispatch(
setPlaybackSetting({ setting: 'fadeDuration', value: e })
setPlaybackSetting({
setting: 'fadeDuration',
value: Number(e),
})
);
if (Number(e) === 0) {
dispatch(
setPlayerVolume({ player: 1, volume: playQueue.volume })
);
dispatch(
setPlayerVolume({ player: 2, volume: playQueue.volume })
);
}
}}
style={{ width: '150px' }}
/>
@ -216,9 +239,12 @@ const Config = () => {
min={1}
max={1000}
onChange={(e) => {
settings.setSync('pollingInterval', e);
settings.setSync('pollingInterval', Number(e));
dispatch(
setPlaybackSetting({ setting: 'pollingInterval', value: e })
setPlaybackSetting({
setting: 'pollingInterval',
value: Number(e),
})
);
}}
style={{ width: '150px' }}
@ -237,6 +263,16 @@ const Config = () => {
<Radio value="equalPower">Equal Power</Radio>
<Radio value="linear">Linear</Radio>
<Radio value="dipped">Dipped</Radio>
<Radio value="constantPower">Constant Power</Radio>
<Radio value="constantPowerSlowFade">
Constant Power (slow fade)
</Radio>
<Radio value="constantPowerSlowCut">
Constant Power (slow cut)
</Radio>
<Radio value="constantPowerFastCut">
Constant Power (fast cut)
</Radio>
</RadioGroup>
<br />
<ControlLabel>Volume fade</ControlLabel>

15
src/redux/playQueueSlice.ts

@ -161,6 +161,12 @@ const getCurrentEntryIndex = (entries: any[], currentSongId: string) => {
return entries.findIndex((entry: any) => entry.id === currentSongId);
};
const handleGaplessPlayback = (state: PlayQueue) => {
if (state.fadeDuration === 0) {
state.player2.volume = state.volume;
}
};
const playQueueSlice = createSlice({
name: 'nowPlaying',
initialState,
@ -169,6 +175,7 @@ const playQueueSlice = createSlice({
const currentEntry = entrySelect(state);
resetPlayerDefaults(state);
handleGaplessPlayback(state);
state.currentSongId = state[currentEntry][0].id;
},
@ -370,6 +377,7 @@ const playQueueSlice = createSlice({
incrementCurrentIndex: (state, action: PayloadAction<string>) => {
const currentEntry = entrySelect(state);
if (state[currentEntry].length >= 1 && state.repeat !== 'one') {
if (state.currentIndex < state[currentEntry].length - 1) {
// Check that current index isn't on the last track of the queue
@ -400,6 +408,7 @@ const playQueueSlice = createSlice({
}
}
handleGaplessPlayback(state);
state.currentSongId = state[currentEntry][state.currentIndex].id;
},
@ -417,6 +426,7 @@ const playQueueSlice = createSlice({
) {
// Reset the player on the end of the playlist if no repeat
resetPlayerDefaults(state);
handleGaplessPlayback(state);
} else if (state.player1.index + 2 >= state[currentEntry].length) {
/* If incrementing would be greater than the total number of entries,
reset it back to 0. Also check if player1 is already set to 0. */
@ -436,6 +446,7 @@ const playQueueSlice = createSlice({
) {
// Reset the player on the end of the playlist if no repeat
resetPlayerDefaults(state);
handleGaplessPlayback(state);
} else if (state.player2.index + 2 >= state[currentEntry].length) {
/* If incrementing would be greater than the total number of entries,
reset it back to 0. Also check if player1 is already set to 0. */
@ -502,6 +513,7 @@ const playQueueSlice = createSlice({
state.player2.volume = 0;
}
handleGaplessPlayback(state);
state.currentSongId = state[currentEntry][state.currentIndex].id;
}
},
@ -524,6 +536,7 @@ const playQueueSlice = createSlice({
state.repeat,
state.currentIndex
);
handleGaplessPlayback(state);
},
setCurrentIndex: (state, action: PayloadAction<Entry>) => {
@ -545,6 +558,7 @@ const playQueueSlice = createSlice({
) => {
// Used with gridview where you just want to set the entry queue directly
resetPlayerDefaults(state);
handleGaplessPlayback(state);
action.payload.entries.map((entry: any) => state.entry.push(entry));
if (state.shuffle) {
@ -570,6 +584,7 @@ const playQueueSlice = createSlice({
Setting the entry queue by row will add all entries, but set the current index to
the row that was double clicked */
resetPlayerDefaults(state);
handleGaplessPlayback(state);
action.payload.entries.map((entry: any) => state.entry.push(entry));
if (state.shuffle) {

Loading…
Cancel
Save