Spaces:
Sleeping
Sleeping
File size: 5,526 Bytes
5008b66 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
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<HTMLCanvasElement>(null);
const animationRef = useRef<number>();
const [fftData, setFftData] = useState<number[]>([]);
const [animatedFftData, setAnimatedFftData] = useState<number[]>([]);
const lastUpdateRef = useRef<number>(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 (
<div className={`flex flex-col gap-2 p-4 bg-slate-950 rounded-lg border border-slate-700 ${className}`}>
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-slate-300 uppercase">
Frequency Spectrum
</h3>
<span className={isActive ? 'text-green-400 text-xs' : 'text-slate-500 text-xs'}>
{isActive ? '● Live' : '○ Offline'}
</span>
</div>
<canvas
ref={canvasRef}
width={600}
height={150}
className="w-full border border-slate-700 rounded bg-black"
/>
</div>
);
}
|