forked from averyfelts/Lofi_Generator
work in progress implementation of: - mixer with channel strips, faders, pan knobs, level meters - timeline with ruler, playhead, sections, keyframe tracks - pattern and progression pickers for drums/chords - automation lanes and mute tracks - loop bracket for loop region selection - export modal placeholder known issues: - drum pattern changes don't update audio engine - timeline keyframes not connected to scheduler - some UI bugs remain this is a checkpoint commit for further iteration
209 lines
6.9 KiB
TypeScript
209 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { TransportControls } from './TransportControls';
|
|
import { Visualizer } from './Visualizer';
|
|
import { Mixer } from '@/components/mixer';
|
|
import { Timeline, ExportModal } from '@/components/timeline';
|
|
import { useAudioEngine } from '@/hooks/useAudioEngine';
|
|
import { useTimeline } from '@/hooks/useTimeline';
|
|
import { Slider } from '@/components/ui/slider';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Download, ChevronDown, ChevronUp } from 'lucide-react';
|
|
import { LayerName } from '@/types/audio';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export function LofiGenerator() {
|
|
const {
|
|
state,
|
|
currentStep,
|
|
meterLevels,
|
|
pans,
|
|
soloed,
|
|
playheadPosition,
|
|
togglePlayback,
|
|
generateNewBeat,
|
|
setMasterVolume,
|
|
setLayerVolume,
|
|
toggleMute,
|
|
setPan,
|
|
toggleSolo,
|
|
setBpm,
|
|
setSwing,
|
|
seek,
|
|
setLoopRegion,
|
|
} = useAudioEngine();
|
|
|
|
const timeline = useTimeline();
|
|
|
|
const [showTimeline, setShowTimeline] = useState(false);
|
|
const [showExportModal, setShowExportModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
timeline.setPlayheadPosition(playheadPosition.bar, playheadPosition.beat);
|
|
}, [playheadPosition, timeline.setPlayheadPosition]);
|
|
|
|
const handleVolumeChange = useCallback(
|
|
(layer: LayerName | 'master', volume: number) => {
|
|
if (layer === 'master') {
|
|
setMasterVolume(volume);
|
|
} else {
|
|
setLayerVolume(layer, volume);
|
|
}
|
|
},
|
|
[setMasterVolume, setLayerVolume]
|
|
);
|
|
|
|
const handleExport = useCallback(async (): Promise<Blob> => {
|
|
return new Blob(['placeholder'], { type: 'audio/wav' });
|
|
}, []);
|
|
|
|
const handleLoopRegionChange = useCallback(
|
|
(start: number, end: number, enabled: boolean) => {
|
|
timeline.setLoopRegion(start, end, enabled);
|
|
setLoopRegion(start, end, enabled);
|
|
},
|
|
[timeline.setLoopRegion, setLoopRegion]
|
|
);
|
|
|
|
const volumes = {
|
|
master: state.volumes.master,
|
|
drums: state.volumes.drums,
|
|
chords: state.volumes.chords,
|
|
ambient: state.volumes.ambient,
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<Card className="w-full max-w-4xl bg-card/80 backdrop-blur-sm border-border/50">
|
|
<CardHeader className="text-center pb-2">
|
|
<CardTitle className="text-2xl font-light tracking-wide">
|
|
lofi generator
|
|
</CardTitle>
|
|
<p className="text-sm text-muted-foreground">beats to relax/study to</p>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4">
|
|
<Visualizer currentStep={currentStep} isPlaying={state.isPlaying} />
|
|
|
|
<div className="flex justify-center items-center gap-2">
|
|
<TransportControls
|
|
isPlaying={state.isPlaying}
|
|
isInitialized={state.isInitialized}
|
|
onTogglePlayback={togglePlayback}
|
|
onGenerateNewBeat={generateNewBeat}
|
|
/>
|
|
<Button
|
|
variant={showTimeline ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setShowTimeline(!showTimeline)}
|
|
className="gap-1 h-9"
|
|
>
|
|
Timeline
|
|
{showTimeline ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowExportModal(true)}
|
|
className="h-9 w-9 p-0"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<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={100}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<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>
|
|
|
|
<Mixer
|
|
volumes={volumes}
|
|
pans={pans}
|
|
muted={state.muted}
|
|
soloed={soloed}
|
|
levels={meterLevels}
|
|
onVolumeChange={handleVolumeChange}
|
|
onPanChange={setPan}
|
|
onMuteToggle={toggleMute}
|
|
onSoloToggle={toggleSolo}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'overflow-hidden transition-all duration-300 ease-in-out',
|
|
showTimeline ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'
|
|
)}
|
|
>
|
|
<Timeline
|
|
state={timeline.state}
|
|
bpm={state.bpm}
|
|
isPlaying={state.isPlaying}
|
|
onDurationChange={timeline.setDuration}
|
|
onSeek={seek}
|
|
onLoopRegionChange={handleLoopRegionChange}
|
|
onSectionAdd={timeline.addSection}
|
|
onSectionResize={timeline.resizeSection}
|
|
onSectionMove={timeline.moveSection}
|
|
onSectionDelete={timeline.deleteSection}
|
|
onDrumKeyframeAdd={timeline.addDrumKeyframe}
|
|
onDrumKeyframeUpdate={timeline.updateDrumKeyframe}
|
|
onDrumKeyframeDelete={timeline.deleteDrumKeyframe}
|
|
onChordKeyframeAdd={timeline.addChordKeyframe}
|
|
onChordKeyframeUpdate={timeline.updateChordKeyframe}
|
|
onChordKeyframeDelete={timeline.deleteChordKeyframe}
|
|
onMuteKeyframeToggle={timeline.toggleMuteKeyframe}
|
|
onAutomationPointAdd={timeline.addAutomationPoint}
|
|
onAutomationPointUpdate={timeline.updateAutomationPoint}
|
|
onAutomationPointDelete={timeline.deleteAutomationPoint}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{showExportModal && (
|
|
<ExportModal
|
|
durationBars={timeline.state.durationBars}
|
|
bpm={state.bpm}
|
|
onExport={handleExport}
|
|
onClose={() => setShowExportModal(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|