diff --git a/src/logic/AcidSynth.ts b/src/logic/AcidSynth.ts index 76dda452..7fc48aa4 100644 --- a/src/logic/AcidSynth.ts +++ b/src/logic/AcidSynth.ts @@ -1,6 +1,7 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' -export class AcidSynth { +export class AcidSynth extends BaseSynth { public synth: Tone.PolySynth | undefined public dist: Tone.Distortion | undefined public outputGain: Tone.Volume | undefined @@ -10,13 +11,12 @@ export class AcidSynth { private _envMod = 0.5 private _decay = 0.2 private _oscType: "sawtooth" | "square" | "sine" = "sawtooth" - private _volume = 0 private _slideTime = 0.1 private _distortionAmount = 0.4 - private initialized = false - - constructor() { } + constructor() { + super() + } public init() { if (this.initialized) return @@ -112,13 +112,6 @@ export class AcidSynth { } } - setVolume(db: number) { - this._volume = db - if (this.outputGain) { - this.outputGain.volume.value = db - } - } - setParams(cutoff: number, resonance: number, envMod: number = 0.5, decay: number = 0.2) { this._cutoff = cutoff this._resonance = resonance @@ -169,4 +162,10 @@ export class AcidSynth { console.error("AcidSynth: triggerNote error", e) } } + + public override dispose() { + this.synth?.dispose() + this.dist?.dispose() + super.dispose() + } } diff --git a/src/logic/BaseSynth.ts b/src/logic/BaseSynth.ts new file mode 100644 index 00000000..cc9e6c4d --- /dev/null +++ b/src/logic/BaseSynth.ts @@ -0,0 +1,25 @@ +import * as Tone from 'tone' + +export abstract class BaseSynth { + protected initialized = false + protected _volume = 0 + public abstract outputGain: Tone.Volume | Tone.Gain | undefined + + public abstract init(): void + + public setVolume(db: number) { + this._volume = db + if (this.outputGain) { + if (this.outputGain instanceof Tone.Volume) { + this.outputGain.volume.rampTo(db, 0.1) + } else if (this.outputGain instanceof Tone.Gain) { + this.outputGain.gain.rampTo(Tone.dbToGain(db), 0.1) + } + } + } + + public dispose() { + this.outputGain?.dispose() + this.initialized = false + } +} diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index 4c95bff1..24895a94 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -1,18 +1,25 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' -export class DrumMachine { - public kick: Tone.MembraneSynth - public snare: Tone.NoiseSynth - public hihat: Tone.NoiseSynth - public hihatOpen: Tone.NoiseSynth - public clap: Tone.NoiseSynth - public ride: Tone.MetalSynth - comp: Tone.Compressor - public output: Tone.Volume +export class DrumMachine extends BaseSynth { + public kick: Tone.MembraneSynth | undefined + public snare: Tone.NoiseSynth | undefined + public hihat: Tone.NoiseSynth | undefined + public hihatOpen: Tone.NoiseSynth | undefined + public clap: Tone.NoiseSynth | undefined + public ride: Tone.MetalSynth | undefined + comp: Tone.Compressor | undefined + public outputGain: Tone.Volume | undefined constructor() { - this.output = new Tone.Volume(0) - this.comp = new Tone.Compressor(-24, 4).connect(this.output) + super() + } + + public init() { + if (this.initialized) return + + this.outputGain = new Tone.Volume(this._volume) + this.comp = new Tone.Compressor(-24, 4).connect(this.outputGain) this.kick = new Tone.MembraneSynth({ pitchDecay: 0.05, @@ -51,72 +58,86 @@ export class DrumMachine { octaves: 1.5, volume: -12 }).connect(this.comp) - } - setVolume(db: number) { - this.output.volume.value = db + this.initialized = true } setKit(kit: '808' | '909') { + if (!this.initialized) this.init() if (kit === '808') { - this.kick.set({ + this.kick?.set({ pitchDecay: 0.05, octaves: 10, oscillator: { type: 'sine' }, envelope: { attack: 0.001, decay: 0.4, sustain: 0.01, release: 1.4 } }) - this.snare.set({ + this.snare?.set({ noise: { type: 'white' }, envelope: { attack: 0.005, decay: 0.2, sustain: 0.02 } }) - this.hihat.set({ noise: { type: 'white' }, envelope: { decay: 0.05 } }) - this.hihatOpen.set({ noise: { type: 'white' }, envelope: { decay: 0.3 } }) - this.ride.set({ envelope: { decay: 1.0 }, harmonicity: 5.1 }) + this.hihat?.set({ noise: { type: 'white' }, envelope: { decay: 0.05 } }) + this.hihatOpen?.set({ noise: { type: 'white' }, envelope: { decay: 0.3 } }) + this.ride?.set({ envelope: { decay: 1.0 }, harmonicity: 5.1 }) } else { // 909 Settings - this.kick.set({ + this.kick?.set({ pitchDecay: 0.02, octaves: 4, oscillator: { type: 'sine' }, envelope: { attack: 0.001, decay: 0.2, sustain: 0, release: 1 } }) - this.snare.set({ + this.snare?.set({ noise: { type: 'pink' }, // Pink noise for 909 snare body envelope: { attack: 0.001, decay: 0.15, sustain: 0 } }) // 909 Hats are metallic/cymbal-like, but we use NoiseSynth. Use Pink for darker/thicker or modify envelope - this.hihat.set({ noise: { type: 'pink' }, envelope: { decay: 0.03 } }) - this.hihatOpen.set({ noise: { type: 'pink' }, envelope: { decay: 0.2 } }) - this.ride.set({ envelope: { decay: 2.0 }, harmonicity: 5.1 }) + this.hihat?.set({ noise: { type: 'pink' }, envelope: { decay: 0.03 } }) + this.hihatOpen?.set({ noise: { type: 'pink' }, envelope: { decay: 0.2 } }) + this.ride?.set({ envelope: { decay: 2.0 }, harmonicity: 5.1 }) } } setDrumParams(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'ride', pitch: number, decay: number) { + if (!this.initialized) this.init() // Simplified mapping for synth params - if (drum === 'kick') { + if (drum === 'kick' && this.kick) { this.kick.envelope.decay = decay // pitch mapping if needed } - if (drum === 'hihat' || drum === 'hihatOpen') { - this[drum].envelope.decay = decay * 0.5 + if ((drum === 'hihat' || drum === 'hihatOpen') && this[drum]) { + this[drum]!.envelope.decay = decay * 0.5 } - if (drum === 'ride') { + if (drum === 'ride' && this.ride) { this.ride.envelope.decay = decay * 2 } } setDrumVolume(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'ride', volume: number) { - if (this[drum]) { - this[drum].volume.value = volume + if (!this.initialized) this.init() + const inst = (this as any)[drum] + if (inst) { + inst.volume.value = volume } } triggerDrum(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'ride', time: number, velocity: number = 0.8) { - if (drum === 'kick') this.kick.triggerAttackRelease('C1', '8n', time, velocity) - else if (drum === 'snare') this.snare.triggerAttackRelease('8n', time, velocity) - else if (drum === 'hihat') this.hihat.triggerAttackRelease('32n', time, velocity) - else if (drum === 'hihatOpen') this.hihatOpen.triggerAttackRelease('16n', time, velocity) - else if (drum === 'clap') this.clap.triggerAttackRelease('8n', time, velocity) - else if (drum === 'ride') this.ride.triggerAttackRelease('16n', time, velocity) + if (!this.initialized) this.init() + if (drum === 'kick') this.kick?.triggerAttackRelease('C1', '8n', time, velocity) + else if (drum === 'snare') this.snare?.triggerAttackRelease('8n', time, velocity) + else if (drum === 'hihat') this.hihat?.triggerAttackRelease('32n', time, velocity) + else if (drum === 'hihatOpen') this.hihatOpen?.triggerAttackRelease('16n', time, velocity) + else if (drum === 'clap') this.clap?.triggerAttackRelease('8n', time, velocity) + else if (drum === 'ride') this.ride?.triggerAttackRelease('16n', time, velocity) + } + + public override dispose() { + this.kick?.dispose() + this.snare?.dispose() + this.hihat?.dispose() + this.hihatOpen?.dispose() + this.clap?.dispose() + this.ride?.dispose() + this.comp?.dispose() + super.dispose() } } diff --git a/src/logic/FMBass.ts b/src/logic/FMBass.ts index 6640279a..cde8480c 100644 --- a/src/logic/FMBass.ts +++ b/src/logic/FMBass.ts @@ -1,19 +1,19 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' -export class FMBass { +export class FMBass extends BaseSynth { private synth: Tone.FMSynth | undefined private dist: Tone.Distortion | undefined - private outputGain: Tone.Volume | undefined + public outputGain: Tone.Volume | undefined private _harmonicity = 1.5 private _modulationIndex = 10 - private _volume = 0 private _attack = 0.01 private _decay = 0.2 - private initialized = false - - constructor() { } + constructor() { + super() + } public init() { if (this.initialized) return @@ -72,11 +72,6 @@ export class FMBass { } } - setVolume(db: number) { - this._volume = db - if (this.outputGain) this.outputGain.volume.value = db - } - triggerNote(note: string, duration: string, time: number, velocity: number = 0.8) { if (!this.initialized) this.init() if (!this.synth) return @@ -87,4 +82,10 @@ export class FMBass { console.error("FMBass: triggerNote error", e) } } + + public override dispose() { + this.synth?.dispose() + this.dist?.dispose() + super.dispose() + } } diff --git a/src/logic/GlobalSequencer.ts b/src/logic/GlobalSequencer.ts index d3d8d8c8..6737169a 100644 --- a/src/logic/GlobalSequencer.ts +++ b/src/logic/GlobalSequencer.ts @@ -93,9 +93,7 @@ export function startSequencerLoop() { if (drums.isPlaying && drumMachine && isTrackActive('drums')) { const patterns = drums.activePatterns - if (drumMachine.output) { - drumMachine.output.volume.value = Tone.gainToDb(autoVolDrums) - } + drumMachine.setVolume(Tone.gainToDb(autoVolDrums)) // Use standardized setVolume if (patterns.kick[step] && !drums.kick.muted) { drumMachine.triggerDrum('kick', time) @@ -127,9 +125,8 @@ export function startSequencerLoop() { if (bassStep && bassStep.active) { if (bass.activeInstrument === 'acid' && bassSynth) { - if (bassSynth.outputGain) { - bassSynth.outputGain.volume.value = Tone.gainToDb(autoVolBass) - } + bassSynth.setVolume(Tone.gainToDb(autoVolBass)) + const isContinuing = prevBassStep?.active && prevBassStep?.slide bassSynth.triggerNote( bassStep.note, @@ -153,9 +150,7 @@ export function startSequencerLoop() { if (leadSynth && isTrackActive('lead')) { // Apply volume - if (leadSynth.outputGain) { - leadSynth.outputGain.volume.value = Tone.gainToDb(autoVolLead) - } + leadSynth.setVolume(Tone.gainToDb(autoVolLead)) if (seq.isStagesPlaying) { try { @@ -194,9 +189,7 @@ export function startSequencerLoop() { if (pads.active && padSynth && totalStep % 32 === 0 && isTrackActive('pads')) { try { - if (padSynth.synth) { - padSynth.synth.volume.value = Tone.gainToDb(autoVolPads) - } + padSynth.setVolume(Tone.gainToDb(autoVolPads)) const progression = generatePadProgression( harmony.root, harmony.scale, @@ -219,9 +212,7 @@ export function startSequencerLoop() { if (harm.isPlaying && harmSynth && totalStep % 32 === 0 && isTrackActive('harm')) { try { - if (harmSynth.outputGain) { - harmSynth.outputGain.volume.value = Tone.gainToDb(autoVolHarm) - } + harmSynth.setVolume(Tone.gainToDb(autoVolHarm)) const rootNote = harmony.root + '2' const rootMidi = Tone.Frequency(rootNote).toMidi() harmSynth.triggerNote(rootNote, '2n', time, 0.7) diff --git a/src/logic/HarmSynth.ts b/src/logic/HarmSynth.ts index 9b691cd3..a2c55ede 100644 --- a/src/logic/HarmSynth.ts +++ b/src/logic/HarmSynth.ts @@ -1,4 +1,5 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' export type HarmOscType = 'sawtooth' | 'square' | 'triangle' | 'sine' @@ -207,7 +208,7 @@ class HarmVoice { } } -export class HarmSynth { +export class HarmSynth extends BaseSynth { private voicePool: HarmVoice[] = [] private activeVoices: Set = new Set() private maxVoices = 16 @@ -243,8 +244,9 @@ export class HarmSynth { private sharedCurve = new Float32Array(4096) private lastCurveParams = { timbre: -1, order: -1, harmonics: -1 } - private initialized = false - constructor() { } + constructor() { + super() + } public init() { if (this.initialized) return @@ -257,7 +259,7 @@ export class HarmSynth { this.chorus = new Tone.Chorus(4, 2.5, 0.5).start() this.delay = new Tone.FeedbackDelay('8n', 0.5) this.reverb = new Tone.Reverb(2) - this.outputGain = new Tone.Volume(0) + this.outputGain = new Tone.Volume(this._volume) this.directBus.chain(this.filter1, this.filter2, this.outputGain) this.fxBus.chain(this.distortion, this.phaser, this.chorus, this.delay, this.reverb, this.outputGain) @@ -339,7 +341,6 @@ export class HarmSynth { setDelay(time: string, feedback: number, wet: number) { if (this.delay) { this.delay.delayTime.value = time; this.delay.feedback.value = feedback; this.delay.wet.value = wet } } setReverb(decay: number, wet: number) { if (this.reverb) { this.reverb.decay = decay; this.reverb.wet.value = wet } } setFilter(idx: 1 | 2, freq: number, q: number, type: BiquadFilterType) { const f = idx === 1 ? this.filter1 : this.filter2; if (f) { f.frequency.value = freq; f.Q.value = q; f.type = type } } - setVolume(db: number) { this.outputGain?.volume.rampTo(db, 0.1) } setOscType(idx: 1 | 2 | 3, type: HarmOscType) { (this.settings as any)[`osc${idx}`].type = type } setOscDetune(idx: 1 | 2 | 3, detune: number) { (this.settings as any)[`osc${idx}`].detune = detune } setEnv(target: 'osc1' | 'osc2' | 'osc3' | 'noise', params: ADSRParams) { (this.settings as any)[target].env = params } @@ -365,4 +366,18 @@ export class HarmSynth { this.settings.complex = { ...this.settings.complex, ...params } this.activeVoices.forEach(v => this.updateWavefolder(v, this.settings.complex.timbre, this.settings.complex.order, this.settings.complex.harmonics)) } + + public override dispose() { + this.voicePool.forEach(v => v.dispose()) + this.fxBus?.dispose() + this.directBus?.dispose() + this.filter1?.dispose() + this.filter2?.dispose() + this.distortion?.dispose() + this.phaser?.dispose() + this.chorus?.dispose() + this.delay?.dispose() + this.reverb?.dispose() + super.dispose() + } } diff --git a/src/logic/PadSynth.ts b/src/logic/PadSynth.ts index 8e8153fd..776dfa79 100644 --- a/src/logic/PadSynth.ts +++ b/src/logic/PadSynth.ts @@ -1,11 +1,19 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' -export class PadSynth { - synth: Tone.PolySynth - filter: Tone.Filter - reverb: Tone.Reverb +export class PadSynth extends BaseSynth { + synth: Tone.PolySynth | undefined + filter: Tone.Filter | undefined + reverb: Tone.Reverb | undefined + public outputGain: Tone.Volume | undefined constructor() { + super() + } + + public init() { + if (this.initialized) return + this.filter = new Tone.Filter({ type: 'lowpass', frequency: 1000, @@ -32,14 +40,24 @@ export class PadSynth { }) this.synth.maxPolyphony = 6 - this.synth.chain(this.filter, this.reverb) + this.outputGain = new Tone.Volume(this._volume) + this.synth.chain(this.filter, this.reverb, this.outputGain) + this.initialized = true } triggerChord(notes: string[], duration: string, time: number, velocity: number = 0.4) { - this.synth.triggerAttackRelease(notes, duration, time, velocity) + if (!this.initialized) this.init() + this.synth?.triggerAttackRelease(notes, duration, time, velocity) } setParams(brightness: number) { - this.filter.frequency.exponentialRampTo(brightness * 4000 + 200, 0.1) + this.filter?.frequency.exponentialRampTo(brightness * 4000 + 200, 0.1) + } + + public override dispose() { + this.synth?.dispose() + this.filter?.dispose() + this.reverb?.dispose() + super.dispose() } } diff --git a/src/logic/SamplerInstrument.ts b/src/logic/SamplerInstrument.ts index f652033c..3474db70 100644 --- a/src/logic/SamplerInstrument.ts +++ b/src/logic/SamplerInstrument.ts @@ -1,27 +1,35 @@ import * as Tone from 'tone' +import { BaseSynth } from './BaseSynth' -export class SamplerInstrument { - player: Tone.GrainPlayer - volume: Tone.Volume +export class SamplerInstrument extends BaseSynth { + player: Tone.GrainPlayer | undefined + public outputGain: Tone.Volume | undefined loaded: boolean = false bufferDuration: number = 0 url: string = '' constructor() { + super() + } + + public init() { + if (this.initialized) return this.player = new Tone.GrainPlayer() - this.volume = new Tone.Volume(0) - this.player.connect(this.volume) + this.outputGain = new Tone.Volume(this._volume) + this.player.connect(this.outputGain) // Default granular settings this.player.grainSize = 0.1 this.player.overlap = 0.1 + this.initialized = true } async load(url: string) { + if (!this.initialized) this.init() if (this.url === url) return this.loaded = false const buffer = await new Tone.ToneAudioBuffer().load(url) - this.player.buffer = buffer + if (this.player) this.player.buffer = buffer this.url = url this.loaded = true this.bufferDuration = buffer.duration @@ -29,7 +37,7 @@ export class SamplerInstrument { } triggerSlice(sliceIndex: number, totalSlices: number, time: number = Tone.now()) { - if (!this.loaded) return + if (!this.loaded || !this.player) return const sliceDuration = this.bufferDuration / totalSlices const offset = sliceIndex * sliceDuration @@ -38,22 +46,19 @@ export class SamplerInstrument { this.player.start(time, offset, sliceDuration) } - setVolume(db: number) { - this.volume.volume.rampTo(db, 0.1) - } - setPlaybackRate(rate: number) { - this.player.playbackRate = rate + if (this.player) this.player.playbackRate = rate } setGranularParams(params: { grainSize?: number, overlap?: number, detune?: number }) { + if (!this.player) return if (params.grainSize !== undefined) this.player.grainSize = params.grainSize if (params.overlap !== undefined) this.player.overlap = params.overlap if (params.detune !== undefined) this.player.detune = params.detune } - dispose() { - this.player.dispose() - this.volume.dispose() + public override dispose() { + this.player?.dispose() + super.dispose() } } diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index 4f9d07a5..5efb8106 100644 --- a/src/store/audioStore.ts +++ b/src/store/audioStore.ts @@ -306,10 +306,12 @@ export const useAudioStore = create((set, get) => ({ set({ loadingStep: 'Constructing Synths (Drums)...' }) await new Promise(r => setTimeout(r, 10)) const drums = new DrumMachine() + drums.init() set({ loadingStep: 'Constructing Synths (Pads)...' }) await new Promise(r => setTimeout(r, 10)) const pads = new PadSynth() + pads.init() set({ loadingStep: 'Constructing Synths (Harm)...' }) await new Promise(r => setTimeout(r, 10)) @@ -319,6 +321,7 @@ export const useAudioStore = create((set, get) => ({ set({ loadingStep: 'Constructing Synths (Sampler)...' }) await new Promise(r => setTimeout(r, 10)) const sampler = new SamplerInstrument() + sampler.init() await sampler.load(useSamplerStore.getState().url) set({ loadingStep: 'Connecting Modules...' }) @@ -354,21 +357,20 @@ export const useAudioStore = create((set, get) => ({ } // Pads - if (pads?.synth) pads.synth.connect(channels['pads']) + if (pads?.outputGain) pads.outputGain.connect(channels['pads']) // Harm - if ((harm as any).outputGain) (harm as any).outputGain.connect(channels['harm']) - else if ((harm as any).output) (harm as any).output.connect(channels['harm']) + if (harm?.outputGain) harm.outputGain.connect(channels['harm']) // Sampler - sampler.volume.connect(channels['sampler']) + if (sampler.outputGain) sampler.outputGain.connect(channels['sampler']) // Apply initial volumes (already set during channel creation, but let's be sure for instrument internal levels) bass?.setVolume(0) // Let channels handle the actual mix lead?.setVolume(0) drums?.setVolume(0) - if (pads?.synth?.volume) pads.synth.volume.value = 0 + pads?.setVolume(0) harm?.setVolume(0) sampler.setVolume(0) @@ -870,10 +872,9 @@ export const useAudioStore = create((set, get) => ({ else if (trackId === 'harm') synth = new HarmSynth() else if (trackId === 'drums') synth = new DrumMachine() - if (synth && 'init' in synth) synth.init() if (synth) { - const out = synth.outputGain || synth.output || synth.synth || synth.volume - if (out) out.toDestination() + synth.init() + if (synth.outputGain) synth.outputGain.toDestination() } trackClips.forEach(clip => {