File size: 4,658 Bytes
0138286 |
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 |
import React, { useState, useCallback } from 'react';
import { AudioInput } from './components/AudioInput';
import { ResultsView } from './components/ResultsView';
import { ApiKeySettings } from './components/ApiKeySettings';
import { analyzeAudio } from './services/geminiService';
import type { VocalProfile, EQSetting } from './types';
type Status = 'idle' | 'processing' | 'success' | 'error';
const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === 'string') {
resolve(reader.result.split(',')[1]);
} else {
reject(new Error('Failed to convert blob to base64'));
}
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
const Header: React.FC = () => (
<header className="py-4 px-6 text-center">
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-brand-blue to-brand-purple">
EQ Template Creator
</h1>
<p className="mt-2 text-lg text-gray-400">Your Personal AI Audio Engineer</p>
</header>
);
const Loader: React.FC<{ message: string }> = ({ message }) => (
<div className="flex flex-col items-center justify-center space-y-4 p-8">
<div className="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-brand-blue"></div>
<p className="text-lg text-gray-300">{message}</p>
</div>
);
export default function App() {
const [status, setStatus] = useState<Status>('idle');
const [error, setError] = useState<string | null>(null);
const [vocalProfile, setVocalProfile] = useState<VocalProfile | null>(null);
const [eqSettings, setEqSettings] = useState<EQSetting[] | null>(null);
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
const [audacityXml, setAudacityXml] = useState<string | null>(null);
const [hasApiKey, setHasApiKey] = useState<boolean>(false);
const handleAudioSubmit = useCallback(async (blob: Blob, mimeType: string) => {
setStatus('processing');
setError(null);
setAudioBlob(blob);
try {
const base64Audio = await blobToBase64(blob);
const result = await analyzeAudio(base64Audio, mimeType);
setVocalProfile(result.vocalProfile);
setEqSettings(result.eqPreset);
setAudacityXml(result.audacityXml);
setStatus('success');
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : 'An unknown error occurred during analysis.');
setStatus('error');
}
}, []);
const handleReset = () => {
setStatus('idle');
setError(null);
setVocalProfile(null);
setEqSettings(null);
setAudioBlob(null);
setAudacityXml(null);
};
const renderContent = () => {
switch (status) {
case 'processing':
return <Loader message="Gemini is analyzing your voice... this may take a moment." />;
case 'success':
return vocalProfile && eqSettings && audioBlob && audacityXml && (
<ResultsView
vocalProfile={vocalProfile}
eqSettings={eqSettings}
audioBlob={audioBlob}
audacityXml={audacityXml}
onReset={handleReset}
/>
);
case 'error':
return (
<div className="text-center p-8 bg-red-900/20 rounded-lg border border-red-500">
<h3 className="text-2xl font-bold text-red-400">Analysis Failed</h3>
<p className="mt-2 text-red-300">{error}</p>
<button
onClick={handleReset}
className="mt-6 px-6 py-2 bg-brand-blue text-white font-semibold rounded-lg hover:bg-opacity-80 transition-all"
>
Try Again
</button>
</div>
);
case 'idle':
default:
return <AudioInput onAudioSubmit={handleAudioSubmit} isProcessing={status === 'processing'}/>;
}
};
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4">
<main className="w-full max-w-4xl mx-auto">
<Header />
<div className="mt-8 mb-6">
<ApiKeySettings onKeyConfigured={setHasApiKey} />
</div>
{hasApiKey && (
<div className="bg-gray-800/50 rounded-2xl shadow-2xl backdrop-blur-sm border border-gray-700">
{renderContent()}
</div>
)}
<footer className="text-center mt-8 text-gray-500 text-sm">
<p>Powered by Google Gemini. For demonstration purposes only.</p>
</footer>
</main>
</div>
);
} |