@@ -83,9 +83,75 @@ export function useWaveform(params: {
8383
8484 const useHalfWaveform = ( ) => ( runtime . setting ?. waveformMode ?? 'half' ) !== 'full'
8585
86+ const drawFineWaveform = ( width : number , height : number ) => {
87+ if ( ! soundCloudMinMaxData || ! audioBuffer ) {
88+ clearCanvases ( )
89+ return
90+ }
91+
92+ const totalBars = soundCloudMinMaxData . length
93+ if ( ! totalBars ) {
94+ clearCanvases ( )
95+ return
96+ }
97+
98+ if ( ! baseCtx || ! progressCtx ) throw new Error ( 'canvas context is null' )
99+
100+ const pixelRatio = window . devicePixelRatio || 1
101+ resizeCanvas ( baseCanvas , baseCtx , width , height , pixelRatio )
102+ resizeCanvas ( progressCanvas , progressCtx , width , height , pixelRatio )
103+
104+ const spacing = width / totalBars
105+ const gap = Math . min ( barGap , spacing * 0.25 )
106+ let drawWidth = spacing - gap
107+ drawWidth = Math . min ( barWidth , drawWidth )
108+ if ( drawWidth <= 0 ) {
109+ drawWidth = spacing || 1
110+ }
111+ drawWidth = Math . max ( 0.2 , Math . min ( drawWidth , spacing ) )
112+ const offset = spacing > drawWidth ? ( spacing - drawWidth ) / 2 : 0
113+
114+ const midY = height / 2
115+ const isHalf = useHalfWaveform ( )
116+ const baselineY = isHalf ? height : midY
117+ const scaleY = isHalf ? baselineY : midY
118+
119+ const baseGradient = baseCtx . createLinearGradient ( 0 , 0 , 0 , height )
120+ baseGradient . addColorStop ( 0 , '#cccccc' )
121+ baseGradient . addColorStop ( 1 , '#cccccc' )
122+
123+ const progressGradient = progressCtx . createLinearGradient ( 0 , 0 , 0 , height )
124+ progressGradient . addColorStop ( 0 , '#0078d4' )
125+ progressGradient . addColorStop ( 1 , '#0078d4' )
126+
127+ baseCtx . fillStyle = baseGradient
128+ progressCtx . fillStyle = progressGradient
129+
130+ for ( let index = 0 ; index < totalBars ; index ++ ) {
131+ const { min, max } = soundCloudMinMaxData [ index ]
132+ const x = index * spacing + offset
133+ const clampedX = Math . max ( 0 , Math . min ( width - drawWidth , x ) )
134+
135+ if ( isHalf ) {
136+ const amplitude = Math . max ( Math . abs ( min ) , Math . abs ( max ) )
137+ const rectHeight = Math . max ( 1 , amplitude * scaleY )
138+ const y = baselineY - rectHeight
139+ baseCtx . fillRect ( clampedX , y , drawWidth , rectHeight )
140+ progressCtx . fillRect ( clampedX , y , drawWidth , rectHeight )
141+ } else {
142+ const barMin = midY + min * midY
143+ const barMax = midY + max * midY
144+ const rectHeight = Math . max ( 1 , barMax - barMin )
145+ baseCtx . fillRect ( clampedX , barMin , drawWidth , rectHeight )
146+ progressCtx . fillRect ( clampedX , barMin , drawWidth , rectHeight )
147+ }
148+ }
149+ }
150+
86151 const computeColumnMetrics = (
87152 columnCount : number ,
88- waveformData : RGBWaveformData | null
153+ waveformData : RGBWaveformData | null ,
154+ skipDownsample = false
89155 ) : ColumnMetrics [ ] => {
90156 if ( ! waveformData || columnCount <= 0 ) return [ ]
91157
@@ -99,10 +165,10 @@ export function useWaveform(params: {
99165
100166 const duration = waveformData . duration || 0
101167 const targetPoints =
102- duration > 0
103- ? Math . max ( 1 , Math . floor ( duration * REKORDBOX_MINI_POINTS_PER_SECOND ) )
104- : totalPoints
105- const reductionStride = Math . max ( 1 , Math . floor ( totalPoints / targetPoints ) )
168+ skipDownsample || duration <= 0
169+ ? totalPoints
170+ : Math . max ( 1 , Math . floor ( duration * REKORDBOX_MINI_POINTS_PER_SECOND ) )
171+ const reductionStride = skipDownsample ? 1 : Math . max ( 1 , Math . floor ( totalPoints / targetPoints ) )
106172 const effectiveTotalPoints = Math . max ( 1 , Math . floor ( totalPoints / reductionStride ) )
107173
108174 const samplesPerColumn = Math . max ( 1 , effectiveTotalPoints / Math . max ( 1 , columnCount ) )
@@ -286,6 +352,7 @@ export function useWaveform(params: {
286352
287353 let animationFrameId : number | null = null
288354 let audioBuffer : AudioBuffer | null = null
355+ let soundCloudMinMaxData : Array < { min : number ; max : number } > | null = null
289356 let hoverEl : HTMLElement | null = null
290357 let isPointerDown = false
291358 type AudioEventName = keyof WebAudioPlayerEvents
@@ -426,7 +493,7 @@ export function useWaveform(params: {
426493 const targetBarWidth = style === WAVEFORM_STYLE_FINE ? fineBarWidth : barWidth
427494 const targetGap = style === WAVEFORM_STYLE_FINE ? barGap * 0.3 : barGap
428495 const columnCount = Math . max ( 1 , Math . floor ( width / ( targetBarWidth + targetGap ) ) )
429- const columns = computeColumnMetrics ( columnCount , waveformData )
496+ const columns = computeColumnMetrics ( columnCount , waveformData , style === WAVEFORM_STYLE_FINE )
430497 if ( ! columns . length ) {
431498 clearCanvases ( )
432499 return
@@ -532,14 +599,19 @@ export function useWaveform(params: {
532599 return
533600 }
534601
602+ const style = getWaveformStyle ( )
603+ if ( style === WAVEFORM_STYLE_FINE ) {
604+ drawFineWaveform ( width , height )
605+ return
606+ }
607+
535608 const waveformData = player . rgbWaveformData ?? null
536609 if ( ! waveformData ) {
537610 clearCanvases ( )
538611 return
539612 }
540613
541- const style = getWaveformStyle ( )
542- if ( style === WAVEFORM_STYLE_SOUND_CLOUD || style === WAVEFORM_STYLE_FINE ) {
614+ if ( style === WAVEFORM_STYLE_SOUND_CLOUD ) {
543615 drawBarWaveform ( width , height , style , waveformData )
544616 } else {
545617 drawRgbStyleWaveform ( width , height , waveformData )
@@ -551,10 +623,12 @@ export function useWaveform(params: {
551623 const updateWaveform = ( ) => {
552624 if ( ! audioPlayer . value ) return
553625 const player = audioPlayer . value
626+ const style = getWaveformStyle ( )
554627 const buffer = player . audioBuffer
555628
556629 if ( ! buffer ) {
557630 audioBuffer = null
631+ soundCloudMinMaxData = null
558632 clearCanvases ( )
559633 updateProgressVisual ( 0 )
560634 syncHoverOverlay ( 0 )
@@ -563,6 +637,32 @@ export function useWaveform(params: {
563637
564638 audioBuffer = buffer
565639
640+ if ( style === WAVEFORM_STYLE_FINE ) {
641+ const channelData = buffer . getChannelData ( 0 )
642+ const samples = channelData . length
643+ const downsampleFactor = Math . max ( 1 , Math . floor ( samples / 2000 ) )
644+
645+ const length = Math . min ( 5000 , Math . floor ( samples / downsampleFactor ) )
646+ soundCloudMinMaxData = new Array ( length )
647+ for ( let i = 0 ; i < length ; i ++ ) {
648+ const start = i * downsampleFactor
649+ const end = Math . min ( start + downsampleFactor , samples )
650+ let min = 1
651+ let max = - 1
652+ for ( let j = start ; j < end ; j ++ ) {
653+ const v = channelData [ j ]
654+ if ( v < min ) min = v
655+ if ( v > max ) max = v
656+ }
657+ soundCloudMinMaxData [ i ] = { min, max }
658+ }
659+
660+ drawWaveform ( true )
661+ return
662+ }
663+
664+ soundCloudMinMaxData = null
665+
566666 if ( player . rgbWaveformData ) {
567667 drawWaveform ( true )
568668 return
0 commit comments