project-standalo-sonic-cloud/components/WaveformDisplay.tsx

133 lines
3.4 KiB
TypeScript

'use client'
import { useEffect, useRef } from 'react'
export interface WaveformDisplayProps {
audioFile: File | string
height?: number
barWidth?: number
barGap?: number
barColor?: string
progressColor?: string
currentTime?: number
onSeek?: (time: number) => void
}
export function WaveformDisplay({
audioFile,
height = 80,
barWidth = 2,
barGap = 1,
barColor = 'rgb(63 63 70)',
progressColor = 'rgb(168 85 247)',
currentTime = 0,
onSeek
}: WaveformDisplayProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const waveformData = useRef<number[]>([])
useEffect(() => {
if (!audioFile) return
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
const canvas = canvasRef.current
if (!canvas) return
const loadAudio = async () => {
try {
let audioBuffer: ArrayBuffer
if (typeof audioFile === 'string') {
const response = await fetch(audioFile)
audioBuffer = await response.arrayBuffer()
} else {
audioBuffer = await audioFile.arrayBuffer()
}
const decodedData = await audioContext.decodeAudioData(audioBuffer)
const rawData = decodedData.getChannelData(0)
const samples = canvas.width / (barWidth + barGap)
const blockSize = Math.floor(rawData.length / samples)
const filteredData: number[] = []
for (let i = 0; i < samples; i++) {
let blockStart = blockSize * i
let sum = 0
for (let j = 0; j < blockSize; j++) {
sum += Math.abs(rawData[blockStart + j])
}
filteredData.push(sum / blockSize)
}
const multiplier = Math.pow(Math.max(...filteredData), -1)
waveformData.current = filteredData.map(n => n * multiplier)
drawWaveform()
} catch (error) {
console.error('Error loading audio:', error)
}
}
loadAudio()
return () => {
audioContext.close()
}
}, [audioFile])
useEffect(() => {
drawWaveform()
}, [currentTime])
const drawWaveform = () => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
const data = waveformData.current
if (data.length === 0) return
const duration = data.length * (barWidth + barGap) / canvas.width
const progress = currentTime / duration
data.forEach((value, index) => {
const x = index * (barWidth + barGap)
const barHeight = value * height
const y = (height - barHeight) / 2
ctx.fillStyle = index < data.length * progress ? progressColor : barColor
ctx.fillRect(x, y, barWidth, barHeight)
})
}
const handleClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!onSeek) return
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / canvas.width
const duration = waveformData.current.length * (barWidth + barGap) / canvas.width
onSeek(percentage * duration)
}
return (
<div className="relative w-full" style={{ height }}>
<canvas
ref={canvasRef}
width={800}
height={height}
className="w-full h-full cursor-pointer"
onClick={handleClick}
/>
</div>
)
}