forked from averyfelts/Lofi_Generator
- Add 4 genres: Hip Hop, Classical, Trap, Pop with unique patterns - Add new instrument layers: Bass, Brass, Piano - Each layer now has 4 instrument variations to choose from - Add genre-specific drum patterns, chord progressions, and melodies - Add duration control (1-10 minutes) - Rename app to "Beat Generator" with modern gradient header - Redesign UI with 2-column instrument grid layout - Add color-coded accent for each instrument section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
'use client';
|
|
|
|
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,
|
|
Music,
|
|
Cloud,
|
|
Guitar,
|
|
Waves,
|
|
Piano,
|
|
} from 'lucide-react';
|
|
import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio';
|
|
|
|
export function LofiGenerator() {
|
|
const {
|
|
state,
|
|
currentStep,
|
|
togglePlayback,
|
|
generateNewBeat,
|
|
setMasterVolume,
|
|
setLayerVolume,
|
|
toggleMute,
|
|
setGenre,
|
|
setDuration,
|
|
setInstrument,
|
|
setBpm,
|
|
setSwing,
|
|
} = useAudioEngine();
|
|
|
|
const genres: { value: Genre; label: string }[] = [
|
|
{ value: 'hiphop', label: 'Hip Hop' },
|
|
{ value: 'classical', label: 'Classical' },
|
|
{ value: 'trap', label: 'Trap' },
|
|
{ value: 'pop', label: 'Pop' },
|
|
];
|
|
|
|
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={10}
|
|
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}
|
|
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={generateNewBeat}
|
|
className="gap-2"
|
|
>
|
|
<Shuffle className="h-4 w-4" />
|
|
Generate
|
|
</Button>
|
|
</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="orange"
|
|
/>
|
|
|
|
<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="blue"
|
|
/>
|
|
|
|
<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={<Music 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="yellow"
|
|
/>
|
|
|
|
<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>
|
|
);
|
|
}
|