new player

Seto Elkahfi committed Aug 1, 2024 at 23:03 UTC 14a8023f7d2d17cb5fcae9cf5aa05a4e61cd1b0e
5 files changed +199 -130
frontend/splitfire-desktop/app/play/_components/audio-info-middle.tsx new
+55
@@ -0,0 +1,55 @@
1 +import { useLogger } from "@/app/_src/lib/logger";
2 +import { useCountDown } from "@/app/_src/lib/useCountDown";
3 +import { useEffect } from "react";
4 +
5 +export function AudioInfoMiddle({
6 + counterDownTimer,
7 +}: {
8 + counterDownTimer: number;
9 +}) {
10 + const timePlayed = "0:01";
11 + const timeTotal = "3:20";
12 + return (
13 + <div className="space-y-2">
14 + <div className="flex-auto flex items-center justify-evenly">
15 + <CountDownView
16 + seconds={counterDownTimer}
17 + type="audio"
18 + onCountDownEnd={() => {}}
19 + />
20 + </div>
21 + <div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
22 + <div className="text-cyan-500 dark:text-slate-100">{timePlayed}</div>
23 + <div className="text-slate-500 dark:text-slate-400">{timeTotal}</div>
24 + </div>
25 + </div>
26 + );
27 +}
28 +
29 +function CountDownView(props: {
30 + seconds: number;
31 + type: string;
32 + onCountDownEnd?: () => void;
33 +}) {
34 + const log = useLogger("CountDownView");
35 + const { secondsLeft, startTimer } = useCountDown();
36 +
37 + useEffect(() => {
38 + log.debug("start counting down seconds:", props.seconds);
39 + startTimer(props.seconds);
40 + // eslint-disable-next-line react-hooks/exhaustive-deps
41 + }, []);
42 +
43 + if (secondsLeft === 0 && props.onCountDownEnd) {
44 + log.debug("CountDownView: onCountDownEnd");
45 + props.onCountDownEnd();
46 + }
47 +
48 + const displayText = secondsLeft === 0 ? props.type : secondsLeft;
49 +
50 + return (
51 + <div className="text-red-500 dark:text-slate-100">
52 + <h1>{displayText}</h1>
53 + </div>
54 + );
55 +}
frontend/splitfire-desktop/app/play/_components/audio-info.tsx new
+28
@@ -0,0 +1,28 @@
1 +import Image from "next/image";
2 +
3 +export function AudioInfo(props: { }) {
4 + const numberPlayed = 785;
5 + const difficulty = "Difficulty: Hard";
6 + const imageUrl = "https://img.freepik.com/free-psd/square-flyer-template-maximalist-business_23-2148524497.jpg?w=1800&t=st=1699458420~exp=1699459020~hmac=5b00d72d6983d04966cc08ccd0fc1f80ad0d7ba75ec20316660e11efd18133cd"
7 + return (
8 + <div className="flex items-center space-x-4">
9 + <Image
10 + src={imageUrl}
11 + alt=""
12 + width="88"
13 + height="88"
14 + className="flex-none rounded-lg bg-slate-100"
15 + loading="lazy"
16 + />
17 + <div className="min-w-0 flex-auto space-y-1 font-semibold">
18 + <p className="text-cyan-500 dark:text-cyan-400 text-sm leading-6">
19 + <abbr title="Track">Played:</abbr> {numberPlayed}
20 + </p>
21 + <h2 className="text-slate-500 dark:text-slate-400 text-sm leading-6 truncate">
22 + Music: New Album The Lorem
23 + </h2>
24 + <p className="text-slate-900 dark:text-slate-50 text-lg">{difficulty}</p>
25 + </div>
26 + </div>
27 + );
28 +}
frontend/splitfire-desktop/app/play/_components/control-button.tsx
+77 -36
@@ -1,43 +1,84 @@
1 -import { Col, Row } from "react-bootstrap"
2 -import { BsPlayBtnFill, BsStopBtnFill, BsFillRecordBtnFill, BsPauseBtnFill } from "react-icons/bs"
1 +import {
2 + BookmarkIcon,
3 + PauseIcon,
4 + CircleIcon,
5 + BookmarkFilledIcon,
6 + ResumeIcon,
7 + PlayIcon,
8 + ResetIcon,
9 +} from "@radix-ui/react-icons";
10 +import { useLogger } from "@/app/_src/lib/logger";
11 +import { useState } from "react";
12
4 -export function ControlButtons(props: { isPlaying: boolean, isRecording: boolean, onClick: () => void, onStop: () => void, onRecord: () => void }) {
13 +export function ControlButtons(props: {
14 + isPlaying: boolean;
15 + isRecording: boolean;
16 + onClick: () => void;
17 + onStop: () => void;
18 + onRecord: () => void;
19 + onResume: () => void;
20 +}) {
21 + const log = useLogger("ControlButtonsV2");
22 + const { isPlaying, isRecording } = props;
23 + const [isBookmarked, setIsBookmarked] = useState(false);
24
6 - const { isPlaying, isRecording, onClick, onStop, onRecord } = props
25 + const toggleBookmark = () => {
26 + log.debug("toggleBookmark");
27 + setIsBookmarked(!isBookmarked);
28 + };
29
8 - const _onPlayOrPause = () => {
9 - if (isRecording) return
30 + const bookMarkText = isBookmarked
31 + ? "Remove from bookmarks"
32 + : "Add to bookmarks";
33 + const recordingText = isRecording ? "Recording" : "Stop recording";
34
11 - onClick()
35 + let playPauseResumeIcon = isPlaying ? (
36 + <PauseIcon color={"black"} width="24" height="24" />
37 + ) : (
38 + <PlayIcon color={"black"} width="24" height="24" />
39 + );
40 + if (isRecording) {
41 + playPauseResumeIcon = <ResumeIcon color={"black"} width="24" height="24" />;
42 }
43
14 - return <>
15 - <Col xs={{span: 1, offset: 4}} className="mb-3 mt-3" color="red" >
16 - { isPlaying ? <BsPauseBtnFill onClick={_onPlayOrPause} size={32} style={ { cursor: isRecording ? undefined : "pointer"}} title="Pause" color={ isRecording ? "grey" : "white"} />
17 - : <BsPlayBtnFill onClick={_onPlayOrPause} size={32} style={ { cursor: isRecording ? undefined : "pointer"}} title="Play" color={ isRecording ? "grey" : "white" } />
18 - }
19 - </Col>
20 - <Col xs={{span: 1}} className="mb-3 mt-3">
21 - <BsStopBtnFill
22 - onClick={() => {
23 - if (isRecording) return
24 -
25 - onStop()
26 - }}
27 - size={32}
28 - style={{ cursor: isRecording ? undefined : "pointer"}}
29 - title="Stop"
30 - color={ isRecording ? "grey" : "white" }
31 - />
32 - </Col>
33 - <Col xs={{span: 1}} className="mb-3 mt-3">
34 - <BsFillRecordBtnFill
35 - onClick={onRecord}
36 - size={32}
37 - style={{ cursor: "pointer"}}
38 - title={ isRecording ? "Stop" : "Record"}
39 - color={ isRecording ? "green" : "red" }
40 - />
41 - </Col>
42 - </>
44 + return (
45 + <div className="bg-slate-50 text-slate-500 py-6 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center">
46 + <div className="flex-auto flex items-center justify-evenly">
47 + <button
48 + type="button"
49 + aria-label={bookMarkText}
50 + onClick={toggleBookmark}
51 + title={bookMarkText}
52 + >
53 + {isBookmarked ? (
54 + <BookmarkFilledIcon color={"black"} width="24" height="24" />
55 + ) : (
56 + <BookmarkIcon color={"black"} width="24" height="24" />
57 + )}
58 + </button>
59 + <button
60 + type="button"
61 + aria-label="Record"
62 + disabled={isPlaying}
63 + title={recordingText}
64 + >
65 + <CircleIcon
66 + color={isRecording ? "blue" : "red"}
67 + width="30"
68 + height="32"
69 + />
70 + </button>
71 + <button
72 + type="button"
73 + aria-label="Play/Pause/Resume"
74 + disabled={isRecording}
75 + >
76 + {playPauseResumeIcon}
77 + </button>
78 + <button type="button" aria-label="Restart" disabled={isRecording}>
79 + <ResetIcon color={"black"} width="24" height="24" />
80 + </button>
81 + </div>
82 + </div>
83 + );
84 }
frontend/splitfire-desktop/app/play/_components/player.tsx
+27 -80
@@ -1,12 +1,7 @@
1 "use client";
2
3 -import { ControlButtonsView } from "@/app/_src/components/player/ControlButtons";
4 -import { CountDownView } from "@/app/_src/components/player/CountDownView";
5 -import { HideShowToggleView } from "@/app/_src/components/player/HideShowToggle";
3 import { ModeDemucs } from "@/app/_src/components/player/models/Mode";
4 import { PlayerState, PlayerVolume } from "@/app/_src/components/player/Player";
8 -import { RecordingView } from "@/app/_src/components/player/RecordingView";
9 -import { VolumeSliderView } from "@/app/_src/components/player/VolumeSliderView";
5 import { useLogger } from "@/app/_src/lib/logger";
6 import {
7 TAURI_PLAYER_PAUSED,
@@ -20,9 +15,9 @@ import {
15 } from "@/app/_src/lib/tauriHandler";
16 import { invoke } from "@tauri-apps/api";
17 import { useState } from "react";
23 -import { Row, Col, Container } from "react-bootstrap";
24 -import { VolumeSlider } from "./volume-slider";
18 import { ControlButtons } from "./control-button";
19 +import { AudioInfo } from "./audio-info";
20 +import { AudioInfoMiddle } from "./audio-info-middle";
21
22 export default function Player({
23 audioId,
@@ -50,8 +45,8 @@ export default function Player({
45 const [otherVolume, setOtherVolume] = useState<string>("100");
46
47 // Player functions
53 - const _onVolumeChange = (mode: ModeDemucs, value: string) => {
54 - log.debug("_onVolumeChange", mode, value);
48 + const onVolumeChange = (mode: ModeDemucs, value: string) => {
49 + log.debug("onVolumeChange", mode, value);
50 switch (mode) {
51 case ModeDemucs.Vocals:
52 setVocalsVolume(value);
@@ -74,7 +69,7 @@ export default function Player({
69 //_toggleRecording()
70 };
71
77 - const _toggleRecording = async () => {
72 + const toggleRecording = async () => {
73 try {
74 // If we are playing playbacks, stop it before recording.
75 if (playerState === PlayerState.PLAYING) {
@@ -88,7 +83,7 @@ export default function Player({
83
84 // If we're already recording, we want to stop it and return.
85 if (playerState === PlayerState.RECORDING) {
91 - _stopRecording();
86 + stopRecording();
87 return;
88 }
89
@@ -110,7 +105,7 @@ export default function Player({
105 const result = await invoke(TAURI_PLAYER_RECORD, {
106 audioId,
107 userId,
113 - playerVolumes: _playerVolumes(),
108 + playerVolumes: currentPlayerVolumes(),
109 });
110 log.debug(TAURI_PLAYER_RECORD, result);
111 }, COUNTING_DOWN_NUMBER * 1000);
@@ -119,7 +114,7 @@ export default function Player({
114 }
115 };
116
122 - const _stopRecording = async () => {
117 + const stopRecording = async () => {
118 setRecordingLength(0);
119 setPlayerState(PlayerState.STOPPED);
120 const stop_recording_result = await invoke(TAURI_PLAYER_RECORD_STOP, {
@@ -147,7 +142,7 @@ export default function Player({
142 log.error(error);
143 }
144 };
150 - const _playerVolumes = (): PlayerVolume[] => {
145 + const currentPlayerVolumes = (): PlayerVolume[] => {
146 return [
147 { mode: ModeDemucs.Vocals, volume: vocalsVolume },
148 { mode: ModeDemucs.Drums, volume: drumsVolume },
@@ -156,7 +151,7 @@ export default function Player({
151 ];
152 };
153
159 - const _togglePlayAudio = async () => {
154 + const togglePlayAudio = async () => {
155 // Should disabled when recording
156 if (playerState === PlayerState.RECORDING) {
157 return;
@@ -170,7 +165,7 @@ export default function Player({
165 setIsCountingCountdown(false);
166 setPlayerState(PlayerState.PLAYING);
167 await invoke(TAURI_PLAYER_PLAY, {
173 - playerVolumes: _playerVolumes(),
168 + playerVolumes: currentPlayerVolumes(),
169 });
170 }, COUNTING_DOWN_NUMBER * 1000);
171 break;
@@ -182,76 +177,28 @@ export default function Player({
177 case PlayerState.PAUSED:
178 log.debug(TAURI_PLAYER_RESUMED, "Resumed");
179 setPlayerState(PlayerState.PLAYING);
185 - await invoke(TAURI_PLAYER_PLAY, { playerVolumes: _playerVolumes() });
180 + await invoke(TAURI_PLAYER_PLAY, { playerVolumes: currentPlayerVolumes() });
181 }
182 } catch (error) {
183 log.error(error);
184 }
185 };
191 -
192 - let buttonOrCounting = <></>;
193 - if (isCountingCountdown) {
194 - buttonOrCounting = <CountDownView seconds={COUNTING_DOWN_NUMBER} type="" />;
195 - } else {
196 - buttonOrCounting = (
197 - <ControlButtons
198 - isPlaying={playerState === PlayerState.PLAYING}
199 - isRecording={playerState === PlayerState.RECORDING}
200 - onClick={_togglePlayAudio}
201 - onStop={_stopPlayer}
202 - onRecord={_toggleRecording}
203 - />
204 - );
205 - }
206 -
207 - let audioWaveform = <></>;
208 - if (
209 - playerState === PlayerState.PLAYING ||
210 - playerState === PlayerState.PAUSED
211 - ) {
212 - audioWaveform = (
213 - <>
214 - <CountDownView seconds={recordingLength} type="Practicing!!!" />
215 - </>
216 - );
217 - } else if (playerState === PlayerState.RECORDING) {
218 - audioWaveform = (
219 - <RecordingView
220 - length={recordingLength}
221 - onRecordingEnd={onFinishedRecording}
222 - />
223 - );
224 - }
225 -
186 +
187 return (
227 - <div className="prose prose-sm prose-invert max-w-none">
228 - <div className="grid grid-rows-4 gap-6">
229 - {buttonOrCounting}
230 - <Row className="h-100 d-inline-block">
231 - <Col xs={12} className="mb-3 mt-3">
232 - <Container style={{ height: 300 }}>
233 - <div className="d-flex align-items-center justify-content-center h-100">
234 - <div className="d-flex flex-column">{audioWaveform}</div>
235 - </div>
236 - </Container>
237 - </Col>
238 - </Row>
239 - <Row className="border border-light">
240 - <Col xs={2} className="mb-3 mt-3">
241 - <p color="red">Volume</p>
242 - </Col>
243 - <Col xs={{ span: 2, offset: 8 }} className="mb-3 mt-3">
244 - <HideShowToggleView
245 - hideVolumeSliders={hideVolumeSliders}
246 - setHideVolumeSliders={setHideVolumeSliders}
247 - />
248 - </Col>
249 - <VolumeSlider
250 - _onVolumeChange={_onVolumeChange}
251 - isHidden={hideVolumeSliders}
252 - />
253 - </Row>
188 + <>
189 + <div className="bg-black border-slate-100 dark:bg-slate-800 dark:border-slate-500 border-b rounded-t-xl p-4 pb-6 sm:p-10 sm:pb-8 lg:p-6 xl:p-10 xl:pb-8 space-y-6 sm:space-y-8 lg:space-y-6 xl:space-y-8 items-center">
190 + <AudioInfo />
191 + <AudioInfoMiddle counterDownTimer={COUNTING_DOWN_NUMBER} />
192 + <ControlButtons
193 + isPlaying={playerState === PlayerState.PLAYING}
194 + isRecording={playerState === PlayerState.RECORDING}
195 + onClick={togglePlayAudio}
196 + onStop={_stopPlayer}
197 + onRecord={toggleRecording}
198 + onResume={function (): void {
199 + throw new Error("Function not implemented.");
200 + } } />
201 </div>
255 - </div>
202 + </>
203 );
204 }
frontend/splitfire-desktop/app/play/page.tsx
+12 -14
@@ -51,15 +51,15 @@ export default function Page() {
51 }
52 }
53 fetchData();
54 - // eslint-disable-next-line react-hooks/exhaustive-deps
54 + // eslint-disable-next-line react-hooks/exhaustive-deps
55 }, []);
56 -
56 +
57 // Sanity check
58 if (!audioFileId || !userId) {
59 log.error("Missing audio file ID or user ID");
60 return <div>Missing audio file ID</div>;
61 }
62 -
62 +
63 return (
64 <>
65 <div className="flex justify-between">
@@ -68,17 +68,15 @@ export default function Page() {
68 </div>
69 </div>
70 <div className="prose prose-sm prose-invert max-w-none">
71 - <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
72 - {state === State.LOADING && (
73 - <IconSpinner className="w-10 h-10 animate-spin" />
74 - )}
75 - {state === State.ERROR && (
76 - <div className="text-red-500">Failed to load song</div>
77 - )}
78 - {state === State.LOADED && (
79 - <Player audioId={audioFileId} userId={userId} />
80 - )}
81 - </div>
71 + {state === State.LOADING && (
72 + <IconSpinner className="w-10 h-10 animate-spin" />
73 + )}
74 + {state === State.ERROR && (
75 + <div className="text-red-500">Failed to load song</div>
76 + )}
77 + {state === State.LOADED && (
78 + <Player audioId={audioFileId} userId={userId} />
79 + )}
80 </div>
81 </>
82 );