From d7422640e21bfe759e065ec5409eeb6d24b3f34f Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:13:40 -0700 Subject: [PATCH 1/2] Optimize waveform rendering and timeline preview updates Improves waveform rendering in ClipTrack by adjusting sample stepping and using requestAnimationFrame for smoother updates. Timeline preview time updates are now throttled to reduce unnecessary state changes and improve performance during mouse movement. --- .../src/routes/editor/Timeline/ClipTrack.tsx | 65 +++++++++++++------ .../src/routes/editor/Timeline/index.tsx | 20 +++--- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index b75b36a68e..3635173bd8 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,35 +90,39 @@ 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; - - const cpX1 = prevX + step / 2; - const cpX2 = x - step / 2; + if (prevX !== x) { + 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(), + (endTime + 0.3 - startTime) / secsPerPixel(), h, ); @@ -146,14 +162,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..2a418b4d47 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(); @@ -194,14 +199,13 @@ export function Timeline() { }); }); }} - onMouseMove={(e) => { - const { left } = timelineBounds; - if (editorState.playing) return; - setEditorState( - "previewTime", - transform().position + secsPerPixel() * (e.clientX - left!), - ); - }} + onMouseMove={(e) => { + const { left } = timelineBounds; + if (editorState.playing || left == null) return; + setPreviewTimeThrottled( + transform().position + secsPerPixel() * (e.clientX - left), + ); + }} onMouseEnter={() => setEditorState("timeline", "hoveredTrack", null)} onMouseLeave={() => { setEditorState("previewTime", null); From 0c77fab0d87c9a236b8cbd7b84197271e6e215b5 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:18:18 -0700 Subject: [PATCH 2/2] formatting --- .../src/routes/editor/Timeline/ClipTrack.tsx | 15 ++++++--------- apps/desktop/src/routes/editor/Timeline/index.tsx | 14 +++++++------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 3635173bd8..b39681397e 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -71,13 +71,13 @@ function WaveformCanvas(props: { 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); @@ -100,7 +100,7 @@ function WaveformCanvas(props: { ) { const index = Math.floor(segmentTime * samplesPerSecond); if (index < 0 || index >= waveform.length) continue; - + const xTime = index / samplesPerSecond; const currentDb = @@ -115,16 +115,13 @@ function WaveformCanvas(props: { const cpX2 = x - step / 2; ctx.bezierCurveTo(cpX1, prevY, cpX2, y, x, y); - + prevX = x; prevY = y; } } - ctx.lineTo( - (endTime + 0.3 - startTime) / secsPerPixel(), - h, - ); + ctx.lineTo((endTime + 0.3 - startTime) / secsPerPixel(), h); ctx.closePath(); ctx.fill(); diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 2a418b4d47..cbd271fdff 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -199,13 +199,13 @@ export function Timeline() { }); }); }} - onMouseMove={(e) => { - const { left } = timelineBounds; - if (editorState.playing || left == null) return; - setPreviewTimeThrottled( - transform().position + secsPerPixel() * (e.clientX - left), - ); - }} + onMouseMove={(e) => { + const { left } = timelineBounds; + if (editorState.playing || left == null) return; + setPreviewTimeThrottled( + transform().position + secsPerPixel() * (e.clientX - left), + ); + }} onMouseEnter={() => setEditorState("timeline", "hoveredTrack", null)} onMouseLeave={() => { setEditorState("previewTime", null);