video-flow-b/components/ui/audio-visualizer.tsx
2025-07-03 05:51:09 +08:00

311 lines
9.2 KiB
TypeScript

'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<HTMLDivElement>(null);
const wavesurfer = useRef<WaveSurfer | null>(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<string>('');
// 生成模拟波形
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 (
<motion.div
className={cn(
'p-4 rounded-lg border-2 bg-white/5 transition-all duration-300',
isActive ? 'border-blue-500 bg-blue-500/10' : 'border-white/20',
className
)}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
>
<div className="space-y-3">
{/* 标题和音量 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-white/10">
{hasError ? (
<AlertCircle className="w-4 h-4 text-yellow-500" />
) : (
<Volume2 className="w-4 h-4" />
)}
</div>
<div>
<div className="text-sm font-medium text-white">
Audio & SFX
</div>
</div>
</div>
</div>
{/* 波形可视化 */}
<div className="relative">
{hasError ? (
// 显示模拟波形图片
<div className="w-full h-[50px] bg-white/5 rounded flex items-center justify-center relative overflow-hidden">
{mockWaveformUrl && (
<img
src={mockWaveformUrl}
alt="Audio waveform"
className="w-full h-full object-cover opacity-70"
/>
)}
{/* 进度条覆盖层 */}
<div
className="absolute left-0 top-0 h-full bg-blue-500/30 transition-all duration-300"
style={{ width: `${progressPercentage}%` }}
/>
{/* 播放游标 */}
<div
className="absolute top-0 bottom-0 w-0.5 bg-white transition-all duration-300"
style={{ left: `${progressPercentage}%` }}
/>
</div>
) : (
<div
ref={waveformRef}
className={cn(
"w-full transition-opacity duration-300",
isLoading ? "opacity-50" : "opacity-100"
)}
/>
)}
{isLoading && !hasError && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
{/* 控制栏 */}
<div className="flex items-center gap-3">
{/* 播放/暂停按钮 */}
<motion.button
className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
onClick={togglePlayPause}
disabled={isLoading && !hasError}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isPlaying ? (
<Pause className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)}
</motion.button>
{/* 静音按钮 */}
<motion.button
className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
onClick={toggleMute}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isMuted ? (
<VolumeX className="w-4 h-4" />
) : (
<Volume2 className="w-4 h-4" />
)}
</motion.button>
{/* 时间显示 */}
<div className="flex-1 text-center">
<span className="text-xs text-white/70">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
</div>
{/* 播放状态指示器 */}
{isPlaying && (
<motion.div
className="h-0.5 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full"
initial={{ scaleX: 0 }}
animate={{ scaleX: [0, 1, 0] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
</div>
</motion.div>
);
}