loop mechanism rework

This commit is contained in:
2025-10-06 02:32:19 +02:00
parent 18766f3d8a
commit 5cc10dec0c

View File

@ -18,42 +18,33 @@ export class ReverbEffect implements Effect {
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 earlyReflectionsNode: GainNode
private earlyReflectionDelays: DelayNode[] = []
private earlyReflectionGains: GainNode[] = []
private earlyReflectionFilters: BiquadFilterNode[] = []
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 lowBandSplitter: BiquadFilterNode
private midBandLowPass: BiquadFilterNode
private midBandHighPass: BiquadFilterNode
private highBandSplitter: BiquadFilterNode
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 lowBandProcessor: BandProcessor
private midBandProcessor: BandProcessor
private highBandProcessor: BandProcessor
private leftOut: GainNode
private rightOut: GainNode
private merger: ChannelMergerNode
private lowEnvFollower: DynamicsCompressorNode
private midEnvFollower: DynamicsCompressorNode
private highEnvFollower: DynamicsCompressorNode
private lowToHighModGain: GainNode
private highToLowModGain: GainNode
private midToGlobalModGain: GainNode
private bandMixer: GainNode
constructor(audioContext: AudioContext) {
this.audioContext = audioContext
const sr = audioContext.sampleRate
const scale = sr / 29761
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
@ -73,69 +64,59 @@ 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
this.earlyReflectionsNode = audioContext.createGain()
this.buildEarlyReflections(sr)
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.lowBandSplitter = audioContext.createBiquadFilter()
this.lowBandSplitter.type = 'lowpass'
this.lowBandSplitter.frequency.value = 250
this.lowBandSplitter.Q.value = 0.707
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.midBandHighPass = audioContext.createBiquadFilter()
this.midBandHighPass.type = 'highpass'
this.midBandHighPass.frequency.value = 250
this.midBandHighPass.Q.value = 0.707
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.midBandLowPass = audioContext.createBiquadFilter()
this.midBandLowPass.type = 'lowpass'
this.midBandLowPass.frequency.value = 2500
this.midBandLowPass.Q.value = 0.707
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.highBandSplitter = audioContext.createBiquadFilter()
this.highBandSplitter.type = 'highpass'
this.highBandSplitter.frequency.value = 2500
this.highBandSplitter.Q.value = 0.707
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.lowBandProcessor = new BandProcessor(audioContext, 'low', sr)
this.midBandProcessor = new BandProcessor(audioContext, 'mid', sr)
this.highBandProcessor = new BandProcessor(audioContext, 'high', sr)
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.lowEnvFollower = audioContext.createDynamicsCompressor()
this.lowEnvFollower.threshold.value = -50
this.lowEnvFollower.knee.value = 40
this.lowEnvFollower.ratio.value = 12
this.lowEnvFollower.attack.value = 0.003
this.lowEnvFollower.release.value = 0.25
this.leftOut = audioContext.createGain()
this.rightOut = audioContext.createGain()
this.merger = audioContext.createChannelMerger(2)
this.midEnvFollower = audioContext.createDynamicsCompressor()
this.midEnvFollower.threshold.value = -50
this.midEnvFollower.knee.value = 40
this.midEnvFollower.ratio.value = 12
this.midEnvFollower.attack.value = 0.003
this.midEnvFollower.release.value = 0.25
this.highEnvFollower = audioContext.createDynamicsCompressor()
this.highEnvFollower.threshold.value = -50
this.highEnvFollower.knee.value = 40
this.highEnvFollower.ratio.value = 12
this.highEnvFollower.attack.value = 0.001
this.highEnvFollower.release.value = 0.1
this.lowToHighModGain = audioContext.createGain()
this.highToLowModGain = audioContext.createGain()
this.midToGlobalModGain = audioContext.createGain()
this.bandMixer = audioContext.createGain()
this.buildGraph()
this.updateDecayAndDamping()
@ -147,80 +128,82 @@ export class ReverbEffect implements Effect {
this.pannerNode.connect(this.outputNode)
}
private buildGraph(): void {
const input = this.inputNode
const wet = this.wetNode
private buildEarlyReflections(sr: number): void {
const primes = [17, 29, 41, 59, 71, 97, 113, 127]
const scale = sr / 48000
input.connect(this.preDelay)
for (let i = 0; i < primes.length; i++) {
const delay = this.audioContext.createDelay(0.2)
delay.delayTime.value = (primes[i] * scale) / 1000
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 gain = this.audioContext.createGain()
gain.gain.value = 0.7 * Math.pow(0.85, i)
const splitter = this.audioContext.createGain()
id4Out.connect(splitter)
const filter = this.audioContext.createBiquadFilter()
filter.type = i % 2 === 0 ? 'lowpass' : 'highshelf'
filter.frequency.value = 3000 + i * 500
filter.gain.value = -2 * i
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)
this.earlyReflectionDelays.push(delay)
this.earlyReflectionGains.push(gain)
this.earlyReflectionFilters.push(filter)
}
}
private createAllPass(input: AudioNode, delay: DelayNode, gain: GainNode): AudioNode {
const output = this.audioContext.createGain()
const feedbackGain = this.audioContext.createGain()
feedbackGain.gain.value = -1
private buildGraph(): void {
this.inputNode.connect(this.earlyReflectionsNode)
input.connect(delay)
input.connect(feedbackGain)
feedbackGain.connect(output)
for (let i = 0; i < this.earlyReflectionDelays.length; i++) {
this.earlyReflectionsNode.connect(this.earlyReflectionDelays[i])
this.earlyReflectionDelays[i].connect(this.earlyReflectionFilters[i])
this.earlyReflectionFilters[i].connect(this.earlyReflectionGains[i])
this.earlyReflectionGains[i].connect(this.wetNode)
}
delay.connect(gain)
gain.connect(output)
gain.connect(input)
this.earlyReflectionsNode.connect(this.lowBandSplitter)
this.earlyReflectionsNode.connect(this.midBandHighPass)
this.earlyReflectionsNode.connect(this.highBandSplitter)
return output
this.midBandHighPass.connect(this.midBandLowPass)
this.lowBandSplitter.connect(this.lowBandProcessor.getInputNode())
this.midBandLowPass.connect(this.midBandProcessor.getInputNode())
this.highBandSplitter.connect(this.highBandProcessor.getInputNode())
this.lowBandProcessor.getOutputNode().connect(this.lowEnvFollower)
this.midBandProcessor.getOutputNode().connect(this.midEnvFollower)
this.highBandProcessor.getOutputNode().connect(this.highEnvFollower)
this.lowEnvFollower.connect(this.lowToHighModGain)
this.highEnvFollower.connect(this.highToLowModGain)
this.midEnvFollower.connect(this.midToGlobalModGain)
this.lowToHighModGain.connect(this.highBandProcessor.getModulationTarget())
this.highToLowModGain.connect(this.lowBandProcessor.getModulationTarget())
this.lowBandProcessor.getOutputNode().connect(this.bandMixer)
this.midBandProcessor.getOutputNode().connect(this.bandMixer)
this.highBandProcessor.getOutputNode().connect(this.bandMixer)
this.bandMixer.connect(this.wetNode)
}
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)
this.lowBandProcessor.setDecay(decay * 1.2)
this.midBandProcessor.setDecay(decay)
this.highBandProcessor.setDecay(decay * 0.6)
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)
this.lowBandProcessor.setDamping(damping * 0.5)
this.midBandProcessor.setDamping(damping)
this.highBandProcessor.setDamping(damping * 1.5)
const modAmount = 0.3
this.lowToHighModGain.gain.value = modAmount
this.highToLowModGain.gain.value = modAmount * 0.7
this.midToGlobalModGain.gain.value = modAmount * 0.5
}
getInputNode(): AudioNode {
@ -299,23 +282,270 @@ export class ReverbEffect implements Effect {
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()
this.earlyReflectionsNode.disconnect()
this.earlyReflectionDelays.forEach(d => d.disconnect())
this.earlyReflectionGains.forEach(g => g.disconnect())
this.earlyReflectionFilters.forEach(f => f.disconnect())
this.lowBandSplitter.disconnect()
this.midBandHighPass.disconnect()
this.midBandLowPass.disconnect()
this.highBandSplitter.disconnect()
this.lowBandProcessor.dispose()
this.midBandProcessor.dispose()
this.highBandProcessor.dispose()
this.lowEnvFollower.disconnect()
this.midEnvFollower.disconnect()
this.highEnvFollower.disconnect()
this.lowToHighModGain.disconnect()
this.highToLowModGain.disconnect()
this.midToGlobalModGain.disconnect()
this.bandMixer.disconnect()
}
}
class BandProcessor {
private audioContext: AudioContext
private bandType: 'low' | 'mid' | 'high'
private inputNode: GainNode
private outputNode: GainNode
private modulationTarget: GainNode
private delay1: DelayNode
private delay2: DelayNode
private allpass1: DelayNode
private allpass2: DelayNode
private ap1Gain: GainNode
private ap2Gain: GainNode
private filter1: BiquadFilterNode
private filter2: BiquadFilterNode
private filter3: BiquadFilterNode
private feedbackGain: GainNode
private saturation: WaveShaperNode
private feedbackMixer: GainNode
constructor(audioContext: AudioContext, bandType: 'low' | 'mid' | 'high', sr: number) {
this.audioContext = audioContext
this.bandType = bandType
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.modulationTarget = audioContext.createGain()
this.modulationTarget.gain.value = 0
const scale = sr / 48000
const delayTimes = this.getDelayTimes(bandType, scale, sr)
this.delay1 = audioContext.createDelay(1.0)
this.delay2 = audioContext.createDelay(1.0)
this.delay1.delayTime.value = delayTimes.d1
this.delay2.delayTime.value = delayTimes.d2
this.allpass1 = audioContext.createDelay(0.1)
this.allpass2 = audioContext.createDelay(0.1)
this.allpass1.delayTime.value = delayTimes.ap1
this.allpass2.delayTime.value = delayTimes.ap2
this.ap1Gain = audioContext.createGain()
this.ap2Gain = audioContext.createGain()
this.ap1Gain.gain.value = 0.7
this.ap2Gain.gain.value = 0.7
this.filter1 = audioContext.createBiquadFilter()
this.filter2 = audioContext.createBiquadFilter()
this.filter3 = audioContext.createBiquadFilter()
this.setupFilters(bandType)
this.feedbackGain = audioContext.createGain()
this.feedbackGain.gain.value = 0.5
this.saturation = audioContext.createWaveShaper()
this.saturation.curve = this.createSaturationCurve(bandType)
this.saturation.oversample = '2x'
this.feedbackMixer = audioContext.createGain()
this.buildGraph()
}
private getDelayTimes(bandType: string, scale: number, sr: number) {
const times: Record<string, { d1: number; d2: number; ap1: number; ap2: number }> = {
low: {
d1: (1201 * scale) / sr,
d2: (6171 * scale) / sr,
ap1: (2333 * scale) / sr,
ap2: (4513 * scale) / sr,
},
mid: {
d1: (907 * scale) / sr,
d2: (4217 * scale) / sr,
ap1: (1801 * scale) / sr,
ap2: (3119 * scale) / sr,
},
high: {
d1: (503 * scale) / sr,
d2: (2153 * scale) / sr,
ap1: (907 * scale) / sr,
ap2: (1453 * scale) / sr,
},
}
return times[bandType]
}
private setupFilters(bandType: string): void {
if (bandType === 'low') {
this.filter1.type = 'lowpass'
this.filter1.frequency.value = 1200
this.filter1.Q.value = 0.707
this.filter2.type = 'lowshelf'
this.filter2.frequency.value = 200
this.filter2.gain.value = 2
this.filter3.type = 'peaking'
this.filter3.frequency.value = 600
this.filter3.Q.value = 1.0
this.filter3.gain.value = -3
} else if (bandType === 'mid') {
this.filter1.type = 'lowpass'
this.filter1.frequency.value = 5000
this.filter1.Q.value = 0.707
this.filter2.type = 'peaking'
this.filter2.frequency.value = 1200
this.filter2.Q.value = 1.5
this.filter2.gain.value = -2
this.filter3.type = 'highshelf'
this.filter3.frequency.value = 3000
this.filter3.gain.value = -4
} else {
this.filter1.type = 'lowpass'
this.filter1.frequency.value = 12000
this.filter1.Q.value = 0.5
this.filter2.type = 'lowpass'
this.filter2.frequency.value = 8000
this.filter2.Q.value = 0.707
this.filter3.type = 'highshelf'
this.filter3.frequency.value = 5000
this.filter3.gain.value = -6
}
}
private createSaturationCurve(bandType: string): Float32Array {
const samples = 4096
const curve = new Float32Array(samples)
const amount = bandType === 'low' ? 0.8 : bandType === 'mid' ? 0.5 : 0.3
for (let i = 0; i < samples; i++) {
const x = (i * 2) / samples - 1
curve[i] = Math.tanh(x * (1 + amount)) / (1 + amount * 0.5)
}
return curve
}
private buildGraph(): void {
this.inputNode.connect(this.delay1)
this.delay1.connect(this.filter1)
this.filter1.connect(this.filter2)
this.filter2.connect(this.filter3)
this.filter3.connect(this.delay2)
const ap1Out = this.createAllPass(this.delay2, this.allpass1, this.ap1Gain)
const ap2Out = this.createAllPass(ap1Out, this.allpass2, this.ap2Gain)
ap2Out.connect(this.feedbackGain)
this.feedbackGain.connect(this.saturation)
this.saturation.connect(this.feedbackMixer)
this.modulationTarget.connect(this.feedbackMixer)
this.feedbackMixer.connect(this.inputNode)
ap2Out.connect(this.outputNode)
}
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
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
getModulationTarget(): AudioNode {
return this.modulationTarget
}
setDecay(decay: number): void {
this.feedbackGain.gain.setTargetAtTime(
Math.min(0.95, decay),
this.audioContext.currentTime,
0.01
)
}
setDamping(damping: number): void {
let cutoff: number
if (this.bandType === 'low') {
cutoff = 500 + damping * 1500
} else if (this.bandType === 'mid') {
cutoff = 2000 + damping * 6000
} else {
cutoff = 4000 + damping * 10000
}
this.filter1.frequency.setTargetAtTime(
cutoff,
this.audioContext.currentTime,
0.01
)
}
dispose(): void {
this.inputNode.disconnect()
this.outputNode.disconnect()
this.modulationTarget.disconnect()
this.delay1.disconnect()
this.delay2.disconnect()
this.allpass1.disconnect()
this.allpass2.disconnect()
this.ap1Gain.disconnect()
this.ap2Gain.disconnect()
this.filter1.disconnect()
this.filter2.disconnect()
this.filter3.disconnect()
this.feedbackGain.disconnect()
this.saturation.disconnect()
this.feedbackMixer.disconnect()
}
}