| import React, { useEffect, useRef } from "react"; |
|
|
| |
| |
| |
| |
| export default function AudioVisualizer({ stream, isActive = true }) { |
| const canvasRef = useRef(null); |
| const animationRef = useRef(null); |
| const analyserRef = useRef(null); |
| const audioContextRef = useRef(null); |
|
|
| useEffect(() => { |
| if (!stream || !isActive) { |
| |
| if (animationRef.current) { |
| cancelAnimationFrame(animationRef.current); |
| } |
| return; |
| } |
|
|
| const canvas = canvasRef.current; |
| if (!canvas) return; |
|
|
| const ctx = canvas.getContext("2d"); |
|
|
| |
| const dpr = window.devicePixelRatio || 1; |
| const rect = canvas.getBoundingClientRect(); |
| canvas.width = rect.width * dpr; |
| canvas.height = rect.height * dpr; |
| ctx.scale(dpr, dpr); |
|
|
| |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| audioContextRef.current = audioContext; |
|
|
| const analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 256; |
| analyser.smoothingTimeConstant = 0.8; |
| analyserRef.current = analyser; |
|
|
| |
| const source = audioContext.createMediaStreamSource(stream); |
| source.connect(analyser); |
|
|
| const bufferLength = analyser.frequencyBinCount; |
| const dataArray = new Uint8Array(bufferLength); |
|
|
| |
| let phase = 0; |
| const baseAmplitude = rect.height * 0.3; |
|
|
| const draw = () => { |
| if (!isActive) return; |
|
|
| animationRef.current = requestAnimationFrame(draw); |
| analyser.getByteFrequencyData(dataArray); |
|
|
| |
| ctx.fillStyle = "rgba(15, 15, 15, 0.15)"; |
| ctx.fillRect(0, 0, rect.width, rect.height); |
|
|
| |
| const average = dataArray.reduce((a, b) => a + b, 0) / bufferLength; |
| const normalizedVolume = average / 255; |
|
|
| |
| const waves = 3; |
| for (let w = 0; w < waves; w++) { |
| ctx.beginPath(); |
|
|
| const waveOffset = (w / waves) * Math.PI * 0.5; |
| const opacity = 0.4 + (w / waves) * 0.4; |
|
|
| |
| const hue = 187 + w * 15; |
| ctx.strokeStyle = `hsla(${hue}, 85%, 55%, ${opacity})`; |
| ctx.lineWidth = 2 + (waves - w); |
| ctx.lineCap = "round"; |
| ctx.lineJoin = "round"; |
|
|
| const centerY = rect.height / 2; |
| const points = 50; |
|
|
| for (let i = 0; i <= points; i++) { |
| const x = (i / points) * rect.width; |
|
|
| |
| const dataIndex = Math.floor((i / points) * bufferLength); |
| const frequency = dataArray[dataIndex] / 255; |
|
|
| |
| const wave1 = Math.sin((i / points) * Math.PI * 4 + phase + waveOffset) * 0.6; |
| const wave2 = Math.sin((i / points) * Math.PI * 2 + phase * 0.7) * 0.3; |
| const wave3 = Math.sin((i / points) * Math.PI * 6 + phase * 1.3) * 0.1; |
|
|
| const combinedWave = wave1 + wave2 + wave3; |
| const amplitude = baseAmplitude * (0.2 + normalizedVolume * 0.8 + frequency * 0.5); |
|
|
| const y = centerY + combinedWave * amplitude; |
|
|
| if (i === 0) { |
| ctx.moveTo(x, y); |
| } else { |
| |
| const prevX = ((i - 1) / points) * rect.width; |
| const cpX = (prevX + x) / 2; |
| ctx.quadraticCurveTo(prevX, ctx.currentY || y, cpX, y); |
| } |
| ctx.currentY = y; |
| } |
|
|
| ctx.stroke(); |
| } |
|
|
| |
| const glowRadius = 30 + normalizedVolume * 40; |
| const gradient = ctx.createRadialGradient( |
| rect.width / 2, rect.height / 2, 0, |
| rect.width / 2, rect.height / 2, glowRadius |
| ); |
| gradient.addColorStop(0, `rgba(34, 211, 238, ${0.3 * normalizedVolume})`); |
| gradient.addColorStop(1, "rgba(34, 211, 238, 0)"); |
|
|
| ctx.fillStyle = gradient; |
| ctx.beginPath(); |
| ctx.arc(rect.width / 2, rect.height / 2, glowRadius, 0, Math.PI * 2); |
| ctx.fill(); |
|
|
| |
| phase += 0.05 + normalizedVolume * 0.1; |
| }; |
|
|
| draw(); |
|
|
| return () => { |
| if (animationRef.current) { |
| cancelAnimationFrame(animationRef.current); |
| } |
| if (audioContextRef.current && audioContextRef.current.state !== "closed") { |
| audioContextRef.current.close(); |
| } |
| }; |
| }, [stream, isActive]); |
|
|
| return ( |
| <canvas |
| ref={canvasRef} |
| className="w-full h-12 rounded-lg" |
| style={{ |
| background: "linear-gradient(180deg, rgba(15,15,15,0.9) 0%, rgba(20,20,20,0.95) 100%)" |
| }} |
| /> |
| ); |
| } |
|
|