- 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>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import * as Tone from 'tone';
|
|
import { AmbientType } from '@/types/audio';
|
|
|
|
export class AmbientLayer {
|
|
private noise1: Tone.Noise | null = null;
|
|
private noise2: Tone.Noise | null = null;
|
|
private filter1: Tone.Filter | null = null;
|
|
private filter2: Tone.Filter | null = null;
|
|
private gain1: Tone.Gain | null = null;
|
|
private gain2: Tone.Gain | null = null;
|
|
private output: Tone.Gain;
|
|
private lfo: Tone.LFO | null = null;
|
|
private currentType: AmbientType = 'rain';
|
|
private isPlaying = false;
|
|
private currentVolume: number = 0.4;
|
|
private isMuted: boolean = false;
|
|
|
|
constructor(destination: Tone.InputNode) {
|
|
this.output = new Tone.Gain(this.currentVolume);
|
|
this.output.connect(destination);
|
|
this.createAmbient('rain');
|
|
}
|
|
|
|
private createAmbient(type: AmbientType): void {
|
|
this.noise1?.dispose();
|
|
this.noise2?.dispose();
|
|
this.filter1?.dispose();
|
|
this.filter2?.dispose();
|
|
this.gain1?.dispose();
|
|
this.gain2?.dispose();
|
|
this.lfo?.dispose();
|
|
|
|
const configs: Record<AmbientType, {
|
|
noise1: { type: 'white' | 'pink' | 'brown'; filter: number; filterType: BiquadFilterType; gain: number };
|
|
noise2: { type: 'white' | 'pink' | 'brown'; filter: number; filterType: BiquadFilterType; gain: number };
|
|
lfoFreq: number;
|
|
}> = {
|
|
rain: {
|
|
noise1: { type: 'pink', filter: 3000, filterType: 'lowpass', gain: 0.15 },
|
|
noise2: { type: 'brown', filter: 1500, filterType: 'bandpass', gain: 0.1 },
|
|
lfoFreq: 0.1,
|
|
},
|
|
vinyl: {
|
|
noise1: { type: 'brown', filter: 2000, filterType: 'bandpass', gain: 0.12 },
|
|
noise2: { type: 'white', filter: 800, filterType: 'lowpass', gain: 0.05 },
|
|
lfoFreq: 0.05,
|
|
},
|
|
nature: {
|
|
noise1: { type: 'pink', filter: 4000, filterType: 'lowpass', gain: 0.1 },
|
|
noise2: { type: 'brown', filter: 500, filterType: 'lowpass', gain: 0.08 },
|
|
lfoFreq: 0.08,
|
|
},
|
|
space: {
|
|
noise1: { type: 'pink', filter: 1000, filterType: 'lowpass', gain: 0.12 },
|
|
noise2: { type: 'brown', filter: 300, filterType: 'lowpass', gain: 0.1 },
|
|
lfoFreq: 0.02,
|
|
},
|
|
};
|
|
|
|
const config = configs[type];
|
|
|
|
this.noise1 = new Tone.Noise(config.noise1.type);
|
|
this.filter1 = new Tone.Filter({
|
|
frequency: config.noise1.filter,
|
|
type: config.noise1.filterType,
|
|
});
|
|
this.gain1 = new Tone.Gain(config.noise1.gain);
|
|
this.noise1.connect(this.filter1);
|
|
this.filter1.connect(this.gain1);
|
|
this.gain1.connect(this.output);
|
|
|
|
this.noise2 = new Tone.Noise(config.noise2.type);
|
|
this.filter2 = new Tone.Filter({
|
|
frequency: config.noise2.filter,
|
|
type: config.noise2.filterType,
|
|
});
|
|
this.gain2 = new Tone.Gain(config.noise2.gain);
|
|
this.noise2.connect(this.filter2);
|
|
this.filter2.connect(this.gain2);
|
|
this.gain2.connect(this.output);
|
|
|
|
this.lfo = new Tone.LFO({
|
|
frequency: config.lfoFreq,
|
|
min: config.noise1.gain * 0.7,
|
|
max: config.noise1.gain * 1.2,
|
|
});
|
|
this.lfo.connect(this.gain1.gain);
|
|
|
|
this.currentType = type;
|
|
|
|
if (this.isPlaying) {
|
|
this.noise1.start();
|
|
this.noise2.start();
|
|
this.lfo.start();
|
|
}
|
|
}
|
|
|
|
setInstrument(type: AmbientType): void {
|
|
this.createAmbient(type);
|
|
}
|
|
|
|
start(): void {
|
|
this.noise1?.start();
|
|
this.noise2?.start();
|
|
this.lfo?.start();
|
|
this.isPlaying = true;
|
|
}
|
|
|
|
stop(): void {
|
|
this.noise1?.stop();
|
|
this.noise2?.stop();
|
|
this.lfo?.stop();
|
|
this.isPlaying = false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.stop();
|
|
this.noise1?.dispose();
|
|
this.noise2?.dispose();
|
|
this.filter1?.dispose();
|
|
this.filter2?.dispose();
|
|
this.gain1?.dispose();
|
|
this.gain2?.dispose();
|
|
this.lfo?.dispose();
|
|
this.output.dispose();
|
|
}
|
|
}
|