mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-15 04:51:06 +02:00
add web audio, replaygain, visualizer (#1289)
* add web audio, replaygain, visualizer * remove volume multiplication in gain --------- Co-authored-by: Jeff <42182408+jeffvli@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,8 @@ interface WebPlayerEngineProps {
|
||||
onEndedPlayer2: () => void;
|
||||
onProgressPlayer1: (e: PlayerOnProgressProps) => void;
|
||||
onProgressPlayer2: (e: PlayerOnProgressProps) => void;
|
||||
onStartedPlayer1: (player: ReactPlayer) => void;
|
||||
onStartedPlayer2: (player: ReactPlayer) => void;
|
||||
playerNum: number;
|
||||
playerRef: RefObject<null | WebPlayerEngineHandle>;
|
||||
playerStatus: PlayerStatus;
|
||||
@@ -52,6 +54,8 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
onEndedPlayer2,
|
||||
onProgressPlayer1,
|
||||
onProgressPlayer2,
|
||||
onStartedPlayer1,
|
||||
onStartedPlayer2,
|
||||
playerNum,
|
||||
playerRef,
|
||||
playerStatus,
|
||||
@@ -158,6 +162,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
onEnded={src1 ? () => onEndedPlayer1() : undefined}
|
||||
onError={handleOnError(player1Ref, () => onEndedPlayer1())}
|
||||
onProgress={onProgressPlayer1}
|
||||
onReady={onStartedPlayer1}
|
||||
playbackRate={speed || 1}
|
||||
playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
@@ -177,6 +182,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
onEnded={src2 ? () => onEndedPlayer2() : undefined}
|
||||
onError={handleOnError(player2Ref, () => onEndedPlayer2())}
|
||||
onProgress={onProgressPlayer2}
|
||||
onReady={onStartedPlayer2}
|
||||
playbackRate={speed || 1}
|
||||
playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
||||
import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import {
|
||||
useMpvSettings,
|
||||
usePlaybackSettings,
|
||||
usePlayerActions,
|
||||
usePlayerData,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
usePlayerProperties,
|
||||
usePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { CrossfadeStyle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||
|
||||
const PLAY_PAUSE_FADE_DURATION = 300;
|
||||
@@ -27,6 +30,9 @@ export function WebPlayer() {
|
||||
const playerRef = useRef<null | WebPlayerEngineHandle>(null);
|
||||
const { num, player1, player2, status } = usePlayerData();
|
||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||
const playback = useMpvSettings();
|
||||
const { webAudio } = useWebAudio();
|
||||
|
||||
const { crossfadeDuration, crossfadeStyle, speed, transitionType } = usePlayerProperties();
|
||||
const isMuted = usePlayerMuted();
|
||||
const volume = usePlayerVolume();
|
||||
@@ -35,6 +41,9 @@ export function WebPlayer() {
|
||||
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
|
||||
const [isTransitioning, setIsTransitioning] = useState<boolean | string>(false);
|
||||
|
||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||
|
||||
const fadeAndSetStatus = useCallback(
|
||||
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
|
||||
if (isTransitioning) {
|
||||
@@ -261,9 +270,134 @@ export function WebPlayer() {
|
||||
return () => clearInterval(interval);
|
||||
}, [localPlayerStatus, num, setTimestamp, transitionType]);
|
||||
|
||||
const calculateReplayGain = useCallback(
|
||||
(song: QueueSong): number => {
|
||||
if (playback.replayGainMode === 'no') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let gain: number | undefined;
|
||||
let peak: number | undefined;
|
||||
|
||||
if (playback.replayGainMode === 'track') {
|
||||
gain = song.gain?.track ?? song.gain?.album;
|
||||
peak = song.peak?.track ?? song.peak?.album;
|
||||
} else {
|
||||
gain = song.gain?.album ?? song.gain?.track;
|
||||
peak = song.peak?.album ?? song.peak?.track;
|
||||
}
|
||||
|
||||
if (gain === undefined) {
|
||||
gain = playback.replayGainFallbackDB;
|
||||
|
||||
if (!gain) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (peak === undefined) {
|
||||
peak = 1;
|
||||
}
|
||||
|
||||
const preAmp = playback.replayGainPreampDB ?? 0;
|
||||
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification§ion=19
|
||||
// Normalized to max gain
|
||||
let expectedGain = 10 ** ((gain + preAmp) / 20);
|
||||
|
||||
// Nothing in the system should allow this. But, in the case that preAmp is a
|
||||
// bad value (not a number, for example), a NaN gain will cause the entire system to panic
|
||||
if (isNaN(expectedGain)) {
|
||||
expectedGain = 1;
|
||||
}
|
||||
|
||||
if (playback.replayGainClip) {
|
||||
return Math.min(expectedGain, 1 / peak);
|
||||
}
|
||||
return expectedGain;
|
||||
},
|
||||
[
|
||||
playback.replayGainClip,
|
||||
playback.replayGainFallbackDB,
|
||||
playback.replayGainMode,
|
||||
playback.replayGainPreampDB,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webAudio) return;
|
||||
|
||||
if (player1 && player1Source && num === 1) {
|
||||
const newGain = calculateReplayGain(player1);
|
||||
|
||||
// This error SHOULD never happen, as calculateReplayGain is expected to
|
||||
// always return a real value. However, to prevent app crash, check this just in case
|
||||
try {
|
||||
webAudio.gains[0].gain.setValueAtTime(Math.max(0, newGain), 0);
|
||||
} catch (error) {
|
||||
console.error('Error setting gain', error);
|
||||
}
|
||||
}
|
||||
}, [calculateReplayGain, num, player1, player1Source, volume, webAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webAudio) return;
|
||||
|
||||
if (player2 && player2Source && num === 2) {
|
||||
const newGain = calculateReplayGain(player2);
|
||||
try {
|
||||
webAudio.gains[1].gain.setValueAtTime(Math.max(0, newGain), 0);
|
||||
} catch (error) {
|
||||
console.error('Error setting gain', error);
|
||||
}
|
||||
}
|
||||
}, [calculateReplayGain, num, player1, player2Source, player2, volume, webAudio]);
|
||||
|
||||
const player1Url = useSongUrl(player1, num === 1, transcode);
|
||||
const player2Url = useSongUrl(player2, num === 2, transcode);
|
||||
|
||||
const handlePlayer1Start = useCallback(
|
||||
async (player: ReactPlayer) => {
|
||||
if (!webAudio || player1Source) return;
|
||||
if (player1Url) {
|
||||
// This should fire once, only if the source is real (meaning we
|
||||
// saw the dummy source) and the context is not ready
|
||||
if (webAudio.context.state !== 'running') {
|
||||
await webAudio.context.resume();
|
||||
}
|
||||
}
|
||||
|
||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
||||
if (internal) {
|
||||
const { context, gains } = webAudio;
|
||||
const source = context.createMediaElementSource(internal);
|
||||
source.connect(gains[0]);
|
||||
setPlayer1Source(source);
|
||||
}
|
||||
},
|
||||
[player1Source, player1Url, webAudio],
|
||||
);
|
||||
|
||||
const handlePlayer2Start = useCallback(
|
||||
async (player: ReactPlayer) => {
|
||||
if (!webAudio || player2Source) return;
|
||||
if (player2Url) {
|
||||
if (webAudio.context.state !== 'running') {
|
||||
await webAudio.context.resume();
|
||||
}
|
||||
}
|
||||
|
||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
||||
if (internal) {
|
||||
const { context, gains } = webAudio;
|
||||
const source = context.createMediaElementSource(internal);
|
||||
source.connect(gains[1]);
|
||||
setPlayer2Source(source);
|
||||
}
|
||||
},
|
||||
[player2Source, player2Url, webAudio],
|
||||
);
|
||||
|
||||
return (
|
||||
<WebPlayerEngine
|
||||
isMuted={isMuted}
|
||||
@@ -272,6 +406,8 @@ export function WebPlayer() {
|
||||
onEndedPlayer2={handleOnEndedPlayer2}
|
||||
onProgressPlayer1={onProgressPlayer1}
|
||||
onProgressPlayer2={onProgressPlayer2}
|
||||
onStartedPlayer1={handlePlayer1Start}
|
||||
onStartedPlayer2={handlePlayer2Start}
|
||||
playerNum={num}
|
||||
playerRef={playerRef}
|
||||
playerStatus={localPlayerStatus}
|
||||
|
||||
Reference in New Issue
Block a user