Better reverberation

This commit is contained in:
2025-10-04 12:54:43 +02:00
parent 012c3534be
commit c6cc1a47c0

View File

@ -6,7 +6,6 @@ export class ReverbEffect implements Effect {
private audioContext: AudioContext
private inputNode: GainNode
private outputNode: GainNode
private convolverNode: ConvolverNode
private wetNode: GainNode
private dryNode: GainNode
private mixNode: GainNode
@ -16,15 +15,49 @@ export class ReverbEffect implements Effect {
private bypassed: boolean = false
private currentWetValue: number = 0
private currentDryValue: number = 1
private currentDecay: number = 2
private currentDamping: number = 50
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.convolverNode = audioContext.createConvolver()
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain()
this.pannerNode = audioContext.createStereoPanner()
@ -40,98 +73,154 @@ export class ReverbEffect implements Effect {
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.inputNode.connect(this.convolverNode)
this.convolverNode.connect(this.wetNode)
this.dryNode.connect(this.mixNode)
this.wetNode.connect(this.mixNode)
this.mixNode.connect(this.pannerNode)
this.pannerNode.connect(this.outputNode)
this.convolverNode.buffer = this.createDummyBuffer()
this.generateReverb(this.currentDecay, this.currentDamping)
}
private createDummyBuffer(): AudioBuffer {
const buffer = this.audioContext.createBuffer(2, this.audioContext.sampleRate * 0.1, this.audioContext.sampleRate)
for (let i = 0; i < 2; i++) {
const data = buffer.getChannelData(i)
for (let j = 0; j < data.length; j++) {
data[j] = (Math.random() * 2 - 1) * Math.exp(-j / (this.audioContext.sampleRate * 0.05))
}
}
return buffer
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 generateReverb(decayTime: number, damping: number): void {
const sampleRate = this.audioContext.sampleRate
const numChannels = 2
const totalTime = decayTime * 1.5
const decaySampleFrames = Math.round(decayTime * sampleRate)
const numSampleFrames = Math.round(totalTime * sampleRate)
const fadeInTime = 0.05
const fadeInSampleFrames = Math.round(fadeInTime * sampleRate)
const decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames)
private createAllPass(input: AudioNode, delay: DelayNode, gain: GainNode): AudioNode {
const output = this.audioContext.createGain()
const feedbackGain = this.audioContext.createGain()
feedbackGain.gain.value = -1
const reverbIR = this.audioContext.createBuffer(numChannels, numSampleFrames, sampleRate)
input.connect(delay)
input.connect(feedbackGain)
feedbackGain.connect(output)
for (let i = 0; i < numChannels; i++) {
const chan = reverbIR.getChannelData(i)
for (let j = 0; j < numSampleFrames; j++) {
chan[j] = (Math.random() * 2 - 1) * Math.pow(decayBase, j)
}
for (let j = 0; j < fadeInSampleFrames; j++) {
chan[j] *= j / fadeInSampleFrames
}
delay.connect(gain)
gain.connect(output)
gain.connect(input)
return output
}
const lpFreqStart = 10000
const lpFreqEnd = 200 + (damping / 100) * 7800
private updateDecayAndDamping(): void {
const decay = this.currentDecay
const damping = this.currentDamping
this.applyGradualLowpass(reverbIR, lpFreqStart, lpFreqEnd, decayTime, (buffer) => {
this.convolverNode.buffer = buffer
})
}
this.leftGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01)
this.rightGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01)
private applyGradualLowpass(
input: AudioBuffer,
lpFreqStart: number,
lpFreqEnd: number,
lpFreqEndAt: number,
callback: (buffer: AudioBuffer) => void
): void {
if (lpFreqStart === 0) {
callback(input)
return
}
const context = new OfflineAudioContext(
input.numberOfChannels,
input.length,
input.sampleRate
)
const player = context.createBufferSource()
player.buffer = input
const filter = context.createBiquadFilter()
lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2)
lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2)
filter.type = 'lowpass'
filter.Q.value = 0.0001
filter.frequency.setValueAtTime(lpFreqStart, 0)
filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt)
player.connect(filter)
filter.connect(context.destination)
player.start()
context.oncomplete = (event) => {
callback(event.renderedBuffer)
}
context.startRendering()
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 {
@ -154,16 +243,16 @@ export class ReverbEffect implements Effect {
}
updateParams(values: Record<string, number | string>): void {
let needsRegeneration = false
let needsUpdate = false
if (values.reverbDecay !== undefined && typeof values.reverbDecay === 'number' && values.reverbDecay !== this.currentDecay) {
this.currentDecay = values.reverbDecay
needsRegeneration = true
if (values.reverbDecay !== undefined && typeof values.reverbDecay === 'number') {
this.currentDecay = values.reverbDecay / 100
needsUpdate = true
}
if (values.reverbDamping !== undefined && typeof values.reverbDamping === 'number' && values.reverbDamping !== this.currentDamping) {
this.currentDamping = values.reverbDamping
needsRegeneration = true
if (values.reverbDamping !== undefined && typeof values.reverbDamping === 'number') {
this.currentDamping = values.reverbDamping / 100
needsUpdate = true
}
if (values.reverbWetDry !== undefined && typeof values.reverbWetDry === 'number') {
@ -195,8 +284,8 @@ export class ReverbEffect implements Effect {
)
}
if (needsRegeneration) {
this.generateReverb(this.currentDecay, this.currentDamping)
if (needsUpdate) {
this.updateDecayAndDamping()
}
}
@ -207,9 +296,26 @@ export class ReverbEffect implements Effect {
this.inputNode.disconnect()
this.outputNode.disconnect()
this.mixNode.disconnect()
this.convolverNode.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()
}
}