@@ -30,6 +30,69 @@ import {
3030 useSegmentWidth ,
3131} from "./Track" ;
3232
33+ const CANVAS_HEIGHT = 52 ;
34+ const WAVEFORM_MIN_DB = - 60 ;
35+ const WAVEFORM_SAMPLE_STEP = 0.1 ;
36+ const WAVEFORM_CONTROL_STEP = 0.05 ;
37+ const WAVEFORM_PADDING_SECONDS = 0.3 ;
38+
39+ function gainToScale ( gain ?: number ) {
40+ if ( ! Number . isFinite ( gain ) ) return 1 ;
41+ const value = gain as number ;
42+ if ( value <= WAVEFORM_MIN_DB ) return 0 ;
43+ return Math . max ( 0 , 1 + value / - WAVEFORM_MIN_DB ) ;
44+ }
45+
46+ function createWaveformPath (
47+ segment : { start : number ; end : number } ,
48+ waveform ?: number [ ] ,
49+ ) {
50+ if ( typeof Path2D === "undefined" ) return ;
51+ if ( ! waveform || waveform . length === 0 ) return ;
52+
53+ const duration = Math . max ( segment . end - segment . start , WAVEFORM_SAMPLE_STEP ) ;
54+ if ( ! Number . isFinite ( duration ) || duration <= 0 ) return ;
55+
56+ const path = new Path2D ( ) ;
57+ path . moveTo ( 0 , 1 ) ;
58+
59+ const amplitudeAt = ( index : number ) => {
60+ const sample = waveform [ index ] ;
61+ const db =
62+ typeof sample === "number" && Number . isFinite ( sample )
63+ ? sample
64+ : WAVEFORM_MIN_DB ;
65+ const clamped = Math . max ( db , WAVEFORM_MIN_DB ) ;
66+ const amplitude = 1 + clamped / - WAVEFORM_MIN_DB ;
67+ return Math . min ( Math . max ( amplitude , 0 ) , 1 ) ;
68+ } ;
69+
70+ const controlStep = Math . min ( WAVEFORM_CONTROL_STEP / duration , 0.25 ) ;
71+
72+ for (
73+ let time = segment . start ;
74+ time <= segment . end + WAVEFORM_SAMPLE_STEP ;
75+ time += WAVEFORM_SAMPLE_STEP
76+ ) {
77+ const index = Math . floor ( time * 10 ) ;
78+ const normalizedX = ( index / 10 - segment . start ) / duration ;
79+ const prevX =
80+ ( index / 10 - WAVEFORM_SAMPLE_STEP - segment . start ) / duration ;
81+ const y = 1 - amplitudeAt ( index ) ;
82+ const prevY = 1 - amplitudeAt ( index - 1 ) ;
83+ const cpX1 = prevX + controlStep / 2 ;
84+ const cpX2 = normalizedX - controlStep / 2 ;
85+ path . bezierCurveTo ( cpX1 , prevY , cpX2 , y , normalizedX , y ) ;
86+ }
87+
88+ const closingX =
89+ ( segment . end + WAVEFORM_PADDING_SECONDS - segment . start ) / duration ;
90+ path . lineTo ( closingX , 1 ) ;
91+ path . closePath ( ) ;
92+
93+ return path ;
94+ }
95+
3396function formatTime ( totalSeconds : number ) : string {
3497 const hours = Math . floor ( totalSeconds / 3600 ) ;
3598 const minutes = Math . floor ( ( totalSeconds % 3600 ) / 60 ) ;
@@ -48,115 +111,61 @@ function WaveformCanvas(props: {
48111 systemWaveform ?: number [ ] ;
49112 micWaveform ?: number [ ] ;
50113 segment : { start : number ; end : number } ;
51- secsPerPixel : number ;
52114} ) {
53115 const { project } = useEditorContext ( ) ;
54-
55- let canvas : HTMLCanvasElement | undefined ;
56116 const { width } = useSegmentContext ( ) ;
57- const { secsPerPixel } = useTimelineContext ( ) ;
58-
59- const render = (
60- ctx : CanvasRenderingContext2D ,
61- h : number ,
62- waveform : number [ ] ,
63- color : string ,
64- gain = 0 ,
65- ) => {
66- const maxAmplitude = h ;
67-
68- // yellow please
69- ctx . fillStyle = color ;
70- ctx . beginPath ( ) ;
71-
72- const step = 0.05 / secsPerPixel ( ) ;
73-
74- ctx . moveTo ( 0 , h ) ;
75-
76- const norm = ( w : number ) => {
77- const ww = Number . isFinite ( w ) ? w : - 60 ;
78- return 1.0 - Math . max ( ww + gain , - 60 ) / - 60 ;
79- } ;
80-
81- for (
82- let segmentTime = props . segment . start ;
83- segmentTime <= props . segment . end + 0.1 ;
84- segmentTime += 0.1
85- ) {
86- const index = Math . floor ( segmentTime * 10 ) ;
87- const xTime = index / 10 ;
88-
89- const currentDb =
90- typeof waveform [ index ] === "number" ? waveform [ index ] : - 60 ;
91- const amplitude = norm ( currentDb ) * maxAmplitude ;
92-
93- const x = ( xTime - props . segment . start ) / secsPerPixel ( ) ;
94- const y = h - amplitude ;
95-
96- const prevX = ( xTime - 0.1 - props . segment . start ) / secsPerPixel ( ) ;
97- const prevDb =
98- typeof waveform [ index - 1 ] === "number" ? waveform [ index - 1 ] : - 60 ;
99- const prevAmplitude = norm ( prevDb ) * maxAmplitude ;
100- const prevY = h - prevAmplitude ;
101-
102- const cpX1 = prevX + step / 2 ;
103- const cpX2 = x - step / 2 ;
104-
105- ctx . bezierCurveTo ( cpX1 , prevY , cpX2 , y , x , y ) ;
106- }
107-
108- ctx . lineTo (
109- ( props . segment . end + 0.3 - props . segment . start ) / secsPerPixel ( ) ,
110- h ,
111- ) ;
117+ const segmentRange = createMemo ( ( ) => ( {
118+ start : props . segment . start ,
119+ end : props . segment . end ,
120+ } ) ) ;
121+ const micPath = createMemo ( ( ) =>
122+ createWaveformPath ( segmentRange ( ) , props . micWaveform ) ,
123+ ) ;
124+ const systemPath = createMemo ( ( ) =>
125+ createWaveformPath ( segmentRange ( ) , props . systemWaveform ) ,
126+ ) ;
112127
113- ctx . closePath ( ) ;
114- ctx . fill ( ) ;
115- } ;
128+ let canvas : HTMLCanvasElement | undefined ;
116129
117- function renderWaveforms ( ) {
130+ createEffect ( ( ) => {
118131 if ( ! canvas ) return ;
119132 const ctx = canvas . getContext ( "2d" ) ;
120133 if ( ! ctx ) return ;
121134
122- const w = width ( ) ;
123- if ( w <= 0 ) return ;
124-
125- const h = canvas . height ;
126- canvas . width = w ;
127- ctx . clearRect ( 0 , 0 , w , h ) ;
128-
129- if ( props . micWaveform )
130- render (
131- ctx ,
132- h ,
133- props . micWaveform ,
134- "rgba(255,255,255,0.4)" ,
135- project . audio . micVolumeDb ,
136- ) ;
137-
138- if ( props . systemWaveform )
139- render (
140- ctx ,
141- h ,
142- props . systemWaveform ,
143- "rgba(255,150,0,0.5)" ,
144- project . audio . systemVolumeDb ,
145- ) ;
146- }
135+ const canvasWidth = Math . max ( width ( ) , 1 ) ;
136+ canvas . width = canvasWidth ;
137+ const canvasHeight = canvas . height ;
138+ ctx . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
139+
140+ const drawPath = (
141+ path : Path2D | undefined ,
142+ color : string ,
143+ gain ?: number ,
144+ ) => {
145+ if ( ! path ) return ;
146+ const scale = gainToScale ( gain ) ;
147+ if ( scale <= 0 ) return ;
148+ ctx . save ( ) ;
149+ ctx . translate ( 0 , - 1 ) ;
150+ ctx . scale ( 1 , scale ) ;
151+ ctx . translate ( 0 , 1 ) ;
152+ ctx . scale ( canvasWidth , canvasHeight ) ;
153+ ctx . fillStyle = color ;
154+ ctx . fill ( path ) ;
155+ ctx . restore ( ) ;
156+ } ;
147157
148- createEffect ( ( ) => {
149- renderWaveforms ( ) ;
158+ drawPath ( micPath ( ) , "rgba(255,255,255,0.4)" , project . audio . micVolumeDb ) ;
159+ drawPath ( systemPath ( ) , "rgba(255,150,0,0.5)" , project . audio . systemVolumeDb ) ;
150160 } ) ;
151161
152162 return (
153163 < canvas
154164 ref = { ( el ) => {
155165 canvas = el ;
156- renderWaveforms ( ) ;
157166 } }
158167 class = "absolute inset-0 w-full h-full pointer-events-none"
159- height = { 52 }
168+ height = { CANVAS_HEIGHT }
160169 />
161170 ) ;
162171}
@@ -184,6 +193,17 @@ export function ClipTrack(
184193 const segments = ( ) : Array < TimelineSegment > =>
185194 project . timeline ?. segments ?? [ { start : 0 , end : duration ( ) , timescale : 1 } ] ;
186195
196+ const segmentOffsets = createMemo ( ( ) => {
197+ const segs = segments ( ) ;
198+ const offsets : number [ ] = new Array ( segs . length ) ;
199+ let sum = 0 ;
200+ for ( let idx = 0 ; idx < segs . length ; idx ++ ) {
201+ offsets [ idx ] = sum ;
202+ sum += ( segs [ idx ] . end - segs [ idx ] . start ) / segs [ idx ] . timescale ;
203+ }
204+ return offsets ;
205+ } ) ;
206+
187207 function onHandleReleased ( ) {
188208 const { transform } = editorState . timeline ;
189209
@@ -210,17 +230,7 @@ export function ClipTrack(
210230 initialStart : number ;
211231 } > ( null ) ;
212232
213- const prefixOffsets = createMemo ( ( ) => {
214- const segs = segments ( ) ;
215- const out : number [ ] = new Array ( segs . length ) ;
216- let sum = 0 ;
217- for ( let k = 0 ; k < segs . length ; k ++ ) {
218- out [ k ] = sum ;
219- sum += ( segs [ k ] . end - segs [ k ] . start ) / segs [ k ] . timescale ;
220- }
221- return out ;
222- } ) ;
223- const prevDuration = createMemo ( ( ) => prefixOffsets ( ) [ i ( ) ] ?? 0 ) ;
233+ const prevDuration = createMemo ( ( ) => segmentOffsets ( ) [ i ( ) ] ?? 0 ) ;
224234
225235 const relativeSegment = createMemo ( ( ) => {
226236 const ds = startHandleDrag ( ) ;
@@ -481,7 +491,6 @@ export function ClipTrack(
481491 micWaveform = { micWaveform ( ) }
482492 systemWaveform = { systemAudioWaveform ( ) }
483493 segment = { segment }
484- secsPerPixel = { secsPerPixel ( ) }
485494 />
486495 ) }
487496
0 commit comments