1.完成进度条的seek功能。

This commit is contained in:
amass 2023-06-14 15:06:52 +08:00
parent bf11df50e9
commit b2fd82e9f4
6 changed files with 50 additions and 73 deletions

View File

@ -35,6 +35,7 @@ export default function () {
const dispatch = useDispatch() const dispatch = useDispatch()
const accessToken = useSelector(state => state.user.accessToken); const accessToken = useSelector(state => state.user.accessToken);
const passportId = useSelector(state => state.user.passportId); const passportId = useSelector(state => state.user.passportId);
const currentTime = useSelector(state => state.recorder.currentTime);
useEffect(() => { useEffect(() => {
yzs.get_record_list(accessToken, passportId).then(list => { yzs.get_record_list(accessToken, passportId).then(list => {
dispatch(setList(list.result)); dispatch(setList(list.result));
@ -53,8 +54,8 @@ export default function () {
mr: `24px`, mr: `24px`,
}} }}
> >
<PlayerBar /> <PlayerBar currentTime={currentTime} />
<RecordLyrics /> <RecordLyrics currentTime={currentTime} />
</Box> </Box>
</ThemeProvider> </ThemeProvider>
</div > </div >

View File

@ -6,7 +6,7 @@ import { useResizeDetector } from 'react-resize-detector';
import pauseIcon from "./assets/play.png"; import pauseIcon from "./assets/play.png";
import playIcon from "./assets/pause.png"; import playIcon from "./assets/pause.png";
import downloadIcon from "./assets/download.png"; import downloadIcon from "./assets/download.png";
import { togglePauseState } from "./business/recorderSlice.js" import { setCurrentTime, togglePauseState } from "./business/recorderSlice.js"
import Waveform from "./components/Waveform"; import Waveform from "./components/Waveform";
const durationFormat = (time) => { const durationFormat = (time) => {
@ -18,10 +18,9 @@ const durationFormat = (time) => {
return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0') + ":" + second.toString().padStart(2, '0'); return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0') + ":" + second.toString().padStart(2, '0');
} }
export default function () { export default function ({ currentTime }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [duration, setDuration] = useState("00:00:00"); const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState("00:00:00");
const [canvasWidth, setCanvasWidth] = useState(0); const [canvasWidth, setCanvasWidth] = useState(0);
const currentIndex = useSelector(state => state.recorder.currentIndex); const currentIndex = useSelector(state => state.recorder.currentIndex);
const recordList = useSelector(state => state.recorder.list); const recordList = useSelector(state => state.recorder.list);
@ -43,10 +42,10 @@ export default function () {
}; };
const onDurationChange = (event) => { const onDurationChange = (event) => {
setDuration(durationFormat(player.current.duration)); setDuration(player.current.duration);
} }
const onTimeUpdate = (event) => { const onTimeUpdate = (event) => {
setCurrentTime(durationFormat(player.current.currentTime)); dispatch(setCurrentTime(player.current.currentTime));
} }
const onResize = useCallback((width, height) => { const onResize = useCallback((width, height) => {
@ -57,7 +56,15 @@ export default function () {
onResize: onResize onResize: onResize
}); });
return <Stack container > const seekRecord = (second) => {
player.current.currentTime = second;
}
return <Stack container sx={{
position: "sticky",
top: 48,
backgroundColor: "#FAFAFA",
}} >
<Container disableGutters maxWidth={false} sx={{ <Container disableGutters maxWidth={false} sx={{
height: 60, height: 60,
display: "flex", display: "flex",
@ -75,7 +82,7 @@ export default function () {
</IconButton> </IconButton>
<audio autoplay ref={player} src={currentBlob} onDurationChange={onDurationChange} onTimeUpdate={onTimeUpdate} /> <audio autoplay ref={player} src={currentBlob} onDurationChange={onDurationChange} onTimeUpdate={onTimeUpdate} />
<Waveform width={canvasWidth} /> <Waveform width={canvasWidth} duration={duration} currentTime={currentTime} playing={!pause} seek={seekRecord} />
<Select <Select
defaultValue={1.0} defaultValue={1.0}
sx={{ sx={{
@ -89,12 +96,10 @@ export default function () {
<MenuItem value={1.5}>1.5X</MenuItem> <MenuItem value={1.5}>1.5X</MenuItem>
<MenuItem value={2.0}>2.0X</MenuItem> <MenuItem value={2.0}>2.0X</MenuItem>
</Select> </Select>
</div> </div>
<Container maxWidth={false} sx={{ backgroundColor: "#F2F4F5" }}> <Container maxWidth={false} sx={{ backgroundColor: "#F2F4F5" }}>
<Typography sx={{ color: "#6B7486" }}> {currentTime} / {duration}</Typography> <Typography sx={{ color: "#6B7486" }}> {durationFormat(currentTime)} / {durationFormat(duration)}</Typography>
</Container> </Container>
</Stack> </Stack>

View File

@ -3,9 +3,14 @@ import { Typography, Paper } from "@mui/material";
import styles from './RecordLyrics.module.css'; import styles from './RecordLyrics.module.css';
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
function isHighlight(currentTime, { start, end }) {
currentTime *= 1000;
return (currentTime > start) && (currentTime <= end);
}
export default function () {
export default function ({ currentTime }) {
const currentLyric = useSelector(state => state.recorder.currentLyric); const currentLyric = useSelector(state => state.recorder.currentLyric);
const currentIndex = useSelector(state => state.recorder.currentIndex); const currentIndex = useSelector(state => state.recorder.currentIndex);
const recordList = useSelector(state => state.recorder.list); const recordList = useSelector(state => state.recorder.list);
@ -16,8 +21,7 @@ export default function () {
return <Paper className={styles.lyricsBrowser}> return <Paper className={styles.lyricsBrowser}>
{recordList.at(currentIndex).type === 1 ? (typeof currentLyric === "object" ? currentLyric.map((lyric, index) => { {recordList.at(currentIndex).type === 1 ? (typeof currentLyric === "object" ? currentLyric.map((lyric, index) => {
return <div className={styles.lyricItem}> return <div className={styles.lyricItem}>
<Typography align="left">{lyric.text}</Typography> <Typography align="left" color={isHighlight(currentTime, lyric) ? "red" : "black"}>{lyric.text}</Typography>
<Typography align="left">{lyric.translation}</Typography>
</div> </div>
}) : <React.Fragment />) : <div> {typeof currentLyric === "string" ? currentLyric : ""}</div>} }) : <React.Fragment />) : <div> {typeof currentLyric === "string" ? currentLyric : ""}</div>}
</Paper> </Paper>

View File

@ -7,6 +7,7 @@ export const recorderSlice = createSlice({
currentIndex: 0, currentIndex: 0,
currentLyric: [], currentLyric: [],
currentBlob: {}, currentBlob: {},
currentTime: 0, // 当前音频播放时间
pause: true, pause: true,
}, },
reducers: { reducers: {
@ -23,6 +24,9 @@ export const recorderSlice = createSlice({
setCurrentBlob: (state, action) => { setCurrentBlob: (state, action) => {
state.currentBlob = action.payload; state.currentBlob = action.payload;
}, },
setCurrentTime: (state, action) => {
state.currentTime = action.payload;
},
togglePauseState: (state) => { togglePauseState: (state) => {
state.pause = !state.pause; state.pause = !state.pause;
}, },
@ -30,6 +34,6 @@ export const recorderSlice = createSlice({
}) })
// Action creators are generated for each case reducer function // Action creators are generated for each case reducer function
export const { setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState } = recorderSlice.actions export const { setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setCurrentTime } = recorderSlice.actions
export default recorderSlice.reducer export default recorderSlice.reducer

View File

@ -1,11 +1,10 @@
import { useEffect, useRef, useState, useCallback } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
import useSetTrackProgress from "./useSetTrackProgress.js"
import { useResizeDetector } from 'react-resize-detector';
const pointWidth = 2; const pointWidth = 2;
const pointMargin = 3; const pointMargin = 3;
const leftPadding = 14; // 留出空间画时间戳
const rightPadding = 14;
const trackDuration = 60; // <audio> duration: seconds
const chunkedData = [10, 12, 12, 15, 20, 30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75, const chunkedData = [10, 12, 12, 15, 20, 30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75,
30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75, 30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75,
30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75, 30, 34, 36, 37, 35, 37, 63, 73, 83, 67, 86, 76, 74, 83, 90, 79, 85, 90, 83, 83, 75, 57, 70, 82, 58, 73, 73, 85, 73, 76, 75,
@ -33,7 +32,7 @@ const pointCoordinates = ({
const pointHeight = Math.round((amplitude / 100) * maxAmplitude) const pointHeight = Math.round((amplitude / 100) * maxAmplitude)
const verticalCenter = Math.round((canvasHeight - pointHeight) / 2) const verticalCenter = Math.round((canvasHeight - pointHeight) / 2)
return [ return [
index * (pointWidth + pointMargin) + 14, // x starting point index * (pointWidth + pointMargin) + leftPadding, // x starting point
(canvasHeight - pointHeight) - verticalCenter + 10, // y starting point (canvasHeight - pointHeight) - verticalCenter + 10, // y starting point
pointWidth, // width pointWidth, // width
pointHeight, // height pointHeight, // height
@ -46,15 +45,16 @@ function drawText(context, width, duration) {
context.fillStyle = "#99A3AF"; context.fillStyle = "#99A3AF";
let timepoint = 0; let timepoint = 0;
let segements = parseInt((width - 30) / 80); let segements = parseInt((width - leftPadding - rightPadding) / 80);
let interval = parseInt(duration / segements); let interval = Math.ceil(duration / segements);
for (let i = 0; i < segements; i++) { console.log("segements", segements, width / 80, width);
for (let i = 0; i <= segements; i++) {
context.fillStyle = "#99A3AF"; context.fillStyle = "#99A3AF";
context.fillText(timeTag(timepoint), i * 80, 10 + 4); context.fillText(timeTag(timepoint), i * 80, 10 + 4);
timepoint += interval; timepoint += interval;
} }
for (let i = 14; i < width; i += 40) { for (let i = leftPadding; i < width - rightPadding; i += 40) {
context.fillStyle = "#9DA7B2"; context.fillStyle = "#9DA7B2";
context.fillRect(i, 0, 1, (i - 14) % 80 == 0 ? 12 : 6); context.fillRect(i, 0, 1, (i - 14) % 80 == 0 ? 12 : 6);
} }
@ -64,7 +64,7 @@ const paintCanvas = ({
canvas, waveformData, duration, canvasWidth, canvasHeight, pointWidth, pointMargin, canvas, waveformData, duration, canvasWidth, canvasHeight, pointWidth, pointMargin,
playingPoint, hoverXCoord, playingPoint, hoverXCoord,
}) => { }) => {
// console.log("paintCanvas", canvasHeight, playingPoint, hoverXCoord); console.log("paintCanvas", duration, canvasHeight, playingPoint, hoverXCoord);
const ref = canvas.current; const ref = canvas.current;
const ctx = ref.getContext('2d') const ctx = ref.getContext('2d')
ctx.clearRect(0, 0, ref.width, ref.height); ctx.clearRect(0, 0, ref.width, ref.height);
@ -96,20 +96,18 @@ const paintCanvas = ({
); );
} }
export default function ({ width }) { export default function ({ width, duration, currentTime, playing, seek }) {
const canvas = useRef(null); const canvas = useRef(null);
const [trackPlaying, setTrackPlaying] = useState(true);
const [trackProgress, setTrackProgress] = useState(0); // [0,100] const [trackProgress, setTrackProgress] = useState(0); // [0,100]
const [hoverXCoord, setHoverXCoord] = useState(0); const [hoverXCoord, setHoverXCoord] = useState(0);
const [startTime, setStartTime] = useState(Date.now());
const playingPoint = (trackProgress * (canvas.current ? width : 0) / 100) / (pointWidth + pointMargin); const playingPoint = (trackProgress * (canvas.current ? (width - leftPadding) : 0) / 100) / (pointWidth + pointMargin);
const paintWaveform = useCallback(() => { const paintWaveform = useCallback(() => {
paintCanvas({ paintCanvas({
canvas, canvas,
waveformData: chunkedData, waveformData: chunkedData,
duration: trackDuration, duration: duration,
canvasWidth: width, canvasWidth: width,
canvasHeight: canvas.current.height, canvasHeight: canvas.current.height,
pointWidth, pointWidth,
@ -117,24 +115,21 @@ export default function ({ width }) {
playingPoint, playingPoint,
hoverXCoord, hoverXCoord,
}) })
}, [playingPoint, hoverXCoord]) }, [playingPoint, hoverXCoord, duration, width])
useSetTrackProgress({
trackProgress, setTrackProgress, trackDuration, startTime,
trackPlaying
});
useEffect(() => { useEffect(() => {
if (!canvas.current) return; if (!canvas.current) return;
paintWaveform() paintWaveform()
}, [canvas, width, duration, hoverXCoord])
}, [canvas, width])
useEffect(() => { useEffect(() => {
paintWaveform() paintWaveform()
}, [playingPoint]) }, [playingPoint])
useEffect(() => {
setTrackProgress(currentTime * 100 / duration);
}, [currentTime]);
const setDefaultX = useCallback(() => { const setDefaultX = useCallback(() => {
setHoverXCoord(0); setHoverXCoord(0);
@ -146,8 +141,8 @@ export default function ({ width }) {
const seekTrack = (e) => { const seekTrack = (e) => {
const xCoord = e.clientX - canvas.current.getBoundingClientRect().left const xCoord = e.clientX - canvas.current.getBoundingClientRect().left
const seekMs = trackDuration * (xCoord / width); const seekSecond = duration * ((xCoord - leftPadding) / (width - leftPadding));
setStartTime(Date.now() / 1000 - seekMs) seek(seekSecond);
} }
return <canvas height={70} width={width} ref={canvas} return <canvas height={70} width={width} ref={canvas}

View File

@ -1,32 +0,0 @@
import { useEffect } from 'react'
const clamp = (val, min, max) => Math.min(Math.max(val, min), max)
export default ({
trackProgress, trackDuration, startTime,
setTrackProgress, trackPlaying,
}) => {
useEffect(() => {
let animation
if (trackPlaying) {
animation = window.requestAnimationFrame(() => {
console.log("ani");
const trackProgressPerc = ((Date.now() / 1000 - startTime)) * 100 / trackDuration
setTrackProgress(
clamp(
trackProgressPerc,
0, 100,
),
)
})
}
return () => {
window.cancelAnimationFrame(animation)
}
}, [
trackPlaying,
trackDuration,
startTime,
trackProgress,
])
}