'use client'
import { useRef, useEffect, useState, useCallback } from 'react'
import { cn } from '@/lib/ui-styles'
interface AudioVisualizerProps {
audioElement: HTMLAudioElement | null
isPlaying: boolean
size?: 'sm' | 'md' | 'lg'
coverImage?: string | null
showCoverOnPause?: boolean
className?: string
}
// 默认封面图组件 - Apple Music 风格
function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
const sizeClasses = {
sm: 'w-full h-full',
md: 'w-full h-full',
lg: 'w-full h-full',
}
const iconSizes = {
sm: 'w-8 h-8',
md: 'w-12 h-12',
lg: 'w-16 h-16',
}
return (
{/* 装饰光晕 */}
{/* 中心音符图标 */}
)
}
// 圆形放射状频谱可视化器 - 尖刺风格
export default function AudioVisualizer({
audioElement,
isPlaying,
size = 'md',
coverImage,
showCoverOnPause = true,
className,
}: AudioVisualizerProps) {
const canvasRef = useRef(null)
const animationRef = useRef(null)
const audioContextRef = useRef(null)
const analyserRef = useRef(null)
const sourceRef = useRef(null)
const connectedAudioRef = useRef(null)
const [isVisualizerActive, setIsVisualizerActive] = useState(false)
const [showCover, setShowCover] = useState(true)
// 尺寸配置 - 增加频谱条数量以获得更密集的尖刺效果
const sizeConfig = {
sm: { width: 160, height: 160, bars: 64, innerRadius: 25, maxBarHeight: 35 },
md: { width: 280, height: 280, bars: 96, innerRadius: 45, maxBarHeight: 65 },
lg: { width: 400, height: 400, bars: 128, innerRadius: 70, maxBarHeight: 95 },
}
const config = sizeConfig[size]
// 初始化音频分析器
const initAudioContext = useCallback(() => {
if (!audioElement) return
const audioElementChanged = connectedAudioRef.current !== audioElement
try {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)()
}
const audioContext = audioContextRef.current
if (audioElementChanged || !analyserRef.current) {
if (sourceRef.current) {
try {
sourceRef.current.disconnect()
} catch (e) {
// 忽略断开连接的错误
}
sourceRef.current = null
}
const analyser = audioContext.createAnalyser()
analyser.fftSize = 512 // 增加 FFT 大小以获得更精细的频率数据
analyser.smoothingTimeConstant = 0.75
const source = audioContext.createMediaElementSource(audioElement)
source.connect(analyser)
analyser.connect(audioContext.destination)
sourceRef.current = source
analyserRef.current = analyser
connectedAudioRef.current = audioElement
console.log('[AudioVisualizer] Audio context initialized for new audio element')
}
setIsVisualizerActive(true)
} catch (error) {
console.error('[AudioVisualizer] Failed to initialize audio context:', error)
}
}, [audioElement])
// 绘制尖刺放射状频谱
const draw = useCallback(() => {
const canvas = canvasRef.current
const analyser = analyserRef.current
if (!canvas || !analyser) return
const ctx = canvas.getContext('2d', { willReadFrequently: true })
if (!ctx) return
const bufferLength = analyser.frequencyBinCount
const dataArray = new Uint8Array(bufferLength)
analyser.getByteFrequencyData(dataArray)
const { width, height, bars, innerRadius, maxBarHeight } = config
const centerX = width / 2
const centerY = height / 2
// 清除画布
ctx.clearRect(0, 0, width, height)
// 绘制深色背景圆
const bgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius + maxBarHeight + 20)
bgGradient.addColorStop(0, 'rgba(15, 23, 42, 0.95)')
bgGradient.addColorStop(0.4, 'rgba(15, 23, 42, 0.9)')
bgGradient.addColorStop(1, 'rgba(15, 23, 42, 0.7)')
ctx.fillStyle = bgGradient
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius + maxBarHeight + 15, 0, Math.PI * 2)
ctx.fill()
// 绘制频谱尖刺
const angleStep = (Math.PI * 2) / bars
const usableData = Math.floor(bufferLength * 0.7)
for (let i = 0; i < bars; i++) {
// 使用多个频率数据点的平均值来平滑
const dataIndex = Math.floor((i / bars) * usableData)
const nextIndex = Math.min(dataIndex + 1, usableData - 1)
const value = (dataArray[dataIndex] + dataArray[nextIndex]) / 2
// 添加一些随机变化使效果更自然
const randomFactor = 0.9 + Math.random() * 0.2
const barHeight = Math.max(3, (value / 255) * maxBarHeight * randomFactor)
const angle = i * angleStep - Math.PI / 2 // 从顶部开始
// 计算尖刺的起点和终点
const x1 = centerX + Math.cos(angle) * innerRadius
const y1 = centerY + Math.sin(angle) * innerRadius
const x2 = centerX + Math.cos(angle) * (innerRadius + barHeight)
const y2 = centerY + Math.sin(angle) * (innerRadius + barHeight)
// 创建渐变色 - 使用项目的蓝色/青色主题
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
const intensity = value / 255
// 根据强度调整颜色
if (intensity > 0.7) {
// 高强度 - 亮青色到白色
gradient.addColorStop(0, 'rgba(34, 211, 238, 0.9)')
gradient.addColorStop(0.5, 'rgba(56, 189, 248, 1)')
gradient.addColorStop(1, 'rgba(255, 255, 255, 1)')
} else if (intensity > 0.4) {
// 中等强度 - 蓝色到青色
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.8)')
gradient.addColorStop(0.5, 'rgba(34, 211, 238, 0.9)')
gradient.addColorStop(1, 'rgba(56, 189, 248, 1)')
} else {
// 低强度 - 深蓝到蓝色
gradient.addColorStop(0, 'rgba(30, 64, 175, 0.6)')
gradient.addColorStop(0.5, 'rgba(59, 130, 246, 0.7)')
gradient.addColorStop(1, 'rgba(96, 165, 250, 0.8)')
}
// 绘制尖刺线条
ctx.beginPath()
ctx.strokeStyle = gradient
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
// 在高强度时添加发光效果
if (intensity > 0.5) {
ctx.beginPath()
ctx.strokeStyle = `rgba(56, 189, 248, ${intensity * 0.3})`
ctx.lineWidth = 4
ctx.lineCap = 'round'
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
}
}
// 绘制内圈发光效果
const innerGlow = ctx.createRadialGradient(centerX, centerY, innerRadius - 10, centerX, centerY, innerRadius + 5)
innerGlow.addColorStop(0, 'rgba(56, 189, 248, 0.1)')
innerGlow.addColorStop(0.5, 'rgba(34, 211, 238, 0.15)')
innerGlow.addColorStop(1, 'rgba(34, 211, 238, 0)')
ctx.fillStyle = innerGlow
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius + 5, 0, Math.PI * 2)
ctx.fill()
// 绘制中心圆 - 深色背景
const centerGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius - 5)
centerGradient.addColorStop(0, 'rgba(15, 23, 42, 1)')
centerGradient.addColorStop(0.7, 'rgba(15, 23, 42, 0.95)')
centerGradient.addColorStop(1, 'rgba(30, 41, 59, 0.9)')
ctx.fillStyle = centerGradient
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
ctx.fill()
// 绘制中心圆边框
ctx.strokeStyle = 'rgba(56, 189, 248, 0.3)'
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
ctx.stroke()
// 绘制中心音符图标
ctx.fillStyle = 'rgba(56, 189, 248, 0.9)'
ctx.font = `${innerRadius * 0.5}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('♪', centerX, centerY)
animationRef.current = requestAnimationFrame(draw)
}, [config])
// 处理播放状态变化
useEffect(() => {
if (isPlaying && audioElement) {
setShowCover(false)
if (!audioContextRef.current || connectedAudioRef.current !== audioElement) {
initAudioContext()
}
if (audioContextRef.current?.state === 'suspended') {
audioContextRef.current.resume()
}
requestAnimationFrame(() => {
if (analyserRef.current) {
draw()
}
})
} else if (!isPlaying) {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
animationRef.current = null
}
if (showCoverOnPause) {
setShowCover(true)
}
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [isPlaying, audioElement, initAudioContext, draw, showCoverOnPause])
// 清理
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [])
const containerClasses = {
sm: 'w-40 h-40',
md: 'w-[280px] h-[280px]',
lg: 'w-[400px] h-[400px]',
}
return (
{/* 封面图层 */}
{coverImage ? (
// eslint-disable-next-line @next/next/no-img-element

) : (
)}
{/* 可视化层 */}
{/* 未播放时的默认背景(当没有封面图时) */}
{!coverImage && !isPlaying && !showCover && (
)}
)
}