@@ -9,10 +9,10 @@ import {
99 createMemo ,
1010 createRoot ,
1111 createSignal ,
12- For ,
1312 Index ,
1413 Match ,
1514 onCleanup ,
15+ onMount ,
1616 Show ,
1717 Switch ,
1818} from "solid-js" ;
@@ -44,20 +44,33 @@ function gainToScale(gain?: number) {
4444 return Math . max ( 0 , 1 + value / - WAVEFORM_MIN_DB ) ;
4545}
4646
47+ const MAX_WAVEFORM_SAMPLES = 6000 ;
48+
4749function createWaveformPath (
4850 segment : { start : number ; end : number } ,
49- waveform ?: number [ ] ,
51+ waveform : number [ ] | undefined ,
52+ targetSamples : number ,
5053) {
5154 if ( typeof Path2D === "undefined" ) return ;
5255 if ( ! waveform || waveform . length === 0 ) return ;
5356
5457 const duration = Math . max ( segment . end - segment . start , WAVEFORM_SAMPLE_STEP ) ;
5558 if ( ! Number . isFinite ( duration ) || duration <= 0 ) return ;
5659
60+ const nativeSamples = Math . ceil ( duration / WAVEFORM_SAMPLE_STEP ) + 1 ;
61+ const numSamples = Math . min (
62+ Math . max ( targetSamples , 50 ) ,
63+ MAX_WAVEFORM_SAMPLES ,
64+ nativeSamples ,
65+ ) ;
66+
67+ const timeStep = duration / numSamples ;
68+
5769 const path = new Path2D ( ) ;
5870 path . moveTo ( 0 , 1 ) ;
5971
60- const amplitudeAt = ( index : number ) => {
72+ const amplitudeAt = ( time : number ) => {
73+ const index = Math . floor ( time * 10 ) ;
6174 const sample = waveform [ index ] ;
6275 const db =
6376 typeof sample === "number" && Number . isFinite ( sample )
@@ -70,17 +83,13 @@ function createWaveformPath(
7083
7184 const controlStep = Math . min ( WAVEFORM_CONTROL_STEP / duration , 0.25 ) ;
7285
73- for (
74- let time = segment . start ;
75- time <= segment . end + WAVEFORM_SAMPLE_STEP ;
76- time += WAVEFORM_SAMPLE_STEP
77- ) {
78- const index = Math . floor ( time * 10 ) ;
79- const normalizedX = ( index / 10 - segment . start ) / duration ;
80- const prevX =
81- ( index / 10 - WAVEFORM_SAMPLE_STEP - segment . start ) / duration ;
82- const y = 1 - amplitudeAt ( index ) ;
83- const prevY = 1 - amplitudeAt ( index - 1 ) ;
86+ for ( let i = 0 ; i <= numSamples ; i ++ ) {
87+ const time = segment . start + i * timeStep ;
88+ const normalizedX = ( time - segment . start ) / duration ;
89+ const prevTime = time - timeStep ;
90+ const prevX = Math . max ( 0 , ( prevTime - segment . start ) / duration ) ;
91+ const y = 1 - amplitudeAt ( time ) ;
92+ const prevY = 1 - amplitudeAt ( prevTime ) ;
8493 const cpX1 = prevX + controlStep / 2 ;
8594 const cpX2 = normalizedX - controlStep / 2 ;
8695 path . bezierCurveTo ( cpX1 , prevY , cpX2 , y , normalizedX , y ) ;
@@ -108,41 +117,115 @@ function formatTime(totalSeconds: number): string {
108117 }
109118}
110119
120+ const MAX_CANVAS_WIDTH = 2000 ;
121+ const SAMPLES_PER_PIXEL = 2 ;
122+
111123function WaveformCanvas ( props : {
112124 systemWaveform ?: number [ ] ;
113125 micWaveform ?: number [ ] ;
114126 segment : { start : number ; end : number } ;
127+ segmentOffset : number ;
115128} ) {
116- const { project } = useEditorContext ( ) ;
129+ const { project, editorState } = useEditorContext ( ) ;
117130 const { width } = useSegmentContext ( ) ;
118- const segmentRange = createMemo ( ( ) => ( {
119- start : props . segment . start ,
120- end : props . segment . end ,
121- } ) ) ;
122- const micPath = createMemo ( ( ) =>
123- createWaveformPath ( segmentRange ( ) , props . micWaveform ) ,
124- ) ;
125- const systemPath = createMemo ( ( ) =>
126- createWaveformPath ( segmentRange ( ) , props . systemWaveform ) ,
127- ) ;
131+ const { timelineBounds } = useTimelineContext ( ) ;
128132
129133 let canvas : HTMLCanvasElement | undefined ;
134+ let rafId : number | null = null ;
135+ let lastRenderKey = "" ;
130136
131- createEffect ( ( ) => {
137+ const renderCanvas = ( ) => {
138+ rafId = null ;
132139 if ( ! canvas ) return ;
133140 const ctx = canvas . getContext ( "2d" ) ;
134141 if ( ! ctx ) return ;
135142
136- const canvasWidth = Math . max ( width ( ) , 1 ) ;
143+ const segmentDuration = props . segment . end - props . segment . start ;
144+ const fullSegmentWidth = width ( ) ;
145+
146+ if ( fullSegmentWidth < 1 || segmentDuration <= 0 ) {
147+ return ;
148+ }
149+
150+ const useVirtualization = fullSegmentWidth > MAX_CANVAS_WIDTH ;
151+
152+ let canvasWidth : number ;
153+ let leftOffsetPx : number ;
154+ let renderWidth : number ;
155+ let renderSegment : { start : number ; end : number } ;
156+
157+ if ( useVirtualization ) {
158+ const viewportWidth = timelineBounds . width ?? 800 ;
159+ const transform = editorState . timeline . transform ;
160+ const viewStart = transform . position ;
161+ const viewEnd = viewStart + transform . zoom ;
162+
163+ const segStart = props . segmentOffset ;
164+ const segEnd = segStart + segmentDuration ;
165+
166+ const visibleStart = Math . max ( viewStart , segStart ) ;
167+ const visibleEnd = Math . min ( viewEnd , segEnd ) ;
168+
169+ if ( visibleEnd <= visibleStart ) {
170+ canvas . width = 1 ;
171+ canvas . style . left = "0px" ;
172+ canvas . style . width = "1px" ;
173+ return ;
174+ }
175+
176+ const visibleStartInSegment = visibleStart - segStart ;
177+ const visibleEndInSegment = visibleEnd - segStart ;
178+
179+ const pxPerSec = fullSegmentWidth / segmentDuration ;
180+ const visibleWidthPx = Math . min (
181+ ( visibleEndInSegment - visibleStartInSegment ) * pxPerSec ,
182+ viewportWidth + 200 ,
183+ ) ;
184+
185+ canvasWidth = Math . min (
186+ Math . max ( Math . ceil ( visibleWidthPx ) , 1 ) ,
187+ MAX_CANVAS_WIDTH ,
188+ ) ;
189+ leftOffsetPx = visibleStartInSegment * pxPerSec ;
190+ renderWidth = visibleWidthPx ;
191+ renderSegment = {
192+ start : props . segment . start + visibleStartInSegment ,
193+ end : props . segment . start + visibleEndInSegment ,
194+ } ;
195+ } else {
196+ canvasWidth = Math . max ( Math . ceil ( fullSegmentWidth ) , 1 ) ;
197+ leftOffsetPx = 0 ;
198+ renderWidth = fullSegmentWidth ;
199+ renderSegment = {
200+ start : props . segment . start ,
201+ end : props . segment . end ,
202+ } ;
203+ }
204+
205+ const renderKey = `${ canvasWidth } -${ renderSegment . start . toFixed ( 2 ) } -${ renderSegment . end . toFixed ( 2 ) } ` ;
206+ if ( renderKey === lastRenderKey ) {
207+ return ;
208+ }
209+ lastRenderKey = renderKey ;
210+
137211 canvas . width = canvasWidth ;
212+ canvas . style . left = `${ leftOffsetPx } px` ;
213+ canvas . style . width = `${ renderWidth } px` ;
214+
138215 const canvasHeight = canvas . height ;
139216 ctx . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
140217
141- const drawPath = (
142- path : Path2D | undefined ,
218+ const numSamples = Math . min (
219+ Math . ceil ( canvasWidth * SAMPLES_PER_PIXEL ) ,
220+ MAX_WAVEFORM_SAMPLES ,
221+ ) ;
222+
223+ const drawWaveform = (
224+ waveform : number [ ] | undefined ,
143225 color : string ,
144226 gain ?: number ,
145227 ) => {
228+ const path = createWaveformPath ( renderSegment , waveform , numSamples ) ;
146229 if ( ! path ) return ;
147230 const scale = gainToScale ( gain ) ;
148231 if ( scale <= 0 ) return ;
@@ -156,16 +239,59 @@ function WaveformCanvas(props: {
156239 ctx . restore ( ) ;
157240 } ;
158241
159- drawPath ( micPath ( ) , "rgba(255,255,255,0.4)" , project . audio . micVolumeDb ) ;
160- drawPath ( systemPath ( ) , "rgba(255,150,0,0.5)" , project . audio . systemVolumeDb ) ;
242+ drawWaveform (
243+ props . micWaveform ,
244+ "rgba(255,255,255,0.4)" ,
245+ project . audio . micVolumeDb ,
246+ ) ;
247+ drawWaveform (
248+ props . systemWaveform ,
249+ "rgba(255,150,0,0.5)" ,
250+ project . audio . systemVolumeDb ,
251+ ) ;
252+ } ;
253+
254+ createEffect ( ( ) => {
255+ width ( ) ;
256+ timelineBounds . width ;
257+ editorState . timeline . transform . position ;
258+ editorState . timeline . transform . zoom ;
259+ props . segment . start ;
260+ props . segment . end ;
261+ props . micWaveform ;
262+ props . systemWaveform ;
263+ project . audio . micVolumeDb ;
264+ project . audio . systemVolumeDb ;
265+
266+ if ( rafId !== null ) {
267+ cancelAnimationFrame ( rafId ) ;
268+ }
269+ rafId = requestAnimationFrame ( renderCanvas ) ;
270+ } ) ;
271+
272+ onMount ( ( ) => {
273+ setTimeout ( ( ) => {
274+ lastRenderKey = "" ;
275+ if ( rafId !== null ) {
276+ cancelAnimationFrame ( rafId ) ;
277+ }
278+ rafId = requestAnimationFrame ( renderCanvas ) ;
279+ } , 300 ) ;
280+ } ) ;
281+
282+ onCleanup ( ( ) => {
283+ if ( rafId !== null ) {
284+ cancelAnimationFrame ( rafId ) ;
285+ }
161286 } ) ;
162287
163288 return (
164289 < canvas
165290 ref = { ( el ) => {
166291 canvas = el ;
167292 } }
168- class = "absolute inset-0 w-full h-full pointer-events-none"
293+ class = "absolute top-0 h-full pointer-events-none"
294+ style = { { left : "0px" } }
169295 height = { CANVAS_HEIGHT }
170296 />
171297 ) ;
@@ -511,6 +637,7 @@ export function ClipTrack(
511637 micWaveform = { micWaveform ( ) }
512638 systemWaveform = { systemAudioWaveform ( ) }
513639 segment = { segment ( ) }
640+ segmentOffset = { prevDuration ( ) }
514641 />
515642 ) }
516643
@@ -764,35 +891,40 @@ function Markings(props: { segment: TimelineSegment; prevDuration: number }) {
764891 const { editorState } = useEditorContext ( ) ;
765892 const { secsPerPixel, markingResolution } = useTimelineContext ( ) ;
766893
767- const markings = ( ) => {
768- const resolution = markingResolution ( ) ;
894+ const transform = ( ) => editorState . timeline . transform ;
769895
770- const { transform } = editorState . timeline ;
896+ const markingParams = ( ) => {
897+ const resolution = markingResolution ( ) ;
771898 const visibleMin =
772- transform . position - props . prevDuration + props . segment . start ;
773- const visibleMax = visibleMin + transform . zoom ;
774-
899+ transform ( ) . position - props . prevDuration + props . segment . start ;
900+ const visibleMax = visibleMin + transform ( ) . zoom ;
775901 const start = Math . floor ( visibleMin / resolution ) ;
902+ const count = Math . ceil ( visibleMax / resolution ) - start ;
903+ return { resolution, start, count } ;
904+ } ;
776905
777- return Array . from (
778- { length : Math . ceil ( visibleMax / resolution ) - start } ,
779- ( _ , i ) => ( start + i ) * resolution ,
780- ) ;
906+ const getMarkingTime = ( index : number ) => {
907+ const { resolution, start } = markingParams ( ) ;
908+ return ( start + index ) * resolution ;
781909 } ;
782910
783911 return (
784- < For each = { markings ( ) } >
785- { ( marking ) => (
786- < div
787- style = { {
788- transform : `translateX(${
789- ( marking - props . segment . start ) / secsPerPixel ( )
790- } px)`,
791- } }
792- class = "absolute z-10 w-px h-12 bg-gradient-to-b from-transparent to-transparent via-white-transparent-40 dark:via-black-transparent-60"
793- />
794- ) }
795- </ For >
912+ < Index each = { Array . from ( { length : markingParams ( ) . count } ) } >
913+ { ( _ , index ) => {
914+ const marking = ( ) => getMarkingTime ( index ) ;
915+ const translateX = ( ) =>
916+ ( marking ( ) - props . segment . start ) / secsPerPixel ( ) ;
917+
918+ return (
919+ < div
920+ style = { {
921+ transform : `translateX(${ translateX ( ) } px)` ,
922+ } }
923+ class = "absolute z-10 w-px h-12 bg-gradient-to-b from-transparent to-transparent via-white-transparent-40 dark:via-black-transparent-60"
924+ />
925+ ) ;
926+ } }
927+ </ Index >
796928 ) ;
797929}
798930
0 commit comments