Skip to content

Commit 4ba9909

Browse files
committed
自制简易音频播放系统降低延迟,增加音频延迟测试页面,细化倍速调节步进
1 parent cb0f522 commit 4ba9909

File tree

12 files changed

+554
-113
lines changed

12 files changed

+554
-113
lines changed

src/components/AMLLWrapper/index.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
// #else
44
import "@applemusic-like-lyrics/core/style.css";
55
// #endif
6-
import {
7-
audioElAtom,
8-
audioPlayingAtom,
9-
currentTimeAtom,
10-
} from "$/states/audio.ts";
6+
import { audioPlayingAtom, currentTimeAtom } from "$/states/audio.ts";
117
import { isDarkThemeAtom, lyricLinesAtom } from "$/states/main.ts";
128
import {
139
lyricWordFadeWidthAtom,
@@ -24,6 +20,7 @@ import classNames from "classnames";
2420
import { useAtomValue, useStore } from "jotai";
2521
import { memo, useEffect, useMemo, useRef } from "react";
2622
import styles from "./index.module.css";
23+
import { audioEngine } from "$/utils/audio";
2724

2825
export const AMLLWrapper = memo(() => {
2926
const originalLyricLines = useAtomValue(lyricLinesAtom);
@@ -34,7 +31,6 @@ export const AMLLWrapper = memo(() => {
3431
const showRomanLines = useAtomValue(showRomanLinesAtom);
3532
const wordFadeWidth = useAtomValue(lyricWordFadeWidthAtom);
3633
const playerRef = useRef<LyricPlayerRef>(null);
37-
const store = useStore();
3834

3935
const lyricLines = useMemo(() => {
4036
return structuredClone(
@@ -60,8 +56,7 @@ export const AMLLWrapper = memo(() => {
6056
boxSizing: "content-box",
6157
}}
6258
onLyricLineClick={(evt) => {
63-
store.get(audioElAtom).currentTime =
64-
evt.line.getLine().startTime / 1000;
59+
audioEngine.seekMusic(evt.line.getLine().startTime / 1000);
6560
}}
6661
lyricLines={lyricLines}
6762
currentTime={currentTime}

src/components/AudioControls/audio-slider.tsx

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import {
2-
audioElAtom,
32
audioPlayingAtom,
43
currentDurationAtom,
54
currentTimeAtom,
6-
loadableAudioWaveformAtom,
75
} from "$/states/audio.ts";
6+
import { audioEngine } from "$/utils/audio";
87
import { msToTimestamp } from "$/utils/timestamp.ts";
98
import { Card } from "@radix-ui/themes";
10-
import { useAtomValue, useSetAtom } from "jotai";
9+
import { useSetAtom } from "jotai";
1110
import { useCallback, useLayoutEffect, useRef } from "react";
1211

1312
export const AudioSlider = () => {
1413
const waveformCanvasRef = useRef<HTMLCanvasElement>(null);
1514
const cachedWaveformRef = useRef<ImageData>(null);
16-
const waveform = useAtomValue(loadableAudioWaveformAtom);
1715
const mouseSeekPosRef = useRef(Number.NaN);
18-
const audioEl = useAtomValue(audioElAtom);
16+
const isPressingRef = useRef(false);
1917

2018
const setCurrentTime = useSetAtom(currentTimeAtom);
2119
const setCurrentDuration = useSetAtom(currentDurationAtom);
@@ -28,7 +26,7 @@ export const AudioSlider = () => {
2826
willReadFrequently: true,
2927
});
3028
if (!ctx) return;
31-
const p = audioEl.currentTime / audioEl.duration;
29+
const p = audioEngine.musicCurrentTime / audioEngine.musicDuration;
3230
const playWidth = canvas.width * p;
3331
ctx.clearRect(0, 0, canvas.width, canvas.height);
3432
const canvasStyles = getComputedStyle(canvas);
@@ -49,7 +47,7 @@ export const AudioSlider = () => {
4947
const mouseSeekWidth = mouseSeekPos * canvas.width;
5048
ctx.globalCompositeOperation = "source-over";
5149
const seekTimestamp = msToTimestamp(
52-
(mouseSeekPos * audioEl.duration * 1000) | 0,
50+
(mouseSeekPos * audioEngine.musicDuration * 1000) | 0,
5351
);
5452
const size = ctx.measureText(seekTimestamp);
5553
ctx.font = `calc(${fontSize} * ${devicePixelRatio}) ${canvasStyles.fontFamily}`;
@@ -72,7 +70,7 @@ export const AudioSlider = () => {
7270
ctx.lineTo(mouseSeekWidth, canvas.height);
7371
ctx.stroke();
7472
}
75-
}, [audioEl]);
73+
}, []);
7674

7775
const redrawWaveform = useCallback(
7876
(waveform: Float32Array) => {
@@ -123,76 +121,92 @@ export const AudioSlider = () => {
123121
useLayoutEffect(() => {
124122
let frame = 0;
125123
const onFrame = () => {
126-
if (audioEl.paused) {
124+
if (!audioEngine.musicPlaying) {
127125
cancelAnimationFrame(frame);
128126
frame = 0;
129127
return;
130128
}
131129
redrawCachedWaveform();
132-
setCurrentTime((audioEl.currentTime * 1000) | 0);
130+
setCurrentTime((audioEngine.musicCurrentTime * 1000) | 0);
133131
frame = requestAnimationFrame(onFrame);
134132
};
135133
const onLoad = () => {
136-
console.log("music duration", audioEl.duration);
137-
setCurrentDuration((audioEl.duration * 1000) | 0);
134+
setCurrentDuration((audioEngine.musicDuration * 1000) | 0);
138135
};
139136
const onPlay = () => {
140137
setAudioPlaying(true);
141138
onFrame();
142139
};
143140
const onPause = () => {
144141
setAudioPlaying(false);
145-
setCurrentTime((audioEl.currentTime * 1000) | 0);
142+
setCurrentTime((audioEngine.musicCurrentTime * 1000) | 0);
146143
if (frame !== 0) {
147144
cancelAnimationFrame(frame);
148145
frame = 0;
149146
}
150147
};
151148
const onSeeked = () => {
152-
setCurrentTime((audioEl.currentTime * 1000) | 0);
149+
setCurrentTime((audioEngine.musicCurrentTime * 1000) | 0);
153150
redrawCachedWaveform();
154151
};
155-
audioEl.addEventListener("loadedmetadata", onLoad);
156-
audioEl.addEventListener("play", onPlay);
157-
audioEl.addEventListener("pause", onPause);
158-
audioEl.addEventListener("seeked", onSeeked);
159-
audioEl.addEventListener("timeupdate", onSeeked);
160-
setAudioPlaying(!audioEl.paused);
152+
audioEngine.addEventListener("music-load", onLoad);
153+
audioEngine.addEventListener("music-resume", onPlay);
154+
audioEngine.addEventListener("music-pause", onPause);
155+
audioEngine.addEventListener("music-seeked", onSeeked);
156+
setAudioPlaying(audioEngine.musicPlaying);
161157

162158
return () => {
163-
audioEl.removeEventListener("loadedmetadata", onLoad);
164-
audioEl.removeEventListener("play", onPlay);
165-
audioEl.removeEventListener("pause", onPause);
166-
audioEl.removeEventListener("seeked", onSeeked);
167-
audioEl.removeEventListener("timeupdate", onSeeked);
159+
audioEngine.removeEventListener("music-load", onLoad);
160+
audioEngine.removeEventListener("music-resume", onPlay);
161+
audioEngine.removeEventListener("music-pause", onPause);
162+
audioEngine.removeEventListener("music-seeked", onSeeked);
168163
};
169164
}, [
170-
audioEl,
171165
setAudioPlaying,
172166
setCurrentDuration,
173167
setCurrentTime,
174168
redrawCachedWaveform,
175169
]);
176170

177171
useLayoutEffect(() => {
178-
if (waveform.state !== "hasData") return;
179-
redrawWaveform(waveform.data);
180-
}, [redrawWaveform, waveform]);
172+
const onMusicUnload = () => {
173+
redrawWaveform(new Float32Array(0));
174+
};
175+
const onMusicLoad = () => {
176+
redrawWaveform(audioEngine.musicWaveform);
177+
};
178+
audioEngine.addEventListener("music-unload", onMusicUnload);
179+
audioEngine.addEventListener("music-load", onMusicLoad);
180+
181+
return () => {
182+
audioEngine.removeEventListener("music-unload", onMusicUnload);
183+
audioEngine.removeEventListener("music-load", onMusicLoad);
184+
};
185+
}, [redrawWaveform]);
181186

182187
useLayoutEffect(() => {
183188
const canvas = waveformCanvasRef.current;
184189
if (!canvas) return;
185190
const obs = new ResizeObserver((entries) => {
186191
canvas.width = entries[0].contentRect.width * devicePixelRatio;
187192
canvas.height = entries[0].contentRect.height * devicePixelRatio;
188-
if (waveform.state !== "hasData") return;
189-
redrawWaveform(waveform.data);
193+
redrawWaveform(audioEngine.musicWaveform);
190194
});
191195
obs.observe(canvas);
192196
return () => {
193197
obs.disconnect();
194198
};
195-
}, [redrawWaveform, waveform]);
199+
}, [redrawWaveform]);
200+
201+
const onTrySeekMusicWhenDragging = useCallback(() => {
202+
const mouseSeekPos = mouseSeekPosRef.current;
203+
if (!Number.isNaN(mouseSeekPos)) {
204+
audioEngine.seekMusic(
205+
Math.max(0, mouseSeekPos * audioEngine.musicDuration),
206+
);
207+
redrawCachedWaveform();
208+
}
209+
}, [redrawCachedWaveform]);
196210

197211
return (
198212
<Card
@@ -203,7 +217,6 @@ export const AudioSlider = () => {
203217
padding: "0",
204218
}}
205219
>
206-
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
207220
<canvas
208221
style={{
209222
width: "100%",
@@ -214,38 +227,41 @@ export const AudioSlider = () => {
214227
onMouseMove={(evt) => {
215228
const rect = evt.currentTarget.getBoundingClientRect();
216229
mouseSeekPosRef.current = (evt.clientX - rect.left) / rect.width;
230+
// if (isPressingRef.current) onTrySeekMusicWhenDragging();
217231
redrawCachedWaveform();
218232
}}
219233
onMouseLeave={() => {
220234
mouseSeekPosRef.current = Number.NaN;
235+
isPressingRef.current = false;
221236
redrawCachedWaveform();
222237
}}
223238
onTouchStart={(evt) => {
224239
const rect = evt.currentTarget.getBoundingClientRect();
225240
mouseSeekPosRef.current =
226241
(evt.touches[0].clientX - rect.left) / rect.width;
242+
isPressingRef.current = true;
243+
onTrySeekMusicWhenDragging();
227244
redrawCachedWaveform();
228245
}}
229246
onTouchMove={(evt) => {
230247
const rect = evt.currentTarget.getBoundingClientRect();
231248
mouseSeekPosRef.current =
232249
(evt.touches[0].clientX - rect.left) / rect.width;
250+
// if (isPressingRef.current) onTrySeekMusicWhenDragging();
233251
redrawCachedWaveform();
234252
}}
235253
onTouchEnd={() => {
236-
const mouseSeekPos = mouseSeekPosRef.current;
237-
if (!Number.isNaN(mouseSeekPos) && audioEl) {
238-
audioEl.currentTime = mouseSeekPos * audioEl.duration;
239-
mouseSeekPosRef.current = Number.NaN;
240-
redrawCachedWaveform();
241-
}
254+
isPressingRef.current = false;
255+
// onTrySeekMusicWhenDragging();
256+
mouseSeekPosRef.current = Number.NaN;
257+
}}
258+
onMouseDown={() => {
259+
isPressingRef.current = true;
260+
onTrySeekMusicWhenDragging();
242261
}}
243-
onClick={() => {
244-
const mouseSeekPos = mouseSeekPosRef.current;
245-
if (!Number.isNaN(mouseSeekPos) && audioEl) {
246-
audioEl.currentTime = mouseSeekPos * audioEl.duration;
247-
redrawCachedWaveform();
248-
}
262+
onMouseUp={() => {
263+
isPressingRef.current = false;
264+
// onTrySeekMusicWhenDragging();
249265
}}
250266
/>
251267
</Card>

src/components/AudioControls/index.tsx

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
audioPlayingAtom,
1616
currentDurationAtom,
1717
currentTimeAtom,
18-
loadedAudioAtom,
1918
playbackRateAtom,
2019
volumeAtom,
2120
} from "$/states/audio.ts";
@@ -28,6 +27,7 @@ import {
2827
keyVolumeDownAtom,
2928
keyVolumeUpAtom,
3029
} from "$/states/keybindings.ts";
30+
import { audioEngine } from "$/utils/audio";
3131
import { useKeyBindingAtom } from "$/utils/keybindings.ts";
3232
import { msToTimestamp } from "$/utils/timestamp.ts";
3333
import {
@@ -118,8 +118,6 @@ const AudioPlaybackKeyBinding = memo(() => {
118118
});
119119

120120
export const AudioControls: FC = memo(() => {
121-
const audioEl = useAtomValue(audioElAtom);
122-
const [audio, setAudio] = useAtom(loadedAudioAtom);
123121
const [audioLoaded, setAudioLoaded] = useState(false);
124122
const currentTime = useAtomValue(currentTimeAtom);
125123
const currentDuration = useAtomValue(currentDurationAtom);
@@ -136,44 +134,66 @@ export const AudioControls: FC = memo(() => {
136134
() => {
137135
const file = inputEl.files?.[0];
138136
if (!file) return;
139-
140-
console.log("loading audio", file);
141-
setAudio(file);
137+
audioEngine.loadMusic(file);
142138
},
143139
{
144140
once: true,
145141
},
146142
);
147143
inputEl.click();
148-
}, [setAudio]);
144+
}, []);
149145

150146
const onTogglePlay = useCallback(() => {
151-
if (audioEl.paused) audioEl.play();
152-
else audioEl.pause();
153-
}, [audioEl]);
147+
if (audioEngine.musicPlaying) {
148+
audioEngine.pauseMusic();
149+
} else {
150+
audioEngine.resumeOrSeekMusic();
151+
}
152+
}, []);
154153

155154
useEffect(() => {
156-
if (audio.size > 0) {
157-
const audioUrl = URL.createObjectURL(audio);
158-
audioEl.src = audioUrl;
155+
const onMusicLoad = () => {
159156
setAudioLoaded(true);
160157
setAudioPlaying(false);
161-
return () => {
162-
audioEl.src = "";
163-
URL.revokeObjectURL(audioUrl);
164-
setAudioLoaded(false);
165-
setAudioPlaying(false);
166-
};
167-
}
168-
}, [audioEl, audio, setAudioPlaying]);
158+
};
159+
const onMusicUnload = () => {
160+
setAudioLoaded(false);
161+
setAudioPlaying(false);
162+
};
163+
const onMusicPause = () => {
164+
setAudioPlaying(false);
165+
};
166+
const onMusicResume = () => {
167+
setAudioPlaying(true);
168+
};
169+
const onVolumeChange = () => {
170+
setVolume(audioEngine.volume);
171+
};
172+
setAudioLoaded(audioEngine.musicLoaded);
173+
setAudioPlaying(audioEngine.musicPlaying);
174+
setVolume(audioEngine.volume);
175+
setPlaybackRate(audioEngine.musicPlayBackRate);
176+
audioEngine.addEventListener("music-load", onMusicLoad);
177+
audioEngine.addEventListener("music-unload", onMusicUnload);
178+
audioEngine.addEventListener("music-pause", onMusicPause);
179+
audioEngine.addEventListener("music-resume", onMusicResume);
180+
audioEngine.addEventListener("volume-change", onVolumeChange);
181+
return () => {
182+
audioEngine.removeEventListener("music-load", onMusicLoad);
183+
audioEngine.removeEventListener("music-unload", onMusicUnload);
184+
audioEngine.removeEventListener("music-pause", onMusicPause);
185+
audioEngine.removeEventListener("music-resume", onMusicResume);
186+
audioEngine.removeEventListener("volume-change", onVolumeChange);
187+
};
188+
}, [setAudioPlaying, setVolume, setPlaybackRate]);
169189

170190
useEffect(() => {
171-
audioEl.volume = volume;
172-
}, [audioEl, volume]);
191+
audioEngine.volume = volume;
192+
}, [volume]);
173193

174194
useEffect(() => {
175-
audioEl.playbackRate = playbackRate;
176-
}, [audioEl, playbackRate]);
195+
audioEngine.musicPlayBackRate = playbackRate;
196+
}, [playbackRate]);
177197

178198
return (
179199
<Card m="2" mt="0">
@@ -202,10 +222,10 @@ export const AudioControls: FC = memo(() => {
202222
</Text>
203223
<Text wrap="nowrap">播放速度</Text>
204224
<Slider
205-
min={0.25}
206-
max={4}
225+
min={0.05}
226+
max={2}
207227
defaultValue={[playbackRate]}
208-
step={0.25}
228+
step={0.05}
209229
onValueChange={(v) => setPlaybackRate(v[0])}
210230
/>
211231
<Text wrap="nowrap" color="gray" size="1">

0 commit comments

Comments
 (0)