import type { WaveformTiers } from '$lib/bindings/mediaAnalysis'; import type { TimelineState } from './renderer'; export interface WaveformData { tiers: WaveformTiers | null; } /** * Pick the best tier for the current viewport. * Returns the tier array that gives at least 1 peak per pixel, * preferring the lowest-resolution tier that meets that threshold. */ function pickTier( tiers: WaveformTiers, visibleStart: number, visibleEnd: number, duration: number, widthPx: number ): number[] { const visibleRange = visibleEnd - visibleStart; const visibleFraction = visibleRange / duration; const candidates: [number[], number][] = [ [tiers.tier0, (tiers.tier0.length * visibleFraction) / widthPx], [tiers.tier1, (tiers.tier1.length * visibleFraction) / widthPx], [tiers.tier2, (tiers.tier2.length * visibleFraction) / widthPx], ]; for (const [tier, peaksPerPx] of candidates) { if (peaksPerPx >= 1) return tier; } return tiers.tier2; } export function drawWaveform( ctx: CanvasRenderingContext2D, state: TimelineState, waveform: WaveformData, duration: number, y: number, height: number ): void { if (duration <= 0 || !waveform.tiers) return; const { width, visibleStart, visibleEnd } = state; const visibleRange = visibleEnd - visibleStart; const peaks = pickTier(waveform.tiers, visibleStart, visibleEnd, duration, width); const peakCount = peaks.length; // Find local maximum across the visible region for dynamic scaling. // This makes the waveform fill the available height when zoomed in, // rather than staying relative to the global max. const visStartIdx = Math.max(0, Math.floor((visibleStart / duration) * peakCount)); const visEndIdx = Math.min(peakCount - 1, Math.ceil((visibleEnd / duration) * peakCount)); let localMax = 0; for (let i = visStartIdx; i <= visEndIdx; i++) { localMax = Math.max(localMax, peaks[i]); } // Avoid division by zero; use 1.0 if all peaks are silent const scale = localMax > 0 ? 1 / localMax : 1; ctx.fillStyle = '#89b4fa44'; ctx.strokeStyle = '#89b4fa88'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, y + height); for (let px = 0; px < width; px++) { const timeStart = visibleStart + ((px - 0.5) / width) * visibleRange; const timeEnd = visibleStart + ((px + 0.5) / width) * visibleRange; const s0 = Math.floor((timeStart / duration) * peakCount); const s1 = Math.floor((timeEnd / duration) * peakCount); let maxPeak = 0; for (let i = Math.max(0, s0); i <= Math.min(s1, peakCount - 1); i++) { maxPeak = Math.max(maxPeak, peaks[i]); } const barHeight = maxPeak * scale * height; ctx.lineTo(px, y + height - barHeight); } ctx.lineTo(width, y + height); ctx.closePath(); ctx.fill(); ctx.stroke(); }