diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index b75b36a68e..b39681397e 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -53,6 +53,7 @@ function WaveformCanvas(props: { const { project } = useEditorContext(); let canvas: HTMLCanvasElement | undefined; + let rafId: number | null = null; const { width } = useSegmentContext(); const { secsPerPixel } = useTimelineContext(); @@ -65,11 +66,22 @@ function WaveformCanvas(props: { ) => { const maxAmplitude = h; - // yellow please ctx.fillStyle = color; ctx.beginPath(); const step = 0.05 / secsPerPixel(); + const samplesPerSecond = 10; + + const startTime = props.segment.start; + const endTime = props.segment.end; + + const pixelsPerSecond = 1 / secsPerPixel(); + const samplesPerPixel = samplesPerSecond / pixelsPerSecond; + + let sampleStep = 0.1; + if (samplesPerPixel < 0.5) { + sampleStep = Math.max(0.1, Math.ceil(1 / samplesPerPixel) * 0.1); + } ctx.moveTo(0, h); @@ -78,37 +90,38 @@ function WaveformCanvas(props: { return 1.0 - Math.max(ww + gain, -60) / -60; }; + let prevX = 0; + let prevY = h; + for ( - let segmentTime = props.segment.start; - segmentTime <= props.segment.end + 0.1; - segmentTime += 0.1 + let segmentTime = startTime; + segmentTime <= endTime + 0.1; + segmentTime += sampleStep ) { - const index = Math.floor(segmentTime * 10); - const xTime = index / 10; + const index = Math.floor(segmentTime * samplesPerSecond); + if (index < 0 || index >= waveform.length) continue; + + const xTime = index / samplesPerSecond; const currentDb = typeof waveform[index] === "number" ? waveform[index] : -60; const amplitude = norm(currentDb) * maxAmplitude; - const x = (xTime - props.segment.start) / secsPerPixel(); + const x = (xTime - startTime) / secsPerPixel(); const y = h - amplitude; - const prevX = (xTime - 0.1 - props.segment.start) / secsPerPixel(); - const prevDb = - typeof waveform[index - 1] === "number" ? waveform[index - 1] : -60; - const prevAmplitude = norm(prevDb) * maxAmplitude; - const prevY = h - prevAmplitude; + if (prevX !== x) { + const cpX1 = prevX + step / 2; + const cpX2 = x - step / 2; - const cpX1 = prevX + step / 2; - const cpX2 = x - step / 2; + ctx.bezierCurveTo(cpX1, prevY, cpX2, y, x, y); - ctx.bezierCurveTo(cpX1, prevY, cpX2, y, x, y); + prevX = x; + prevY = y; + } } - ctx.lineTo( - (props.segment.end + 0.3 - props.segment.start) / secsPerPixel(), - h, - ); + ctx.lineTo((endTime + 0.3 - startTime) / secsPerPixel(), h); ctx.closePath(); ctx.fill(); @@ -146,14 +159,25 @@ function WaveformCanvas(props: { } createEffect(() => { - renderWaveforms(); + // track reactive deps + void width(); + void secsPerPixel(); + void project.audio.micVolumeDb; + void project.audio.systemVolumeDb; + if (rafId !== null) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(renderWaveforms); + }); + + onCleanup(() => { + if (rafId !== null) cancelAnimationFrame(rafId); }); return ( { canvas = el; - renderWaveforms(); + if (rafId !== null) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(renderWaveforms); }} class="absolute inset-0 w-full h-full pointer-events-none" height={52} diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 4f849a0f83..cbd271fdff 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1,5 +1,6 @@ import { createElementBounds } from "@solid-primitives/bounds"; import { createEventListener } from "@solid-primitives/event-listener"; +import { throttle } from "@solid-primitives/scheduled"; import { platform } from "@tauri-apps/plugin-os"; import { cx } from "cva"; import { batch, createRoot, createSignal, For, onMount, Show } from "solid-js"; @@ -38,6 +39,10 @@ export function Timeline() { const secsPerPixel = () => transform().zoom / (timelineBounds.width ?? 1); + const setPreviewTimeThrottled = throttle((time: number) => { + setEditorState("previewTime", time); + }, 16); + onMount(() => { if (!project.timeline) { const resume = projectHistory.pause(); @@ -196,10 +201,9 @@ export function Timeline() { }} onMouseMove={(e) => { const { left } = timelineBounds; - if (editorState.playing) return; - setEditorState( - "previewTime", - transform().position + secsPerPixel() * (e.clientX - left!), + if (editorState.playing || left == null) return; + setPreviewTimeThrottled( + transform().position + secsPerPixel() * (e.clientX - left), ); }} onMouseEnter={() => setEditorState("timeline", "hoveredTrack", null)}