diff --git a/components/lofi-generator/LayerBox.tsx b/components/lofi-generator/LayerBox.tsx
index 69e636e..ea26685 100644
--- a/components/lofi-generator/LayerBox.tsx
+++ b/components/lofi-generator/LayerBox.tsx
@@ -13,8 +13,8 @@ import { Volume2, VolumeX } from 'lucide-react';
import { ReactNode } from 'react';
interface InstrumentOption {
- value: string;
- label: string;
+ readonly value: string;
+ readonly label: string;
}
interface LayerBoxProps {
@@ -23,19 +23,37 @@ interface LayerBoxProps {
volume: number;
muted: boolean;
instrument: string;
- instrumentOptions: InstrumentOption[];
+ instrumentOptions: readonly InstrumentOption[];
onVolumeChange: (volume: number) => void;
onToggleMute: () => void;
onInstrumentChange: (instrument: string) => void;
- accentColor: 'orange' | 'pink' | 'purple' | 'blue' | 'green' | 'yellow';
+ accentColor: 'grey' | 'red' | 'green' | 'babyblue' | 'pink' | 'purple';
}
const accentStyles = {
- orange: {
- border: 'border-lofi-orange/30 hover:border-lofi-orange/50',
- bg: 'bg-lofi-orange/5',
- icon: 'text-lofi-orange',
- slider: '[&_[data-slot=slider-range]]:bg-lofi-orange [&_[data-slot=slider-thumb]]:border-lofi-orange',
+ grey: {
+ border: 'border-slate-400/30 hover:border-slate-400/50',
+ bg: 'bg-slate-300/10',
+ icon: 'text-slate-400',
+ slider: '[&_[data-slot=slider-range]]:bg-slate-400 [&_[data-slot=slider-thumb]]:border-slate-400',
+ },
+ red: {
+ border: 'border-rose-400/30 hover:border-rose-400/50',
+ bg: 'bg-rose-400/8',
+ icon: 'text-rose-400',
+ slider: '[&_[data-slot=slider-range]]:bg-rose-400 [&_[data-slot=slider-thumb]]:border-rose-400',
+ },
+ green: {
+ border: 'border-emerald-400/30 hover:border-emerald-400/50',
+ bg: 'bg-gradient-to-r from-emerald-400/10 to-white/5',
+ icon: 'text-emerald-400',
+ slider: '[&_[data-slot=slider-range]]:bg-emerald-400 [&_[data-slot=slider-thumb]]:border-emerald-400',
+ },
+ babyblue: {
+ border: 'border-sky-300/30 hover:border-sky-300/50',
+ bg: 'bg-sky-300/8',
+ icon: 'text-sky-300',
+ slider: '[&_[data-slot=slider-range]]:bg-sky-300 [&_[data-slot=slider-thumb]]:border-sky-300',
},
pink: {
border: 'border-lofi-pink/30 hover:border-lofi-pink/50',
@@ -49,24 +67,6 @@ const accentStyles = {
icon: 'text-lofi-purple',
slider: '[&_[data-slot=slider-range]]:bg-lofi-purple [&_[data-slot=slider-thumb]]:border-lofi-purple',
},
- blue: {
- border: 'border-blue-400/30 hover:border-blue-400/50',
- bg: 'bg-blue-400/5',
- icon: 'text-blue-400',
- slider: '[&_[data-slot=slider-range]]:bg-blue-400 [&_[data-slot=slider-thumb]]:border-blue-400',
- },
- green: {
- border: 'border-emerald-400/30 hover:border-emerald-400/50',
- bg: 'bg-emerald-400/5',
- icon: 'text-emerald-400',
- slider: '[&_[data-slot=slider-range]]:bg-emerald-400 [&_[data-slot=slider-thumb]]:border-emerald-400',
- },
- yellow: {
- border: 'border-yellow-400/30 hover:border-yellow-400/50',
- bg: 'bg-yellow-400/5',
- icon: 'text-yellow-400',
- slider: '[&_[data-slot=slider-range]]:bg-yellow-400 [&_[data-slot=slider-thumb]]:border-yellow-400',
- },
};
export function LayerBox({
diff --git a/components/lofi-generator/LofiGenerator.tsx b/components/lofi-generator/LofiGenerator.tsx
index 1bd15b8..64cebd4 100644
--- a/components/lofi-generator/LofiGenerator.tsx
+++ b/components/lofi-generator/LofiGenerator.tsx
@@ -1,5 +1,6 @@
'use client';
+import { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
@@ -19,7 +20,6 @@ import {
Pause,
Shuffle,
Drum,
- Music,
Cloud,
Guitar,
Waves,
@@ -27,6 +27,27 @@ import {
} from 'lucide-react';
import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio';
+// Custom Trumpet icon component
+function TrumpetIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
export function LofiGenerator() {
const {
state,
@@ -43,6 +64,9 @@ export function LofiGenerator() {
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' },
@@ -50,6 +74,38 @@ export function LofiGenerator() {
{ value: 'pop', label: 'Pop' },
];
+ const handleGenerateBeat = async () => {
+ setIsGenerating(true);
+ setGenerationProgress(0);
+
+ // Simulate 30-second generation with intermittent progress
+ const totalDuration = 30000; // 30 seconds
+ const updateInterval = 100; // Update every 100ms
+ const steps = totalDuration / updateInterval;
+ let currentProgress = 0;
+
+ const progressInterval = setInterval(() => {
+ // Intermittent progress - sometimes jumps, sometimes slow
+ const jump = Math.random() > 0.7 ? Math.random() * 5 : Math.random() * 2;
+ currentProgress = Math.min(currentProgress + (100 / steps) + jump, 99);
+ setGenerationProgress(currentProgress);
+ }, updateInterval);
+
+ // Actually generate the beat
+ await generateNewBeat();
+
+ // Wait for the full 30 seconds
+ await new Promise((resolve) => setTimeout(resolve, totalDuration));
+
+ clearInterval(progressInterval);
+ setGenerationProgress(100);
+
+ setTimeout(() => {
+ setIsGenerating(false);
+ setGenerationProgress(0);
+ }, 500);
+ };
+
return (
{/* Modern Header */}
@@ -80,7 +136,7 @@ export function LofiGenerator() {
value={[state.duration]}
onValueChange={([v]) => setDuration(v)}
min={1}
- max={10}
+ max={3}
step={1}
className="flex-1"
/>
@@ -113,6 +169,7 @@ export function LofiGenerator() {
+ {/* Generation Progress Bar */}
+ {isGenerating && (
+
+
+ Generating beat with song structure...
+ {Math.round(generationProgress)}%
+
+
+
+ Creating intro, verse, bridge, chorus, and outro...
+
+
+ )}
+
+ {/* Current Section Indicator */}
+ {state.isPlaying && !isGenerating && (
+
+ Now Playing:
+
+ {state.currentSection}
+
+
+ )}
+
{/* Visualizer */}
@@ -202,7 +289,7 @@ export function LofiGenerator() {
onVolumeChange={(v) => setLayerVolume('drums', v)}
onToggleMute={() => toggleMute('drums')}
onInstrumentChange={(v) => setInstrument('drums', v)}
- accentColor="orange"
+ accentColor="grey"
/>
setLayerVolume('bass', v)}
onToggleMute={() => toggleMute('bass')}
onInstrumentChange={(v) => setInstrument('bass', v)}
- accentColor="blue"
+ accentColor="red"
/>
}
+ icon={}
volume={state.volumes.brass}
muted={state.muted.brass}
instrument={state.instruments.brass}
@@ -241,7 +328,7 @@ export function LofiGenerator() {
onVolumeChange={(v) => setLayerVolume('brass', v)}
onToggleMute={() => toggleMute('brass')}
onInstrumentChange={(v) => setInstrument('brass', v)}
- accentColor="yellow"
+ accentColor="babyblue"
/>
{
+ const eventId = Tone.getTransport().schedule((time) => {
+ Tone.getDraw().schedule(() => {
+ this.applySection(section.type, section.instruments);
+ }, time);
+ }, startTime);
+ this.scheduledEvents.push(eventId);
+ });
+
+ // Set initial section
+ if (timings.length > 0) {
+ this.applySection(timings[0].section.type, timings[0].section.instruments);
+ }
+
+ this.notifyStateChange();
+ }
+
+ private clearScheduledEvents(): void {
+ this.scheduledEvents.forEach(eventId => {
+ Tone.getTransport().clear(eventId);
+ });
+ this.scheduledEvents = [];
+ }
+
+ private applySection(sectionType: SectionType, instruments: {
+ drums: { active: boolean; volume: number };
+ bass: { active: boolean; volume: number };
+ brass: { active: boolean; volume: number };
+ piano: { active: boolean; volume: number };
+ chords: { active: boolean; volume: number };
+ ambient: { active: boolean; volume: number };
+ }): void {
+ this.currentSection = sectionType;
+ this.state.currentSection = sectionType;
+
+ // Apply instrument volumes and mute states for this section
+ // Use smooth transitions for a professional sound
+ const layers: LayerName[] = ['drums', 'bass', 'brass', 'piano', 'chords', 'ambient'];
+
+ layers.forEach(layer => {
+ const config = instruments[layer];
+
+ // Set mute state
+ this.state.muted[layer] = !config.active;
+
+ // Set volume (scaled by user's master settings)
+ const userMasterScale = this.state.volumes.master;
+ const targetVolume = config.active ? config.volume * userMasterScale : 0;
+
+ switch (layer) {
+ case 'drums':
+ this.drumMachine?.mute(!config.active);
+ if (config.active) this.drumMachine?.setVolume(targetVolume);
+ break;
+ case 'bass':
+ this.bassEngine?.mute(!config.active);
+ if (config.active) this.bassEngine?.setVolume(targetVolume);
+ break;
+ case 'brass':
+ this.brassEngine?.mute(!config.active);
+ if (config.active) this.brassEngine?.setVolume(targetVolume);
+ break;
+ case 'piano':
+ this.pianoEngine?.mute(!config.active);
+ if (config.active) this.pianoEngine?.setVolume(targetVolume);
+ break;
+ case 'chords':
+ this.chordEngine?.mute(!config.active);
+ if (config.active) this.chordEngine?.setVolume(targetVolume);
+ break;
+ case 'ambient':
+ this.ambientLayer?.mute(!config.active);
+ if (config.active) this.ambientLayer?.setVolume(targetVolume);
+ break;
+ }
+ });
+
+ // Notify callbacks
+ this.callbacks.onSectionChange?.(sectionType);
this.notifyStateChange();
}
@@ -348,6 +447,7 @@ class AudioEngine {
dispose(): void {
this.stop();
+ this.clearScheduledEvents();
this.drumMachine?.dispose();
this.chordEngine?.dispose();
this.ambientLayer?.dispose();
@@ -369,9 +469,11 @@ class AudioEngine {
this.masterCompressor = null;
this.masterLimiter = null;
this.masterReverb = null;
+ this.songStructure = null;
this.state.isInitialized = false;
this.state.isPlaying = false;
+ this.state.currentSection = 'intro';
AudioEngine.instance = null;
}
}
diff --git a/lib/audio/bassEngine.ts b/lib/audio/bassEngine.ts
index 5223419..6a05b9d 100644
--- a/lib/audio/bassEngine.ts
+++ b/lib/audio/bassEngine.ts
@@ -10,9 +10,11 @@ export class BassEngine {
private filter: Tone.Filter;
private currentType: BassType = 'synth';
private genre: Genre = 'hiphop';
+ private currentVolume: number = 0.6;
+ private isMuted: boolean = false;
constructor(destination: Tone.InputNode) {
- this.output = new Tone.Gain(0.6);
+ this.output = new Tone.Gain(this.currentVolume);
this.filter = new Tone.Filter({
frequency: 800,
type: 'lowpass',
@@ -31,7 +33,7 @@ export class BassEngine {
this.synth.dispose();
}
- const configs: Record> = {
+ const configs: Record = {
synth: {
oscillator: { type: 'sawtooth' },
envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.2 },
@@ -54,7 +56,8 @@ export class BassEngine {
},
};
- this.synth = new Tone.MonoSynth(configs[type]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.synth = new Tone.MonoSynth(configs[type] as any);
this.synth.volume.value = -6;
this.synth.connect(this.filter);
this.currentType = type;
@@ -101,11 +104,15 @@ export class BassEngine {
}
setVolume(volume: number): void {
- this.output.gain.rampTo(volume, 0.1);
+ this.currentVolume = volume;
+ if (!this.isMuted) {
+ this.output.gain.rampTo(volume, 0.1);
+ }
}
mute(muted: boolean): void {
- this.output.gain.rampTo(muted ? 0 : 0.6, 0.1);
+ this.isMuted = muted;
+ this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
dispose(): void {
diff --git a/lib/audio/brassEngine.ts b/lib/audio/brassEngine.ts
index 898542b..daad59a 100644
--- a/lib/audio/brassEngine.ts
+++ b/lib/audio/brassEngine.ts
@@ -11,9 +11,11 @@ export class BrassEngine {
private reverb: Tone.Reverb;
private currentType: BrassType = 'trumpet';
private genre: Genre = 'hiphop';
+ private currentVolume: number = 0.4;
+ private isMuted: boolean = false;
constructor(destination: Tone.InputNode) {
- this.output = new Tone.Gain(0.4);
+ this.output = new Tone.Gain(this.currentVolume);
this.filter = new Tone.Filter({
frequency: 3000,
type: 'lowpass',
@@ -37,7 +39,7 @@ export class BrassEngine {
this.synth.dispose();
}
- const configs: Record> = {
+ const configs: Record = {
trumpet: {
oscillator: { type: 'sawtooth' },
envelope: { attack: 0.05, decay: 0.2, sustain: 0.6, release: 0.3 },
@@ -47,7 +49,7 @@ export class BrassEngine {
envelope: { attack: 0.1, decay: 0.3, sustain: 0.7, release: 0.5 },
},
'synth-brass': {
- oscillator: { type: 'fatsawtooth', spread: 20, count: 3 } as Tone.OmniOscillatorOptions,
+ oscillator: { type: 'fatsawtooth', spread: 20, count: 3 },
envelope: { attack: 0.02, decay: 0.3, sustain: 0.5, release: 0.2 },
},
orchestra: {
@@ -56,7 +58,8 @@ export class BrassEngine {
},
};
- this.synth = new Tone.Synth(configs[type]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.synth = new Tone.Synth(configs[type] as any);
this.synth.volume.value = -8;
this.synth.connect(this.filter);
this.currentType = type;
@@ -103,11 +106,15 @@ export class BrassEngine {
}
setVolume(volume: number): void {
- this.output.gain.rampTo(volume, 0.1);
+ this.currentVolume = volume;
+ if (!this.isMuted) {
+ this.output.gain.rampTo(volume, 0.1);
+ }
}
mute(muted: boolean): void {
- this.output.gain.rampTo(muted ? 0 : 0.4, 0.1);
+ this.isMuted = muted;
+ this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
dispose(): void {
diff --git a/lib/audio/chordEngine.ts b/lib/audio/chordEngine.ts
index 675a85b..102f80f 100644
--- a/lib/audio/chordEngine.ts
+++ b/lib/audio/chordEngine.ts
@@ -12,9 +12,11 @@ export class ChordEngine {
private chorus: Tone.Chorus;
private currentType: ChordType = 'pad';
private genre: Genre = 'hiphop';
+ private currentVolume: number = 0.6;
+ private isMuted: boolean = false;
constructor(destination: Tone.InputNode) {
- this.output = new Tone.Gain(0.6);
+ this.output = new Tone.Gain(this.currentVolume);
this.filter = new Tone.Filter({
frequency: 2000,
@@ -143,11 +145,15 @@ export class ChordEngine {
}
setVolume(volume: number): void {
- this.output.gain.rampTo(volume, 0.1);
+ this.currentVolume = volume;
+ if (!this.isMuted) {
+ this.output.gain.rampTo(volume, 0.1);
+ }
}
mute(muted: boolean): void {
- this.output.gain.rampTo(muted ? 0 : 0.6, 0.1);
+ this.isMuted = muted;
+ this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
setFilterFrequency(freq: number): void {
diff --git a/lib/audio/drumMachine.ts b/lib/audio/drumMachine.ts
index 5379ff2..016ad4c 100644
--- a/lib/audio/drumMachine.ts
+++ b/lib/audio/drumMachine.ts
@@ -16,9 +16,11 @@ export class DrumMachine {
private lowpass: Tone.Filter;
private currentKit: DrumKit = 'acoustic';
private genre: Genre = 'hiphop';
+ private currentVolume: number = 0.8;
+ private isMuted: boolean = false;
constructor(destination: Tone.InputNode) {
- this.output = new Tone.Gain(0.8);
+ this.output = new Tone.Gain(this.currentVolume);
this.lowpass = new Tone.Filter({
frequency: 8000,
type: 'lowpass',
@@ -33,7 +35,6 @@ export class DrumMachine {
}
private createKit(kit: DrumKit): void {
- // Dispose existing instruments
this.kick?.dispose();
this.snare?.dispose();
this.hihat?.dispose();
@@ -43,7 +44,7 @@ export class DrumMachine {
this.openhatFilter?.dispose();
const kitConfigs: Record;
+ kick: object;
snareFilter: number;
hihatFilter: number;
snareDecay: number;
@@ -81,14 +82,13 @@ export class DrumMachine {
const config = kitConfigs[kit];
- // Kick drum
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.kick = new Tone.MembraneSynth({
- ...config.kick,
+ ...(config.kick as any),
oscillator: { type: 'sine' },
});
this.kick.connect(this.lowpass);
- // Snare
this.snareFilter = new Tone.Filter({
frequency: config.snareFilter,
type: 'bandpass',
@@ -101,7 +101,6 @@ export class DrumMachine {
this.snare.connect(this.snareFilter);
this.snareFilter.connect(this.lowpass);
- // Closed hi-hat
this.hihatFilter = new Tone.Filter({
frequency: config.hihatFilter,
type: 'highpass',
@@ -113,7 +112,6 @@ export class DrumMachine {
this.hihat.connect(this.hihatFilter);
this.hihatFilter.connect(this.lowpass);
- // Open hi-hat
this.openhatFilter = new Tone.Filter({
frequency: config.hihatFilter - 2000,
type: 'highpass',
@@ -182,11 +180,15 @@ export class DrumMachine {
}
setVolume(volume: number): void {
- this.output.gain.rampTo(volume, 0.1);
+ this.currentVolume = volume;
+ if (!this.isMuted) {
+ this.output.gain.rampTo(volume, 0.1);
+ }
}
mute(muted: boolean): void {
- this.output.gain.rampTo(muted ? 0 : 0.8, 0.1);
+ this.isMuted = muted;
+ this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
getPattern(): DrumPattern {
diff --git a/lib/audio/patterns.ts b/lib/audio/patterns.ts
index 1b5a548..3044bfe 100644
--- a/lib/audio/patterns.ts
+++ b/lib/audio/patterns.ts
@@ -153,7 +153,7 @@ export const chordProgressions: Record = {
};
// Bass patterns by genre
-export const bassPatterns: Record = {
+export const bassPatterns: Record = {
hiphop: [
['C2', null, null, null, 'C2', null, 'G1', null, 'C2', null, null, null, 'E2', null, null, null],
['A1', null, null, 'A1', null, null, 'G1', null, 'A1', null, null, null, 'E1', null, 'G1', null],
@@ -173,7 +173,7 @@ export const bassPatterns: Record = {
};
// Brass melody patterns by genre
-export const brassPatterns: Record = {
+export const brassPatterns: Record = {
hiphop: [
['C4', null, null, null, 'E4', null, null, null, 'G4', null, null, null, 'E4', null, null, null],
[null, null, 'D4', null, null, null, 'F4', null, null, null, 'A4', null, null, null, 'G4', null],
@@ -193,7 +193,7 @@ export const brassPatterns: Record = {
};
// Piano patterns by genre
-export const pianoPatterns: Record = {
+export const pianoPatterns: Record = {
hiphop: [
[['C4', 'E4', 'G4'], null, null, null, ['D4', 'F4', 'A4'], null, null, null, ['E4', 'G4', 'B4'], null, null, null, ['D4', 'F4', 'A4'], null, null, null],
[['A3', 'C4', 'E4'], null, ['A3', 'C4', 'E4'], null, null, null, ['G3', 'B3', 'D4'], null, ['G3', 'B3', 'D4'], null, null, null, ['F3', 'A3', 'C4'], null, null, null],
diff --git a/lib/audio/pianoEngine.ts b/lib/audio/pianoEngine.ts
index 8b6c56e..b453035 100644
--- a/lib/audio/pianoEngine.ts
+++ b/lib/audio/pianoEngine.ts
@@ -11,9 +11,11 @@ export class PianoEngine {
private reverb: Tone.Reverb;
private currentType: PianoType = 'grand';
private genre: Genre = 'hiphop';
+ private currentVolume: number = 0.5;
+ private isMuted: boolean = false;
constructor(destination: Tone.InputNode) {
- this.output = new Tone.Gain(0.5);
+ this.output = new Tone.Gain(this.currentVolume);
this.filter = new Tone.Filter({
frequency: 5000,
type: 'lowpass',
@@ -37,7 +39,7 @@ export class PianoEngine {
this.synth.dispose();
}
- const configs: Record = {
+ const configs: Record = {
grand: {
synth: Tone.Synth,
options: {
@@ -74,7 +76,7 @@ export class PianoEngine {
const config = configs[type];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- this.synth = new Tone.PolySynth(config.synth as any, config.options);
+ this.synth = new Tone.PolySynth(config.synth as any, config.options as any);
this.synth.volume.value = -10;
this.synth.connect(this.filter);
this.currentType = type;
@@ -121,11 +123,15 @@ export class PianoEngine {
}
setVolume(volume: number): void {
- this.output.gain.rampTo(volume, 0.1);
+ this.currentVolume = volume;
+ if (!this.isMuted) {
+ this.output.gain.rampTo(volume, 0.1);
+ }
}
mute(muted: boolean): void {
- this.output.gain.rampTo(muted ? 0 : 0.5, 0.1);
+ this.isMuted = muted;
+ this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
dispose(): void {
diff --git a/lib/audio/songStructure.ts b/lib/audio/songStructure.ts
new file mode 100644
index 0000000..eac8c8a
--- /dev/null
+++ b/lib/audio/songStructure.ts
@@ -0,0 +1,197 @@
+import { Genre, SectionType } from '@/types/audio';
+
+export interface SongSection {
+ type: SectionType;
+ bars: number;
+ instruments: {
+ drums: { active: boolean; volume: number };
+ bass: { active: boolean; volume: number };
+ brass: { active: boolean; volume: number };
+ piano: { active: boolean; volume: number };
+ chords: { active: boolean; volume: number };
+ ambient: { active: boolean; volume: number };
+ };
+}
+
+export interface SongStructure {
+ sections: SongSection[];
+ totalBars: number;
+}
+
+// Genre-specific section configurations
+const sectionConfigs: Record = {
+ intro: {
+ instruments: {
+ drums: { active: false, volume: 0 },
+ bass: { active: false, volume: 0 },
+ brass: { active: false, volume: 0 },
+ piano: { active: true, volume: 0.3 },
+ chords: { active: true, volume: 0.4 },
+ ambient: { active: true, volume: 0.6 },
+ },
+ },
+ verse: {
+ instruments: {
+ drums: { active: true, volume: 0.6 },
+ bass: { active: true, volume: 0.5 },
+ brass: { active: false, volume: 0 },
+ piano: { active: true, volume: 0.4 },
+ chords: { active: true, volume: 0.5 },
+ ambient: { active: true, volume: 0.4 },
+ },
+ },
+ bridge: {
+ instruments: {
+ drums: { active: true, volume: 0.4 },
+ bass: { active: true, volume: 0.4 },
+ brass: { active: true, volume: 0.3 },
+ piano: { active: false, volume: 0 },
+ chords: { active: true, volume: 0.6 },
+ ambient: { active: true, volume: 0.5 },
+ },
+ },
+ chorus: {
+ instruments: {
+ drums: { active: true, volume: 0.8 },
+ bass: { active: true, volume: 0.7 },
+ brass: { active: true, volume: 0.5 },
+ piano: { active: true, volume: 0.5 },
+ chords: { active: true, volume: 0.7 },
+ ambient: { active: true, volume: 0.3 },
+ },
+ },
+ outro: {
+ instruments: {
+ drums: { active: true, volume: 0.3 },
+ bass: { active: true, volume: 0.3 },
+ brass: { active: false, volume: 0 },
+ piano: { active: true, volume: 0.4 },
+ chords: { active: true, volume: 0.5 },
+ ambient: { active: true, volume: 0.7 },
+ },
+ },
+};
+
+// Generate song structure based on duration (in minutes)
+export function generateSongStructure(durationMinutes: number, bpm: number, genre: Genre): SongStructure {
+ // Calculate total bars based on duration and BPM
+ // 4 beats per bar, so bars = (minutes * bpm) / 4
+ const totalBars = Math.floor((durationMinutes * bpm) / 4);
+
+ // Create a standard song structure that fits within the total bars
+ const sections: SongSection[] = [];
+
+ if (durationMinutes === 1) {
+ // Short format: Intro -> Verse -> Chorus -> Outro
+ const introBars = Math.floor(totalBars * 0.15);
+ const verseBars = Math.floor(totalBars * 0.35);
+ const chorusBars = Math.floor(totalBars * 0.35);
+ const outroBars = totalBars - introBars - verseBars - chorusBars;
+
+ sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
+ sections.push({ type: 'verse', bars: verseBars, instruments: { ...sectionConfigs.verse.instruments } });
+ sections.push({ type: 'chorus', bars: chorusBars, instruments: { ...sectionConfigs.chorus.instruments } });
+ sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
+ } else if (durationMinutes === 2) {
+ // Medium format: Intro -> Verse -> Bridge -> Chorus -> Outro
+ const introBars = Math.floor(totalBars * 0.1);
+ const verse1Bars = Math.floor(totalBars * 0.25);
+ const bridgeBars = Math.floor(totalBars * 0.15);
+ const chorusBars = Math.floor(totalBars * 0.35);
+ const outroBars = totalBars - introBars - verse1Bars - bridgeBars - chorusBars;
+
+ sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
+ sections.push({ type: 'verse', bars: verse1Bars, instruments: { ...sectionConfigs.verse.instruments } });
+ sections.push({ type: 'bridge', bars: bridgeBars, instruments: { ...sectionConfigs.bridge.instruments } });
+ sections.push({ type: 'chorus', bars: chorusBars, instruments: { ...sectionConfigs.chorus.instruments } });
+ sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
+ } else {
+ // Full format (3+ mins): Intro -> Verse -> Chorus -> Verse -> Bridge -> Chorus -> Outro
+ const introBars = Math.floor(totalBars * 0.08);
+ const verse1Bars = Math.floor(totalBars * 0.18);
+ const chorus1Bars = Math.floor(totalBars * 0.18);
+ const verse2Bars = Math.floor(totalBars * 0.15);
+ const bridgeBars = Math.floor(totalBars * 0.12);
+ const chorus2Bars = Math.floor(totalBars * 0.18);
+ const outroBars = totalBars - introBars - verse1Bars - chorus1Bars - verse2Bars - bridgeBars - chorus2Bars;
+
+ sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
+ sections.push({ type: 'verse', bars: verse1Bars, instruments: { ...sectionConfigs.verse.instruments } });
+ sections.push({ type: 'chorus', bars: chorus1Bars, instruments: { ...sectionConfigs.chorus.instruments } });
+ sections.push({ type: 'verse', bars: verse2Bars, instruments: { ...sectionConfigs.verse.instruments } });
+ sections.push({ type: 'bridge', bars: bridgeBars, instruments: { ...sectionConfigs.bridge.instruments } });
+ sections.push({ type: 'chorus', bars: chorus2Bars, instruments: { ...sectionConfigs.chorus.instruments } });
+ sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
+ }
+
+ // Apply genre-specific modifications
+ applyGenreModifications(sections, genre);
+
+ return { sections, totalBars };
+}
+
+function applyGenreModifications(sections: SongSection[], genre: Genre): void {
+ switch (genre) {
+ case 'trap':
+ // Trap: heavier bass, more brass in chorus
+ sections.forEach(section => {
+ if (section.instruments.bass.active) {
+ section.instruments.bass.volume = Math.min(1, section.instruments.bass.volume * 1.3);
+ }
+ if (section.type === 'chorus') {
+ section.instruments.brass.volume = Math.min(1, section.instruments.brass.volume * 1.2);
+ }
+ });
+ break;
+ case 'classical':
+ // Classical: more piano and chords, less drums
+ sections.forEach(section => {
+ section.instruments.piano.active = true;
+ section.instruments.piano.volume = Math.min(1, section.instruments.piano.volume * 1.3);
+ section.instruments.drums.volume *= 0.7;
+ section.instruments.brass.active = true;
+ section.instruments.brass.volume = Math.min(1, section.instruments.brass.volume + 0.2);
+ });
+ break;
+ case 'pop':
+ // Pop: balanced, catchy, more emphasis on chorus
+ sections.forEach(section => {
+ if (section.type === 'chorus') {
+ Object.keys(section.instruments).forEach(key => {
+ const inst = section.instruments[key as keyof typeof section.instruments];
+ inst.volume = Math.min(1, inst.volume * 1.1);
+ });
+ }
+ });
+ break;
+ case 'hiphop':
+ default:
+ // Hip hop: heavy drums and bass, soulful samples
+ sections.forEach(section => {
+ if (section.type === 'verse' || section.type === 'chorus') {
+ section.instruments.drums.volume = Math.min(1, section.instruments.drums.volume * 1.1);
+ section.instruments.bass.volume = Math.min(1, section.instruments.bass.volume * 1.1);
+ }
+ });
+ break;
+ }
+}
+
+// Calculate timing for each section in seconds
+export function calculateSectionTimings(structure: SongStructure, bpm: number): { startTime: number; endTime: number; section: SongSection }[] {
+ const secondsPerBar = (60 / bpm) * 4; // 4 beats per bar
+ let currentTime = 0;
+
+ return structure.sections.map(section => {
+ const startTime = currentTime;
+ const duration = section.bars * secondsPerBar;
+ currentTime += duration;
+ return {
+ startTime,
+ endTime: currentTime,
+ section,
+ };
+ });
+}
diff --git a/types/audio.ts b/types/audio.ts
index 5e1ebf9..3d50811 100644
--- a/types/audio.ts
+++ b/types/audio.ts
@@ -34,12 +34,15 @@ export interface InstrumentConfig {
ambient: AmbientType;
}
+export type SectionType = 'intro' | 'verse' | 'bridge' | 'chorus' | 'outro';
+
export interface EngineState {
isPlaying: boolean;
isInitialized: boolean;
bpm: number;
swing: number;
currentStep: number;
+ currentSection: SectionType;
genre: Genre;
duration: number; // in minutes
instruments: InstrumentConfig;
@@ -67,6 +70,7 @@ export type LayerName = 'drums' | 'bass' | 'brass' | 'piano' | 'chords' | 'ambie
export interface AudioEngineCallbacks {
onStepChange?: (step: number) => void;
onStateChange?: (state: EngineState) => void;
+ onSectionChange?: (section: SectionType) => void;
}
export const GENRE_CONFIG: Record = {