Loading bar now only displays during actual beat generation instead of forcing a fixed 30-second wait time. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
363 lines
13 KiB
TypeScript
363 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Slider } from '@/components/ui/slider';
|
|
import { Label } from '@/components/ui/label';
|
|
import { LayerBox } from './LayerBox';
|
|
import { Visualizer } from './Visualizer';
|
|
import { useAudioEngine } from '@/hooks/useAudioEngine';
|
|
import {
|
|
Play,
|
|
Pause,
|
|
Shuffle,
|
|
Drum,
|
|
Cloud,
|
|
Guitar,
|
|
Waves,
|
|
Piano,
|
|
} from 'lucide-react';
|
|
import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio';
|
|
|
|
// Custom Trumpet icon component
|
|
function TrumpetIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg
|
|
className={className}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M3 12h4l3-3v6l-3-3" />
|
|
<path d="M10 12h8" />
|
|
<circle cx="20" cy="12" r="2" />
|
|
<path d="M7 9V6" />
|
|
<path d="M7 15v3" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function LofiGenerator() {
|
|
const {
|
|
state,
|
|
currentStep,
|
|
togglePlayback,
|
|
generateNewBeat,
|
|
setMasterVolume,
|
|
setLayerVolume,
|
|
toggleMute,
|
|
setGenre,
|
|
setDuration,
|
|
setInstrument,
|
|
setBpm,
|
|
setSwing,
|
|
} = useAudioEngine();
|
|
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const [generationProgress, setGenerationProgress] = useState(0);
|
|
|
|
const genres: { value: Genre; label: string }[] = [
|
|
{ value: 'hiphop', label: 'Hip Hop' },
|
|
{ value: 'classical', label: 'Classical' },
|
|
{ value: 'trap', label: 'Trap' },
|
|
{ value: 'pop', label: 'Pop' },
|
|
];
|
|
|
|
const handleGenerateBeat = async () => {
|
|
setIsGenerating(true);
|
|
setGenerationProgress(0);
|
|
|
|
// Animate progress while generating
|
|
let currentProgress = 0;
|
|
const progressInterval = setInterval(() => {
|
|
const jump = Math.random() > 0.5 ? Math.random() * 15 : Math.random() * 8;
|
|
currentProgress = Math.min(currentProgress + jump, 90);
|
|
setGenerationProgress(currentProgress);
|
|
}, 50);
|
|
|
|
// Generate the beat
|
|
await generateNewBeat();
|
|
|
|
clearInterval(progressInterval);
|
|
setGenerationProgress(100);
|
|
|
|
setTimeout(() => {
|
|
setIsGenerating(false);
|
|
setGenerationProgress(0);
|
|
}, 300);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col items-center p-4 pt-8">
|
|
{/* Modern Header */}
|
|
<header className="w-full max-w-2xl mb-8">
|
|
<div className="flex items-center justify-center gap-3 mb-2">
|
|
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-lofi-orange via-lofi-pink to-lofi-purple flex items-center justify-center">
|
|
<Waves className="h-5 w-5 text-white" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple bg-clip-text text-transparent">
|
|
Beat Generator
|
|
</h1>
|
|
</div>
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
Create custom beats across multiple genres
|
|
</p>
|
|
</header>
|
|
|
|
<Card className="w-full max-w-2xl bg-card/80 backdrop-blur-sm border-border/50">
|
|
<CardContent className="p-6 space-y-6">
|
|
{/* Top Controls: Duration & Genre */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
Duration
|
|
</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Slider
|
|
value={[state.duration]}
|
|
onValueChange={([v]) => setDuration(v)}
|
|
min={1}
|
|
max={3}
|
|
step={1}
|
|
className="flex-1"
|
|
/>
|
|
<span className="text-sm font-mono w-12 text-right">
|
|
{state.duration} min
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
Genre
|
|
</Label>
|
|
<Select value={state.genre} onValueChange={(v) => setGenre(v as Genre)}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{genres.map((g) => (
|
|
<SelectItem key={g.value} value={g.value}>
|
|
{g.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Transport Controls */}
|
|
<div className="flex items-center justify-center gap-4">
|
|
<Button
|
|
size="lg"
|
|
onClick={togglePlayback}
|
|
disabled={isGenerating}
|
|
className="h-14 w-14 rounded-full bg-gradient-to-br from-lofi-orange to-lofi-pink hover:opacity-90"
|
|
>
|
|
{state.isPlaying ? (
|
|
<Pause className="h-6 w-6" />
|
|
) : (
|
|
<Play className="h-6 w-6 ml-1" />
|
|
)}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="secondary"
|
|
size="lg"
|
|
onClick={handleGenerateBeat}
|
|
disabled={isGenerating}
|
|
className="gap-2"
|
|
>
|
|
<Shuffle className="h-4 w-4" />
|
|
Generate
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Generation Progress Bar */}
|
|
{isGenerating && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>Generating beat with song structure...</span>
|
|
<span>{Math.round(generationProgress)}%</span>
|
|
</div>
|
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple transition-all duration-100 ease-out"
|
|
style={{ width: `${generationProgress}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground/60 text-center">
|
|
Creating intro, verse, bridge, chorus, and outro...
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Current Section Indicator */}
|
|
{state.isPlaying && !isGenerating && (
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Now Playing:</span>
|
|
<span className="px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-lofi-orange/20 via-lofi-pink/20 to-lofi-purple/20 border border-lofi-pink/30 text-lofi-pink capitalize">
|
|
{state.currentSection}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Visualizer */}
|
|
<Visualizer currentStep={currentStep} isPlaying={state.isPlaying} />
|
|
|
|
{/* Master Controls */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">Master</Label>
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{Math.round(state.volumes.master * 100)}%
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[state.volumes.master]}
|
|
onValueChange={([v]) => setMasterVolume(v)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">BPM</Label>
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{state.bpm}
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[state.bpm]}
|
|
onValueChange={([v]) => setBpm(v)}
|
|
min={60}
|
|
max={180}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">Swing</Label>
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{Math.round(state.swing * 100)}%
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[state.swing]}
|
|
onValueChange={([v]) => setSwing(v)}
|
|
min={0}
|
|
max={0.5}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Instrument Layers */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
Instruments
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<LayerBox
|
|
title="Drums"
|
|
icon={<Drum className="h-4 w-4" />}
|
|
volume={state.volumes.drums}
|
|
muted={state.muted.drums}
|
|
instrument={state.instruments.drums}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.drums}
|
|
onVolumeChange={(v) => setLayerVolume('drums', v)}
|
|
onToggleMute={() => toggleMute('drums')}
|
|
onInstrumentChange={(v) => setInstrument('drums', v)}
|
|
accentColor="grey"
|
|
/>
|
|
|
|
<LayerBox
|
|
title="Bass"
|
|
icon={<Guitar className="h-4 w-4" />}
|
|
volume={state.volumes.bass}
|
|
muted={state.muted.bass}
|
|
instrument={state.instruments.bass}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.bass}
|
|
onVolumeChange={(v) => setLayerVolume('bass', v)}
|
|
onToggleMute={() => toggleMute('bass')}
|
|
onInstrumentChange={(v) => setInstrument('bass', v)}
|
|
accentColor="red"
|
|
/>
|
|
|
|
<LayerBox
|
|
title="Piano"
|
|
icon={<Piano className="h-4 w-4" />}
|
|
volume={state.volumes.piano}
|
|
muted={state.muted.piano}
|
|
instrument={state.instruments.piano}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.piano}
|
|
onVolumeChange={(v) => setLayerVolume('piano', v)}
|
|
onToggleMute={() => toggleMute('piano')}
|
|
onInstrumentChange={(v) => setInstrument('piano', v)}
|
|
accentColor="green"
|
|
/>
|
|
|
|
<LayerBox
|
|
title="Brass"
|
|
icon={<TrumpetIcon className="h-4 w-4" />}
|
|
volume={state.volumes.brass}
|
|
muted={state.muted.brass}
|
|
instrument={state.instruments.brass}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.brass}
|
|
onVolumeChange={(v) => setLayerVolume('brass', v)}
|
|
onToggleMute={() => toggleMute('brass')}
|
|
onInstrumentChange={(v) => setInstrument('brass', v)}
|
|
accentColor="babyblue"
|
|
/>
|
|
|
|
<LayerBox
|
|
title="Chords"
|
|
icon={<Waves className="h-4 w-4" />}
|
|
volume={state.volumes.chords}
|
|
muted={state.muted.chords}
|
|
instrument={state.instruments.chords}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.chords}
|
|
onVolumeChange={(v) => setLayerVolume('chords', v)}
|
|
onToggleMute={() => toggleMute('chords')}
|
|
onInstrumentChange={(v) => setInstrument('chords', v)}
|
|
accentColor="pink"
|
|
/>
|
|
|
|
<LayerBox
|
|
title="Ambient"
|
|
icon={<Cloud className="h-4 w-4" />}
|
|
volume={state.volumes.ambient}
|
|
muted={state.muted.ambient}
|
|
instrument={state.instruments.ambient}
|
|
instrumentOptions={INSTRUMENT_OPTIONS.ambient}
|
|
onVolumeChange={(v) => setLayerVolume('ambient', v)}
|
|
onToggleMute={() => toggleMute('ambient')}
|
|
onInstrumentChange={(v) => setInstrument('ambient', v)}
|
|
accentColor="purple"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<p className="text-center text-xs text-muted-foreground/60 pt-2">
|
|
Click play to initialize audio engine • Genre: {GENRE_CONFIG[state.genre].name}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|