Better reverberation
This commit is contained in:
@ -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)
|
||||
|
||||
const lpFreqStart = 10000
|
||||
const lpFreqEnd = 200 + (damping / 100) * 7800
|
||||
|
||||
this.applyGradualLowpass(reverbIR, lpFreqStart, lpFreqEnd, decayTime, (buffer) => {
|
||||
this.convolverNode.buffer = buffer
|
||||
})
|
||||
return output
|
||||
}
|
||||
|
||||
private applyGradualLowpass(
|
||||
input: AudioBuffer,
|
||||
lpFreqStart: number,
|
||||
lpFreqEnd: number,
|
||||
lpFreqEndAt: number,
|
||||
callback: (buffer: AudioBuffer) => void
|
||||
): void {
|
||||
if (lpFreqStart === 0) {
|
||||
callback(input)
|
||||
return
|
||||
}
|
||||
private updateDecayAndDamping(): void {
|
||||
const decay = this.currentDecay
|
||||
const damping = this.currentDamping
|
||||
|
||||
const context = new OfflineAudioContext(
|
||||
input.numberOfChannels,
|
||||
input.length,
|
||||
input.sampleRate
|
||||
)
|
||||
this.leftGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01)
|
||||
this.rightGain.gain.setTargetAtTime(decay, this.audioContext.currentTime, 0.01)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user