'use client'; import React, { useRef, useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { Play, Pause, Volume2, VolumeX, AlertCircle } from 'lucide-react'; import { cn } from '@/public/lib/utils'; import WaveSurfer from 'wavesurfer.js'; interface AudioVisualizerProps { audioUrl?: string; title?: string; volume?: number; isActive?: boolean; className?: string; onVolumeChange?: (volume: number) => void; } // 模拟波形数据生成器 const generateMockWaveform = (width = 300, height = 50) => { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d')!; // 绘制模拟波形 ctx.fillStyle = '#6b7280'; const barWidth = 2; const gap = 1; const numBars = Math.floor(width / (barWidth + gap)); for (let i = 0; i < numBars; i++) { const x = i * (barWidth + gap); const barHeight = Math.random() * height * 0.8 + height * 0.1; const y = (height - barHeight) / 2; ctx.fillRect(x, y, barWidth, barHeight); } return canvas.toDataURL(); }; export function AudioVisualizer({ audioUrl = '/audio/demo.mp3', title = 'Background Music', volume = 75, isActive = false, className, onVolumeChange }: AudioVisualizerProps) { const waveformRef = useRef(null); const wavesurfer = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [isMuted, setIsMuted] = useState(false); const [duration, setDuration] = useState(120); // 默认2分钟 const [currentTime, setCurrentTime] = useState(0); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); const [mockWaveformUrl, setMockWaveformUrl] = useState(''); // 生成模拟波形 useEffect(() => { if (waveformRef.current) { const width = waveformRef.current.clientWidth || 300; const mockUrl = generateMockWaveform(width, 50); setMockWaveformUrl(mockUrl); } }, [isActive]); // 初始化 Wavesurfer useEffect(() => { if (!waveformRef.current) return; // 创建 wavesurfer 实例 wavesurfer.current = WaveSurfer.create({ container: waveformRef.current, waveColor: isActive ? '#3b82f6' : '#6b7280', progressColor: isActive ? '#1d4ed8' : '#374151', cursorColor: '#ffffff', barWidth: 2, barRadius: 3, height: 50, normalize: true, backend: 'WebAudio', mediaControls: false, }); // 尝试加载音频,如果失败则使用模拟数据 setIsLoading(true); setHasError(false); wavesurfer.current.load(audioUrl).catch(() => { console.warn('音频文件加载失败,使用模拟数据'); setHasError(true); setIsLoading(false); setDuration(120); // Set a default duration for demo mode }); // 事件监听 wavesurfer.current.on('ready', () => { setDuration(wavesurfer.current?.getDuration() || 120); setIsLoading(false); if (wavesurfer.current) { wavesurfer.current.setVolume(volume / 100); } }); wavesurfer.current.on('timeupdate', () => { setCurrentTime(wavesurfer.current?.getCurrentTime() || 0); }); wavesurfer.current.on('play', () => { setIsPlaying(true); }); wavesurfer.current.on('pause', () => { setIsPlaying(false); }); wavesurfer.current.on('finish', () => { setIsPlaying(false); setCurrentTime(0); }); wavesurfer.current.on('error', (error) => { console.warn('Wavesurfer error:', error); setHasError(true); setIsLoading(false); }); return () => { if (wavesurfer.current) { wavesurfer.current.destroy(); } }; }, [audioUrl]); // 更新波形颜色当 isActive 改变时 // Note: setOptions method might not be available in all WaveSurfer versions // Colors are already set during initialization // 更新音量 useEffect(() => { if (wavesurfer.current && !hasError) { wavesurfer.current.setVolume(isMuted ? 0 : volume / 100); } }, [volume, isMuted, hasError]); // 模拟播放进度(当使用模拟数据时) useEffect(() => { let interval: NodeJS.Timeout; if (isPlaying && hasError) { interval = setInterval(() => { setCurrentTime(prev => { const next = prev + 1; if (next >= duration) { setIsPlaying(false); return 0; } return next; }); }, 1000); } return () => clearInterval(interval); }, [isPlaying, hasError, duration]); const togglePlayPause = () => { if (hasError) { // 模拟播放/暂停 setIsPlaying(!isPlaying); } else if (wavesurfer.current) { wavesurfer.current.playPause(); } }; const toggleMute = () => { setIsMuted(!isMuted); }; const formatTime = (time: number) => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0; return (
{/* 标题和音量 */}
{hasError ? ( ) : ( )}
Audio & SFX
{/* 波形可视化 */}
{hasError ? ( // 显示模拟波形图片
{mockWaveformUrl && ( Audio waveform )} {/* 进度条覆盖层 */}
{/* 播放游标 */}
) : (
)} {isLoading && !hasError && (
)}
{/* 控制栏 */}
{/* 播放/暂停按钮 */} {isPlaying ? ( ) : ( )} {/* 静音按钮 */} {isMuted ? ( ) : ( )} {/* 时间显示 */}
{formatTime(currentTime)} / {formatTime(duration)}
{/* 播放状态指示器 */} {isPlaying && ( )}
); }