import { useEffect, useRef, useState } from 'react'; import api from '@/services/api'; interface FFTVisualizerProps { isActive: boolean; audioFilename?: string; synthesizerStartTime?: number | null; className?: string; } export default function FFTVisualizer({ isActive, audioFilename, synthesizerStartTime, className = "" }: FFTVisualizerProps) { const canvasRef = useRef(null); const animationRef = useRef(); const [fftData, setFftData] = useState([]); const [animatedFftData, setAnimatedFftData] = useState([]); const lastUpdateRef = useRef(0); const BINS = 32; // Fetch and analyze real audio data from backend - SIMPLE useEffect(() => { if (!isActive || !audioFilename) { setFftData([]); setAnimatedFftData([]); return; } const fetchAndAnalyzeAudio = async () => { try { console.log('[FFT] Fetching:', audioFilename); const arrayBuffer = await api.getAudio(audioFilename); console.log('[FFT] Got audio, size:', arrayBuffer.byteLength); // Decode audio const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); const channelData = audioBuffer.getChannelData(0); console.log('[FFT] Decoded, samples:', channelData.length); // SUPER SIMPLE: 32 bins, just max amplitude per bin const samplesPerBin = Math.floor(channelData.length / BINS); const result: number[] = []; for (let i = 0; i < BINS; i++) { const start = i * samplesPerBin; const end = Math.min(start + samplesPerBin, channelData.length); let maxAmp = 0; for (let j = start; j < end; j++) { maxAmp = Math.max(maxAmp, Math.abs(channelData[j])); } result.push(maxAmp * 255); } console.log('[FFT] Result:', result); setFftData(result); lastUpdateRef.current = Date.now(); } catch (err) { console.error('[FFT] Error:', err); } }; fetchAndAnalyzeAudio(); const interval = setInterval(fetchAndAnalyzeAudio, 3000); return () => clearInterval(interval); }, [isActive, audioFilename]); // Smooth animation between FFT updates useEffect(() => { if (!isActive) return; const animate = () => { const nowSec = synthesizerStartTime ? (Date.now() - synthesizerStartTime) / 1000 : Date.now() / 1000; setAnimatedFftData(prev => { // If no real FFT data yet, produce a synchronized placeholder animation if (fftData.length === 0) { const placeholder = new Array(BINS).fill(0).map((_, i) => { const phase = i * 0.35; const val = (Math.sin(nowSec * 2 + phase) + 1) / 2; // 0..1 const env = (Math.sin(nowSec * 0.7 + i * 0.13) + 1) / 2; return Math.min(255, Math.max(0, (val * 0.6 + env * 0.4) * 255 + (Math.random() - 0.5) * 8)); }); return placeholder; } // If we have real data, smoothly animate current bars toward targets if (prev.length === 0) return fftData; const animationSpeed = 0.15; // adjust for faster/slower animation return prev.map((current, i) => { const target = fftData[i] || 0; const diff = target - current; return current + diff * animationSpeed; }); }); animationRef.current = requestAnimationFrame(animate); }; animationRef.current = requestAnimationFrame(animate); return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current); }; }, [isActive, fftData]); // Draw useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width; const height = canvas.height; // Clear ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, width, height); if (animatedFftData.length > 0) { // Draw bars with animation const barWidth = width / animatedFftData.length; for (let i = 0; i < animatedFftData.length; i++) { const magnitude = Math.min(animatedFftData[i], 255); const barHeight = (magnitude / 255) * (height - 20); const x = i * barWidth; const y = height - barHeight - 10; // Simple green color ctx.fillStyle = '#00ff00'; ctx.fillRect(x + 1, y, barWidth - 2, barHeight); } } else if (isActive) { ctx.fillStyle = 'rgba(150, 150, 150, 0.7)'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Loading...', width / 2, height / 2); } }, [animatedFftData, isActive]); return (

Frequency Spectrum

{isActive ? '● Live' : '○ Offline'}
); }