preview speed control

This commit is contained in:
2026-06-08 23:46:51 -07:00
parent b5345f0345
commit 2bfa1e9269
4 changed files with 72 additions and 6 deletions
+50 -2
View File
@@ -18,6 +18,7 @@
selectAll,
setActiveSwitch,
setFps,
setPreviewSpeed,
setVideoUrl,
snapToFrame,
updateMarker,
@@ -38,7 +39,12 @@
function rescheduleAudio() {
if (!videoEl) return;
audioEngine.reschedule(videoEl.currentTime, app.markers, !videoEl.paused);
audioEngine.reschedule(
videoEl.currentTime,
app.markers,
!videoEl.paused,
app.previewSpeed,
);
lastScheduledTime = videoEl.currentTime;
}
@@ -60,11 +66,18 @@
const playing = app.isPlaying;
void app.markers;
void app.activeSwitch;
void app.previewSpeed;
if (playing && Math.abs(t - lastScheduledTime) > 0.25) {
rescheduleAudio();
}
});
$effect(() => {
const speed = app.previewSpeed;
controller?.setPlaybackRate(speed);
if (app.isPlaying) rescheduleAudio();
});
onMount(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (isEditableTarget(event.target)) return;
@@ -254,6 +267,22 @@
</button>
<span class="shortcut-hint">Ctrl+Space</span>
<label class="speed-control">
Preview
<input
type="range"
min="25"
max="200"
step="5"
value={Math.round(app.previewSpeed * 100)}
disabled={!app.videoUrl}
oninput={(event) => {
setPreviewSpeed(Number(event.currentTarget.value) / 100);
}}
/>
<span class="speed-value">{Math.round(app.previewSpeed * 100)}%</span>
</label>
<label class="switch-select">
Switch
<select
@@ -378,8 +407,27 @@
border-radius: 4px;
}
.switch-select {
.speed-control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
margin-left: auto;
}
.speed-control input[type='range'] {
width: 8rem;
accent-color: #68d391;
}
.speed-value {
min-width: 2.75rem;
text-align: right;
color: #a0aec0;
font-size: 0.85rem;
}
.switch-select {
display: flex;
align-items: center;
gap: 0.5rem;
+10 -4
View File
@@ -11,7 +11,12 @@ import { app } from '../store.svelte';
export type AudioEngine = {
init: (video: HTMLVideoElement) => Promise<void>;
reschedule: (currentTime: number, markers: Marker[], isPlaying: boolean) => void;
reschedule: (
currentTime: number,
markers: Marker[],
isPlaying: boolean,
playbackRate: number,
) => void;
playKey: (code: string, keyLabel: string, phase: SoundPhase) => void;
preloadActiveSwitch: () => Promise<void>;
destroy: () => void;
@@ -146,7 +151,7 @@ export function createAudioEngine(): AudioEngine {
await preloadSwitch(app.activeSwitch);
},
reschedule(time: number, markers: Marker[], playing: boolean) {
reschedule(time: number, markers: Marker[], playing: boolean, playbackRate: number) {
if (!ctx || !playing) {
clearScheduled();
return;
@@ -155,6 +160,7 @@ export function createAudioEngine(): AudioEngine {
clearScheduled();
const audioCtx = ctx;
const startAt = audioCtx.currentTime + 0.02;
const rate = playbackRate > 0 ? playbackRate : 1;
const switchType = app.activeSwitch;
void (async () => {
@@ -172,14 +178,14 @@ export function createAudioEngine(): AudioEngine {
const releaseTime = markerReleaseTime(marker);
if (marker.time >= time - 0.05) {
const pressDelay = marker.time - time;
const pressDelay = (marker.time - time) / rate;
const pressBuffer = await loadBuffer(switchType, 'press', pressKey);
const pressSource = playBuffer(pressBuffer, startAt + Math.max(0, pressDelay));
if (pressSource) scheduledSources.push(pressSource);
}
if (releaseTime >= time - 0.05) {
const releaseDelay = releaseTime - time;
const releaseDelay = (releaseTime - time) / rate;
const releaseBuffer = await loadBuffer(switchType, 'release', releaseKey);
const releaseSource = playBuffer(releaseBuffer, startAt + Math.max(0, releaseDelay));
if (releaseSource) scheduledSources.push(releaseSource);
+5
View File
@@ -14,6 +14,7 @@ export const app = $state({
videoUrl: null as string | null,
videoName: null as string | null,
timelineWidth: 800,
previewSpeed: 1,
});
let markerCounter = 0;
@@ -52,6 +53,10 @@ export function setFps(value: number) {
app.fps = value;
}
export function setPreviewSpeed(value: number) {
app.previewSpeed = Math.max(0.25, Math.min(2, value));
}
export function setPixelsPerSecond(value: number) {
app.pixelsPerSecond = value;
}
+7
View File
@@ -6,6 +6,7 @@ export type VideoController = {
togglePlayPause: () => void;
stepFrame: (delta: number) => void;
seek: (time: number) => void;
setPlaybackRate: (rate: number) => void;
};
export function attachVideoPlayer(video: HTMLVideoElement): VideoController {
@@ -71,6 +72,9 @@ export function attachVideoPlayer(video: HTMLVideoElement): VideoController {
if (video.readyState >= 1) onLoadedMetadata();
video.preservesPitch = true;
video.playbackRate = app.previewSpeed;
return {
element: video,
destroy() {
@@ -99,5 +103,8 @@ export function attachVideoPlayer(video: HTMLVideoElement): VideoController {
video.currentTime = clamped;
setCurrentTime(clamped);
},
setPlaybackRate(rate: number) {
video.playbackRate = rate;
},
};
}