AJ50's picture
Initial voice cloning backend with all dependencies
5008b66
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>
);
}