133 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|