Lofi_Generator/lib/audio/songStructure.ts
Avery Felts 26d66f329c Add song structure generation and fix volume controls
- Fix volume sliders by tracking currentVolume separately from mute state
- Add song structure system with intro, verse, bridge, chorus, outro sections
- Implement automatic section transitions during playback based on duration
- Add 30-second loading bar with progress animation during beat generation
- Update instrument box colors (grey drums, red bass, green piano, baby blue brass)
- Add custom trumpet SVG icon for brass section
- Set duration slider max to 3 minutes
- Fix TypeScript type errors in audio engine classes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:30:33 -07:00

198 lines
7.9 KiB
TypeScript

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<SectionType, {
instruments: SongSection['instruments'];
}> = {
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,
};
});
}