- 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>
198 lines
7.9 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|