import type { Effect } from './Effect.interface' export class ReverbEffect implements Effect { readonly id = 'reverb' private audioContext: AudioContext private inputNode: GainNode private outputNode: GainNode private wetNode: GainNode private dryNode: GainNode private mixNode: GainNode private pannerNode: StereoPannerNode private panLfoNode: OscillatorNode private panLfoGainNode: GainNode private bypassed: boolean = false private currentWetValue: number = 0 private currentDryValue: number = 1 private currentDecay: number = 0.5 private currentDamping: number = 0.5 private preDelay: DelayNode private inputDiffusion1: DelayNode private inputDiffusion2: DelayNode private inputDiffusion3: DelayNode private inputDiffusion4: DelayNode private inputDiffusionGain1: GainNode private inputDiffusionGain2: GainNode private inputDiffusionGain3: GainNode private inputDiffusionGain4: GainNode private leftDelay1: DelayNode private leftDelay2: DelayNode private leftFilter: BiquadFilterNode private leftGain: GainNode private leftAp1: DelayNode private leftAp2: DelayNode private leftApGain1: GainNode private leftApGain2: GainNode private rightDelay1: DelayNode private rightDelay2: DelayNode private rightFilter: BiquadFilterNode private rightGain: GainNode private rightAp1: DelayNode private rightAp2: DelayNode private rightApGain1: GainNode private rightApGain2: GainNode private leftOut: GainNode private rightOut: GainNode private merger: ChannelMergerNode constructor(audioContext: AudioContext) { this.audioContext = audioContext const sr = audioContext.sampleRate const scale = sr / 29761 this.inputNode = audioContext.createGain() this.outputNode = audioContext.createGain() this.mixNode = audioContext.createGain() this.wetNode = audioContext.createGain() this.dryNode = audioContext.createGain() this.pannerNode = audioContext.createStereoPanner() this.panLfoNode = audioContext.createOscillator() this.panLfoGainNode = audioContext.createGain() this.wetNode.gain.value = 0 this.dryNode.gain.value = 1 this.panLfoNode.frequency.value = 0 this.panLfoGainNode.gain.value = 0 this.panLfoNode.connect(this.panLfoGainNode) this.panLfoGainNode.connect(this.pannerNode.pan) this.panLfoNode.start() this.preDelay = audioContext.createDelay(0.1) this.preDelay.delayTime.value = 0.0 const diffusionCoef = 0.75 this.inputDiffusion1 = audioContext.createDelay(0.1) this.inputDiffusion2 = audioContext.createDelay(0.1) this.inputDiffusion3 = audioContext.createDelay(0.1) this.inputDiffusion4 = audioContext.createDelay(0.1) this.inputDiffusion1.delayTime.value = (142 * scale) / sr this.inputDiffusion2.delayTime.value = (107 * scale) / sr this.inputDiffusion3.delayTime.value = (379 * scale) / sr this.inputDiffusion4.delayTime.value = (277 * scale) / sr this.inputDiffusionGain1 = audioContext.createGain() this.inputDiffusionGain2 = audioContext.createGain() this.inputDiffusionGain3 = audioContext.createGain() this.inputDiffusionGain4 = audioContext.createGain() this.inputDiffusionGain1.gain.value = diffusionCoef this.inputDiffusionGain2.gain.value = diffusionCoef this.inputDiffusionGain3.gain.value = diffusionCoef this.inputDiffusionGain4.gain.value = diffusionCoef this.leftDelay1 = audioContext.createDelay(0.5) this.leftDelay2 = audioContext.createDelay(0.5) this.leftDelay1.delayTime.value = (672 * scale) / sr this.leftDelay2.delayTime.value = (4453 * scale) / sr this.leftFilter = audioContext.createBiquadFilter() this.leftFilter.type = 'lowpass' this.leftFilter.frequency.value = 5000 this.leftGain = audioContext.createGain() this.leftGain.gain.value = 0.5 this.leftAp1 = audioContext.createDelay(0.1) this.leftAp2 = audioContext.createDelay(0.2) this.leftAp1.delayTime.value = (1800 * scale) / sr this.leftAp2.delayTime.value = (3720 * scale) / sr this.leftApGain1 = audioContext.createGain() this.leftApGain2 = audioContext.createGain() this.leftApGain1.gain.value = 0.7 this.leftApGain2.gain.value = 0.7 this.rightDelay1 = audioContext.createDelay(0.5) this.rightDelay2 = audioContext.createDelay(0.5) this.rightDelay1.delayTime.value = (908 * scale) / sr this.rightDelay2.delayTime.value = (4217 * scale) / sr this.rightFilter = audioContext.createBiquadFilter() this.rightFilter.type = 'lowpass' this.rightFilter.frequency.value = 5000 this.rightGain = audioContext.createGain() this.rightGain.gain.value = 0.5 this.rightAp1 = audioContext.createDelay(0.1) this.rightAp2 = audioContext.createDelay(0.2) this.rightAp1.delayTime.value = (2656 * scale) / sr this.rightAp2.delayTime.value = (3163 * scale) / sr this.rightApGain1 = audioContext.createGain() this.rightApGain2 = audioContext.createGain() this.rightApGain1.gain.value = 0.7 this.rightApGain2.gain.value = 0.7 this.leftOut = audioContext.createGain() this.rightOut = audioContext.createGain() this.merger = audioContext.createChannelMerger(2) this.buildGraph() this.updateDecayAndDamping() this.inputNode.connect(this.dryNode) this.dryNode.connect(this.mixNode) this.wetNode.connect(this.mixNode) this.mixNode.connect(this.pannerNode) this.pannerNode.connect(this.outputNode) } private buildGraph(): void { const input = this.inputNode const wet = this.wetNode input.connect(this.preDelay) const id1In = this.createAllPass(this.preDelay, this.inputDiffusion1, this.inputDiffusionGain1) const id2In = this.createAllPass(id1In, this.inputDiffusion2, this.inputDiffusionGain2) const id3In = this.createAllPass(id2In, this.inputDiffusion3, this.inputDiffusionGain3) const id4Out = this.createAllPass(id3In, this.inputDiffusion4, this.inputDiffusionGain4) const splitter = this.audioContext.createGain() id4Out.connect(splitter) const leftIn = this.audioContext.createGain() const rightIn = this.audioContext.createGain() splitter.connect(leftIn) splitter.connect(rightIn) leftIn.connect(this.leftDelay1) this.leftDelay1.connect(this.leftFilter) this.leftFilter.connect(this.leftDelay2) const leftAp1Out = this.createAllPass(this.leftDelay2, this.leftAp1, this.leftApGain1) const leftAp2Out = this.createAllPass(leftAp1Out, this.leftAp2, this.leftApGain2) leftAp2Out.connect(this.leftGain) this.leftGain.connect(rightIn) leftAp2Out.connect(this.leftOut) this.leftOut.connect(this.merger, 0, 0) rightIn.connect(this.rightDelay1) this.rightDelay1.connect(this.rightFilter) this.rightFilter.connect(this.rightDelay2) const rightAp1Out = this.createAllPass(this.rightDelay2, this.rightAp1, this.rightApGain1) const rightAp2Out = this.createAllPass(rightAp1Out, this.rightAp2, this.rightApGain2) rightAp2Out.connect(this.rightGain) this.rightGain.connect(leftIn) rightAp2Out.connect(this.rightOut) this.rightOut.connect(this.merger, 0, 1) this.merger.connect(wet) } private createAllPass(input: AudioNode, delay: DelayNode, gain: GainNode): AudioNode { const output = this.audioContext.createGain() const feedbackGain = this.audioContext.createGain() feedbackGain.gain.value = -1 input.connect(delay) input.connect(feedbackGain) feedbackGain.connect(output) delay.connect(gain) gain.connect(output) gain.connect(input) return output } private updateDecayAndDamping(): void { const decay = this.currentDecay const damping = this.currentDamping this.leftGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01) this.rightGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01) const cutoff = 1000 + damping * 14000 this.leftFilter.frequency.setTargetAtTime(cutoff, this.audioContext.currentTime, 0.01) this.rightFilter.frequency.setTargetAtTime(cutoff, this.audioContext.currentTime, 0.01) } getInputNode(): AudioNode { return this.inputNode } getOutputNode(): AudioNode { return this.outputNode } setBypass(bypass: boolean): void { this.bypassed = bypass if (bypass) { this.wetNode.gain.value = 0 this.dryNode.gain.value = 1 } else { this.wetNode.gain.value = this.currentWetValue this.dryNode.gain.value = this.currentDryValue } } updateParams(values: Record): void { let needsUpdate = false if (values.reverbDecay !== undefined && typeof values.reverbDecay === 'number') { this.currentDecay = values.reverbDecay / 100 needsUpdate = true } if (values.reverbDamping !== undefined && typeof values.reverbDamping === 'number') { this.currentDamping = values.reverbDamping / 100 needsUpdate = true } if (values.reverbWetDry !== undefined && typeof values.reverbWetDry === 'number') { const wet = values.reverbWetDry / 100 this.currentWetValue = wet this.currentDryValue = 1 - wet if (!this.bypassed) { this.wetNode.gain.value = this.currentWetValue this.dryNode.gain.value = this.currentDryValue } } if (values.reverbPanRate !== undefined && typeof values.reverbPanRate === 'number') { const rate = values.reverbPanRate this.panLfoNode.frequency.setTargetAtTime( rate, this.audioContext.currentTime, 0.01 ) } if (values.reverbPanWidth !== undefined && typeof values.reverbPanWidth === 'number') { const width = values.reverbPanWidth / 100 this.panLfoGainNode.gain.setTargetAtTime( width, this.audioContext.currentTime, 0.01 ) } if (needsUpdate) { this.updateDecayAndDamping() } } dispose(): void { this.panLfoNode.stop() this.panLfoNode.disconnect() this.panLfoGainNode.disconnect() this.inputNode.disconnect() this.outputNode.disconnect() this.mixNode.disconnect() this.wetNode.disconnect() this.dryNode.disconnect() this.pannerNode.disconnect() this.preDelay.disconnect() this.inputDiffusion1.disconnect() this.inputDiffusion2.disconnect() this.inputDiffusion3.disconnect() this.inputDiffusion4.disconnect() this.leftDelay1.disconnect() this.leftDelay2.disconnect() this.leftFilter.disconnect() this.leftGain.disconnect() this.leftAp1.disconnect() this.leftAp2.disconnect() this.rightDelay1.disconnect() this.rightDelay2.disconnect() this.rightFilter.disconnect() this.rightGain.disconnect() this.rightAp1.disconnect() this.rightAp2.disconnect() this.merger.disconnect() } }