322 lines
11 KiB
TypeScript
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()
|
|
}
|
|
}
|