Lofi_Generator/lib/audio/chordEngine.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

176 lines
4.5 KiB
TypeScript

import * as Tone from 'tone';
import { ChordProgression, ChordType, Genre } from '@/types/audio';
import { getRandomProgression } from './patterns';
export class ChordEngine {
private synth: Tone.PolySynth | null = null;
private sequence: Tone.Sequence | null = null;
private progression: ChordProgression;
private output: Tone.Gain;
private filter: Tone.Filter;
private reverb: Tone.Reverb;
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(this.currentVolume);
this.filter = new Tone.Filter({
frequency: 2000,
type: 'lowpass',
rolloff: -24,
});
this.reverb = new Tone.Reverb({
decay: 3,
wet: 0.4,
});
this.chorus = new Tone.Chorus({
frequency: 0.5,
delayTime: 3.5,
depth: 0.5,
wet: 0.3,
}).start();
this.filter.connect(this.chorus);
this.chorus.connect(this.reverb);
this.reverb.connect(this.output);
this.output.connect(destination);
this.progression = getRandomProgression(this.genre);
this.createSynth('pad');
}
private createSynth(type: ChordType): void {
if (this.synth) {
this.synth.dispose();
}
const configs: Record<ChordType, { synth: typeof Tone.Synth | typeof Tone.FMSynth; options: object }> = {
pad: {
synth: Tone.FMSynth,
options: {
harmonicity: 2,
modulationIndex: 1.5,
oscillator: { type: 'sine' },
envelope: { attack: 0.3, decay: 0.3, sustain: 0.8, release: 1.5 },
modulation: { type: 'sine' },
modulationEnvelope: { attack: 0.5, decay: 0.2, sustain: 0.5, release: 0.5 },
},
},
strings: {
synth: Tone.Synth,
options: {
oscillator: { type: 'sawtooth' },
envelope: { attack: 0.4, decay: 0.3, sustain: 0.9, release: 2 },
},
},
organ: {
synth: Tone.Synth,
options: {
oscillator: { type: 'sine' },
envelope: { attack: 0.01, decay: 0.1, sustain: 0.9, release: 0.3 },
},
},
synth: {
synth: Tone.Synth,
options: {
oscillator: { type: 'fatsawtooth', spread: 30, count: 3 },
envelope: { attack: 0.1, decay: 0.2, sustain: 0.6, release: 0.8 },
},
},
};
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.volume.value = -12;
this.synth.connect(this.filter);
this.currentType = type;
}
setInstrument(type: ChordType): void {
this.createSynth(type);
}
setGenre(genre: Genre): void {
this.genre = genre;
this.progression = getRandomProgression(genre);
if (this.sequence) {
this.createSequence();
}
}
createSequence(): void {
if (this.sequence) {
this.sequence.dispose();
}
const steps = Array.from({ length: this.progression.chords.length }, (_, i) => i);
this.sequence = new Tone.Sequence(
(time, step) => {
const chord = this.progression.chords[step];
const duration = this.progression.durations[step];
if (this.synth) {
this.synth.releaseAll(time);
this.synth.triggerAttackRelease(chord, duration, time, 0.5);
}
},
steps,
'2n'
);
this.sequence.start(0);
}
setProgression(progression: ChordProgression): void {
this.progression = progression;
if (this.sequence) {
this.createSequence();
}
}
randomize(): ChordProgression {
this.progression = getRandomProgression(this.genre);
if (this.sequence) {
this.createSequence();
}
return this.progression;
}
setVolume(volume: number): void {
this.currentVolume = volume;
if (!this.isMuted) {
this.output.gain.rampTo(volume, 0.1);
}
}
mute(muted: boolean): void {
this.isMuted = muted;
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
}
setFilterFrequency(freq: number): void {
this.filter.frequency.rampTo(freq, 0.1);
}
getProgression(): ChordProgression {
return this.progression;
}
dispose(): void {
this.sequence?.dispose();
this.synth?.dispose();
this.filter.dispose();
this.reverb.dispose();
this.chorus.dispose();
this.output.dispose();
}
}