diff --git a/package.json b/package.json index ebddb7406..f025f7b27 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@tanstack/react-query-persist-client": "^5.90.11", "@ts-rest/core": "^3.52.1", "@types/react-window": "^1.8.8", + "@wavesurfer/react": "^1.0.11", "@xhayper/discord-rpc": "^1.3.0", "audiomotion-analyzer": "^4.5.1", "auto-text-size": "^0.2.3", @@ -126,6 +127,7 @@ "react-window-v2": "npm:react-window@^2.2.3", "semver": "^7.5.4", "string-to-color": "^2.2.2", + "wavesurfer.js": "^7.11.1", "ws": "^8.18.2", "zod": "^3.22.3", "zustand": "^5.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afee425d5..4d5971368 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@types/react-window': specifier: ^1.8.8 version: 1.8.8 + '@wavesurfer/react': + specifier: ^1.0.11 + version: 1.0.11(react@19.1.0)(wavesurfer.js@7.11.1) '@xhayper/discord-rpc': specifier: ^1.3.0 version: 1.3.0 @@ -206,6 +209,9 @@ importers: string-to-color: specifier: ^2.2.2 version: 2.2.2 + wavesurfer.js: + specifier: ^7.11.1 + version: 7.11.1 ws: specifier: ^8.18.2 version: 8.18.2 @@ -2069,6 +2075,12 @@ packages: resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@wavesurfer/react@1.0.11': + resolution: {integrity: sha512-DRpaA3MRTKy4Jby12xvoHASa+w31FZtxaqanXcJjfqNqfamkKi8VJfRnz+Uub9LkpdgoAc3g5SuZF75lEcGgzQ==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + wavesurfer.js: '>=7.7.14' + '@xhayper/discord-rpc@1.3.0': resolution: {integrity: sha512-0NmUTiODl7u3UEjmO6y0Syp3dmgVLAt2EHrH4QKTQcXRwtF8Wl7Eipdn/GSSZ8HkDwxQFvcDGJMxT9VWB0pH8g==} engines: {node: '>=18.20.7'} @@ -5601,6 +5613,9 @@ packages: resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} engines: {node: 8.* || >= 10.*} + wavesurfer.js@7.11.1: + resolution: {integrity: sha512-8Q+wwItpjJAlhQ7crQLtKwgfbqqczm5/wx+76K4PptP+MBAjB0OA78+A9OuLnULz/8GpAQ+fKM6s81DonEO0Sg==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -7716,6 +7731,11 @@ snapshots: '@vladfrangu/async_event_emitter@2.4.6': {} + '@wavesurfer/react@1.0.11(react@19.1.0)(wavesurfer.js@7.11.1)': + dependencies: + react: 19.1.0 + wavesurfer.js: 7.11.1 + '@xhayper/discord-rpc@1.3.0': dependencies: '@discordjs/rest': 2.5.1 @@ -11637,6 +11657,8 @@ snapshots: matcher-collection: 2.0.1 minimatch: 3.1.2 + wavesurfer.js@7.11.1: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a13bc0273..eb47c6a37 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -725,6 +725,16 @@ "playerAlbumArtResolution": "player album art resolution", "playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player", "playerbarOpenDrawer": "playerbar fullscreen toggle", + "playerbarSlider": "playerbar slider", + "playerbarSliderType_optionSlider": "slider", + "playerbarSliderType_optionWaveform": "waveform", + "playerbarWaveformAlign": "waveform align", + "playerbarWaveformAlign_optionTop": "top", + "playerbarWaveformAlign_optionCenter": "center", + "playerbarWaveformAlign_optionBottom": "bottom", + "playerbarWaveformBarWidth": "waveform bar width", + "playerbarWaveformGap": "waveform gap", + "playerbarWaveformRadius": "waveform radius", "preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available", "preferLocalLyrics": "prefer local lyrics", "preservePitch_description": "preserves pitch when modifying playback speed", diff --git a/src/renderer/features/player/audio-player/engine/wavesurfer-player-engine.tsx b/src/renderer/features/player/audio-player/engine/wavesurfer-player-engine.tsx new file mode 100644 index 000000000..d87f84115 --- /dev/null +++ b/src/renderer/features/player/audio-player/engine/wavesurfer-player-engine.tsx @@ -0,0 +1,272 @@ +import type { RefObject } from 'react'; +import type WaveSurfer from 'wavesurfer.js'; + +import { useWavesurfer } from '@wavesurfer/react'; +import { useEffect, useImperativeHandle, useRef, useState } from 'react'; + +import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types'; +import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils'; +import { PlayerStatus } from '/@/shared/types/types'; + +export interface WaveSurferPlayerEngineHandle extends AudioPlayer { + player1(): { + ref: null | WaveSurfer; + setVolume: (volume: number) => void; + }; + player2(): { + ref: null | WaveSurfer; + setVolume: (volume: number) => void; + }; +} + +interface WaveSurferPlayerEngineProps { + isMuted: boolean; + isTransitioning: boolean; + onEndedPlayer1: () => void; + onEndedPlayer2: () => void; + onProgressPlayer1: (e: PlayerOnProgressProps) => void; + onProgressPlayer2: (e: PlayerOnProgressProps) => void; + playerNum: number; + playerRef: RefObject; + playerStatus: PlayerStatus; + speed?: number; + src1: string | undefined; + src2: string | undefined; + volume: number; +} + +// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393 +// This is used so that the player will always have an