forked from averyfelts/Lofi_Generator
- Set up Next.js project with shadcn/ui and Tailwind CSS - Created audio engine with MembraneSynth drums, FMSynth chords, and ambient noise layers - Implemented 16-step drum sequencer with boom bap patterns - Added jazz chord progressions (ii-V-I, minor key, neo soul) - Built React hook for audio state management - Created UI components: transport controls, volume sliders, layer mixer, beat visualizer - Applied lofi-themed dark color scheme with oklch colors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
140 lines
3.2 KiB
TypeScript
140 lines
3.2 KiB
TypeScript
import * as Tone from 'tone';
|
|
import { ChordProgression } from '@/types/audio';
|
|
import { getRandomProgression } from './patterns';
|
|
|
|
export class ChordEngine {
|
|
private synth: Tone.PolySynth;
|
|
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;
|
|
|
|
constructor(destination: Tone.InputNode) {
|
|
this.output = new Tone.Gain(0.6);
|
|
|
|
// Warm lofi filter
|
|
this.filter = new Tone.Filter({
|
|
frequency: 2000,
|
|
type: 'lowpass',
|
|
rolloff: -24,
|
|
});
|
|
|
|
// Dreamy reverb
|
|
this.reverb = new Tone.Reverb({
|
|
decay: 3,
|
|
wet: 0.4,
|
|
});
|
|
|
|
// Subtle chorus for width
|
|
this.chorus = new Tone.Chorus({
|
|
frequency: 0.5,
|
|
delayTime: 3.5,
|
|
depth: 0.5,
|
|
wet: 0.3,
|
|
}).start();
|
|
|
|
// FM Synth for warm, evolving pad sound
|
|
this.synth = new Tone.PolySynth(Tone.FMSynth, {
|
|
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,
|
|
},
|
|
});
|
|
|
|
// Lower the overall synth volume to prevent clipping
|
|
this.synth.volume.value = -12;
|
|
|
|
// Chain: synth -> filter -> chorus -> reverb -> output -> destination
|
|
this.synth.connect(this.filter);
|
|
this.filter.connect(this.chorus);
|
|
this.chorus.connect(this.reverb);
|
|
this.reverb.connect(this.output);
|
|
this.output.connect(destination);
|
|
|
|
// Initialize with a random progression
|
|
this.progression = getRandomProgression();
|
|
}
|
|
|
|
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];
|
|
|
|
// Release previous notes and play new chord
|
|
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;
|
|
// Recreate sequence with new progression
|
|
if (this.sequence) {
|
|
this.createSequence();
|
|
}
|
|
}
|
|
|
|
randomize(): ChordProgression {
|
|
this.progression = getRandomProgression();
|
|
if (this.sequence) {
|
|
this.createSequence();
|
|
}
|
|
return this.progression;
|
|
}
|
|
|
|
setVolume(volume: number): void {
|
|
this.output.gain.rampTo(volume, 0.1);
|
|
}
|
|
|
|
mute(muted: boolean): void {
|
|
this.output.gain.rampTo(muted ? 0 : 0.6, 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();
|
|
}
|
|
}
|