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