This commit is contained in:
2025-09-30 21:19:43 +02:00
parent d867f12fcd
commit 21c983b41e
7 changed files with 339 additions and 31 deletions

View File

@ -6,45 +6,120 @@ export class ReverbEffect implements Effect {
private audioContext: AudioContext
private inputNode: GainNode
private outputNode: GainNode
private finalOutputNode: GainNode
private convolverNode: ConvolverNode
private wetNode: GainNode
private dryNode: 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 = 2
private currentDamping: number = 50
constructor(audioContext: AudioContext) {
this.audioContext = audioContext
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.finalOutputNode = audioContext.createGain()
this.convolverNode = audioContext.createConvolver()
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.inputNode.connect(this.dryNode)
this.inputNode.connect(this.convolverNode)
this.convolverNode.connect(this.wetNode)
this.dryNode.connect(this.outputNode)
this.wetNode.connect(this.outputNode)
this.outputNode.connect(this.pannerNode)
this.pannerNode.connect(this.finalOutputNode)
this.generateImpulseResponse()
this.generateReverb(this.currentDecay, this.currentDamping)
}
private generateImpulseResponse(): void {
const length = this.audioContext.sampleRate * 2
const impulse = this.audioContext.createBuffer(2, length, this.audioContext.sampleRate)
const left = impulse.getChannelData(0)
const right = impulse.getChannelData(1)
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)
for (let i = 0; i < length; i++) {
left[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
right[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
const reverbIR = this.audioContext.createBuffer(numChannels, numSampleFrames, sampleRate)
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
}
}
this.convolverNode.buffer = impulse
const lpFreqStart = 10000
const lpFreqEnd = 200 + (damping / 100) * 7800
this.applyGradualLowpass(reverbIR, lpFreqStart, lpFreqEnd, decayTime, (buffer) => {
this.convolverNode.buffer = buffer
})
}
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()
}
getInputNode(): AudioNode {
@ -52,7 +127,7 @@ export class ReverbEffect implements Effect {
}
getOutputNode(): AudioNode {
return this.outputNode
return this.finalOutputNode
}
setBypass(bypass: boolean): void {
@ -67,6 +142,18 @@ export class ReverbEffect implements Effect {
}
updateParams(values: Record<string, number>): void {
let needsRegeneration = false
if (values.reverbDecay !== undefined && values.reverbDecay !== this.currentDecay) {
this.currentDecay = values.reverbDecay
needsRegeneration = true
}
if (values.reverbDamping !== undefined && values.reverbDamping !== this.currentDamping) {
this.currentDamping = values.reverbDamping
needsRegeneration = true
}
if (values.reverbWetDry !== undefined) {
const wet = values.reverbWetDry / 100
this.currentWetValue = wet
@ -77,13 +164,40 @@ export class ReverbEffect implements Effect {
this.dryNode.gain.value = this.currentDryValue
}
}
if (values.reverbPanRate !== undefined) {
const rate = values.reverbPanRate
this.panLfoNode.frequency.setTargetAtTime(
rate,
this.audioContext.currentTime,
0.01
)
}
if (values.reverbPanWidth !== undefined) {
const width = values.reverbPanWidth / 100
this.panLfoGainNode.gain.setTargetAtTime(
width,
this.audioContext.currentTime,
0.01
)
}
if (needsRegeneration) {
this.generateReverb(this.currentDecay, this.currentDamping)
}
}
dispose(): void {
this.panLfoNode.stop()
this.panLfoNode.disconnect()
this.panLfoGainNode.disconnect()
this.inputNode.disconnect()
this.outputNode.disconnect()
this.finalOutputNode.disconnect()
this.convolverNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect()
this.pannerNode.disconnect()
}
}
}