danielrosehill's picture
commit
0138286
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>
);
}