'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, responsive: true, height: 50, normalize: true, backend: 'WebAudio', mediaControls: false, }); // 尝试加载音频,如果失败则使用模拟数据 setIsLoading(true); setHasError(false); wavesurfer.current.load(audioUrl).catch(() => { console.warn('音频文件加载失败,使用模拟数据'); setHasError(true); setIsLoading(false); // 如果加载失败,创建空的音频上下文用于演示 if (wavesurfer.current) { wavesurfer.current.empty(); // 创建模拟的峰值数据 const peaks = Array.from({ length: 1000 }, () => Math.random() * 2 - 1); wavesurfer.current.loadDecodedBuffer({ getChannelData: () => new Float32Array(peaks), length: peaks.length, sampleRate: 44100, numberOfChannels: 1, duration: 120 } as any); } }); // 事件监听 wavesurfer.current.on('ready', () => { setDuration(wavesurfer.current?.getDuration() || 120); setIsLoading(false); if (wavesurfer.current) { wavesurfer.current.setVolume(volume / 100); } }); wavesurfer.current.on('audioprocess', () => { setCurrentTime(wavesurfer.current?.getCurrentTime() || 0); }); wavesurfer.current.on('seek', () => { 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 改变时 useEffect(() => { if (wavesurfer.current && !hasError) { wavesurfer.current.setOptions({ waveColor: isActive ? '#3b82f6' : '#6b7280', progressColor: isActive ? '#1d4ed8' : '#374151', }); } }, [isActive, hasError]); // 更新音量 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 ? ( ) : ( )}
{title} {hasError && (Demo)}
Audio track
{volume}%
{/* 波形可视化 */}
{hasError ? ( // 显示模拟波形图片
{mockWaveformUrl && ( Audio waveform )} {/* 进度条覆盖层 */}
{/* 播放游标 */}
) : (
)} {isLoading && !hasError && (
)}
{/* 控制栏 */}
{/* 播放/暂停按钮 */} {isPlaying ? ( ) : ( )} {/* 静音按钮 */} {isMuted ? ( ) : ( )} {/* 时间显示 */}
{formatTime(currentTime)} / {formatTime(duration)}
{/* 音量控制 */}
{ const newVolume = parseInt(e.target.value); onVolumeChange?.(newVolume); }} className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer" style={{ background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${volume}%, rgba(255,255,255,0.2) ${volume}%, rgba(255,255,255,0.2) 100%)` }} /> {volume}%
{/* 播放状态指示器 */} {isPlaying && ( )} {/* 错误提示 */} {hasError && (
演示模式 - 使用模拟音频数据
)}
); }