modularity

This commit is contained in:
2025-09-30 14:20:50 +02:00
parent 0342de51e2
commit 16dd4c08bf
26 changed files with 892 additions and 198 deletions

View File

@ -0,0 +1,63 @@
import type { Effect } from './Effect.interface'
export class DelayEffect implements Effect {
readonly id = 'delay'
private inputNode: GainNode
private outputNode: GainNode
private delayNode: DelayNode
private feedbackNode: GainNode
private wetNode: GainNode
private dryNode: GainNode
constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.delayNode = audioContext.createDelay(2.0)
this.feedbackNode = audioContext.createGain()
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain()
this.dryNode.gain.value = 1
this.wetNode.gain.value = 0
this.feedbackNode.gain.value = 0.5
this.inputNode.connect(this.dryNode)
this.inputNode.connect(this.delayNode)
this.delayNode.connect(this.feedbackNode)
this.feedbackNode.connect(this.delayNode)
this.delayNode.connect(this.wetNode)
this.dryNode.connect(this.outputNode)
this.wetNode.connect(this.outputNode)
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
updateParams(values: Record<string, number>): void {
if (values.delayTime !== undefined) {
this.delayNode.delayTime.value = values.delayTime / 1000
const delayAmount = Math.min(values.delayTime / 1000, 0.5)
this.wetNode.gain.value = delayAmount
this.dryNode.gain.value = 1 - delayAmount
}
if (values.delayFeedback !== undefined) {
this.feedbackNode.gain.value = values.delayFeedback / 100
}
}
dispose(): void {
this.inputNode.disconnect()
this.outputNode.disconnect()
this.delayNode.disconnect()
this.feedbackNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect()
}
}

View File

@ -0,0 +1,11 @@
export interface Effect {
readonly id: string
getInputNode(): AudioNode
getOutputNode(): AudioNode
updateParams(values: Record<string, number>): void
dispose(): void
}
export interface EffectFactory {
create(audioContext: AudioContext): Effect
}

View File

@ -0,0 +1,65 @@
import type { Effect } from './Effect.interface'
import { DelayEffect } from './DelayEffect'
import { ReverbEffect } from './ReverbEffect'
import { PassThroughEffect } from './PassThroughEffect'
export class EffectsChain {
private inputNode: GainNode
private outputNode: GainNode
private masterGainNode: GainNode
private effects: Effect[]
constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.masterGainNode = audioContext.createGain()
this.effects = [
new DelayEffect(audioContext),
new ReverbEffect(audioContext),
new PassThroughEffect(audioContext, 'bitcrush'),
new PassThroughEffect(audioContext, 'clipmode')
]
this.setupChain()
}
private setupChain(): void {
let currentInput: AudioNode = this.inputNode
for (const effect of this.effects) {
currentInput.connect(effect.getInputNode())
currentInput = effect.getOutputNode()
}
currentInput.connect(this.masterGainNode)
this.masterGainNode.connect(this.outputNode)
}
updateEffects(values: Record<string, number>): void {
for (const effect of this.effects) {
effect.updateParams(values)
}
if (values.masterVolume !== undefined) {
this.masterGainNode.gain.value = values.masterVolume / 100
}
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
dispose(): void {
for (const effect of this.effects) {
effect.dispose()
}
this.inputNode.disconnect()
this.outputNode.disconnect()
this.masterGainNode.disconnect()
}
}

View File

@ -0,0 +1,27 @@
import type { Effect } from './Effect.interface'
export class PassThroughEffect implements Effect {
readonly id: string
private node: GainNode
constructor(audioContext: AudioContext, id: string) {
this.id = id
this.node = audioContext.createGain()
this.node.gain.value = 1
}
getInputNode(): AudioNode {
return this.node
}
getOutputNode(): AudioNode {
return this.node
}
updateParams(_values: Record<string, number>): void {
}
dispose(): void {
this.node.disconnect()
}
}

View File

@ -0,0 +1,70 @@
import type { Effect } from './Effect.interface'
export class ReverbEffect implements Effect {
readonly id = 'reverb'
private audioContext: AudioContext
private inputNode: GainNode
private outputNode: GainNode
private convolverNode: ConvolverNode
private wetNode: GainNode
private dryNode: GainNode
constructor(audioContext: AudioContext) {
this.audioContext = audioContext
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.convolverNode = audioContext.createConvolver()
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain()
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
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.generateImpulseResponse()
}
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)
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)
}
this.convolverNode.buffer = impulse
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
updateParams(values: Record<string, number>): void {
if (values.reverbWetDry !== undefined) {
const wet = values.reverbWetDry / 100
this.wetNode.gain.value = wet
this.dryNode.gain.value = 1 - wet
}
}
dispose(): void {
this.inputNode.disconnect()
this.outputNode.disconnect()
this.convolverNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect()
}
}