Files
bruitiste/src/domain/audio/effects/ReverbEffect.ts
2025-10-04 12:54:43 +02:00

322 lines
11 KiB
TypeScript

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<string, number | string>): 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()
}
}