'use client'; import * as React from 'react'; import { cn } from '@/lib/utils/cn'; import { generateMinMaxPeaks } from '@/lib/waveform/peaks'; export interface WaveformProps { audioBuffer: AudioBuffer | null; currentTime: number; duration: number; onSeek?: (time: number) => void; className?: string; height?: number; zoom?: number; scrollOffset?: number; amplitudeScale?: number; } export function Waveform({ audioBuffer, currentTime, duration, onSeek, className, height = 128, zoom = 1, scrollOffset = 0, amplitudeScale = 1, }: WaveformProps) { const canvasRef = React.useRef(null); const containerRef = React.useRef(null); const [width, setWidth] = React.useState(800); const [isDragging, setIsDragging] = React.useState(false); // Handle resize React.useEffect(() => { const handleResize = () => { if (containerRef.current) { setWidth(containerRef.current.clientWidth); } }; handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Draw waveform React.useEffect(() => { const canvas = canvasRef.current; if (!canvas || !audioBuffer) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; ctx.scale(dpr, dpr); // Clear canvas ctx.fillStyle = getComputedStyle(canvas).getPropertyValue('--color-waveform-bg') || '#f5f5f5'; ctx.fillRect(0, 0, width, height); // Calculate visible width based on zoom const visibleWidth = Math.floor(width * zoom); // Generate peaks for visible portion const { min, max } = generateMinMaxPeaks(audioBuffer, visibleWidth, 0); // Draw waveform const middle = height / 2; const baseScale = (height / 2) * amplitudeScale; // Waveform color const waveformColor = getComputedStyle(canvas).getPropertyValue('--color-waveform') || '#3b82f6'; const progressColor = getComputedStyle(canvas).getPropertyValue('--color-waveform-progress') || '#10b981'; // Calculate progress position const progressX = duration > 0 ? ((currentTime / duration) * visibleWidth) - scrollOffset : 0; // Draw grid lines (every 1 second) ctx.strokeStyle = 'rgba(128, 128, 128, 0.2)'; ctx.lineWidth = 1; const secondsPerPixel = duration / visibleWidth; const pixelsPerSecond = visibleWidth / duration; for (let sec = 0; sec < duration; sec++) { const x = (sec * pixelsPerSecond) - scrollOffset; if (x >= 0 && x <= width) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } } // Draw waveform with scroll offset const startIdx = Math.max(0, Math.floor(scrollOffset)); const endIdx = Math.min(visibleWidth, Math.floor(scrollOffset + width)); for (let i = startIdx; i < endIdx; i++) { const x = i - scrollOffset; if (x < 0 || x >= width) continue; const minVal = min[i] * baseScale; const maxVal = max[i] * baseScale; // Use different color for played portion ctx.fillStyle = x < progressX ? progressColor : waveformColor; ctx.fillRect( x, middle + minVal, 1, Math.max(1, maxVal - minVal) ); } // Draw center line ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, middle); ctx.lineTo(width, middle); ctx.stroke(); // Draw playhead if (progressX >= 0 && progressX <= width) { ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(progressX, 0); ctx.lineTo(progressX, height); ctx.stroke(); } }, [audioBuffer, width, height, currentTime, duration, zoom, scrollOffset, amplitudeScale]); const handleClick = (e: React.MouseEvent) => { if (!onSeek || !duration || isDragging) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; // Account for zoom and scroll const visibleWidth = width * zoom; const actualX = x + scrollOffset; const clickedTime = (actualX / visibleWidth) * duration; onSeek(clickedTime); }; const handleMouseDown = (e: React.MouseEvent) => { if (!onSeek || !duration) return; setIsDragging(true); handleClick(e); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging || !onSeek || !duration) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; // Account for zoom and scroll const visibleWidth = width * zoom; const actualX = x + scrollOffset; const clickedTime = (actualX / visibleWidth) * duration; onSeek(Math.max(0, Math.min(duration, clickedTime))); }; const handleMouseUp = () => { setIsDragging(false); }; const handleMouseLeave = () => { setIsDragging(false); }; return (
{audioBuffer ? ( ) : (

Load an audio file to see waveform

)}
); }