import type { Effect } from './Effect.interface' type ClipMode = 'wrap' | 'clamp' | 'fold' export class FoldCrushEffect implements Effect { readonly id = 'foldcrush' private inputNode: GainNode private outputNode: GainNode private processorNode: ScriptProcessorNode private wetNode: GainNode private dryNode: GainNode private clipMode: ClipMode = 'wrap' private drive: number = 1 private bitDepth: number = 16 private crushAmount: number = 0 constructor(audioContext: AudioContext) { this.inputNode = audioContext.createGain() this.outputNode = audioContext.createGain() this.processorNode = audioContext.createScriptProcessor(4096, 1, 1) this.wetNode = audioContext.createGain() this.dryNode = audioContext.createGain() this.wetNode.gain.value = 1 this.dryNode.gain.value = 0 this.processorNode.onaudioprocess = (e) => { const input = e.inputBuffer.getChannelData(0) const output = e.outputBuffer.getChannelData(0) for (let i = 0; i < input.length; i++) { const driven = input[i] * this.drive let processed = this.processWavefolder(driven) processed = this.processBitcrush(processed, i, output) output[i] = processed } } this.inputNode.connect(this.dryNode) this.inputNode.connect(this.processorNode) this.processorNode.connect(this.wetNode) this.dryNode.connect(this.outputNode) this.wetNode.connect(this.outputNode) } private processWavefolder(sample: number): number { switch (this.clipMode) { case 'wrap': return this.wrap(sample) case 'clamp': return this.clamp(sample) case 'fold': return this.fold(sample) default: return sample } } private wrap(sample: number): number { const range = 2.0 let wrapped = sample while (wrapped > 1.0) wrapped -= range while (wrapped < -1.0) wrapped += range return wrapped } private clamp(sample: number): number { return Math.max(-1.0, Math.min(1.0, sample)) } private fold(sample: number): number { let folded = sample while (folded > 1.0 || folded < -1.0) { if (folded > 1.0) { folded = 2.0 - folded } if (folded < -1.0) { folded = -2.0 - folded } } return folded } private bitcrushPhase: number = 0 private lastCrushedValue: number = 0 private processBitcrush(sample: number, index: number, output: Float32Array): number { if (this.crushAmount === 0 && this.bitDepth === 16) { return sample } const step = Math.pow(0.5, this.bitDepth) const phaseIncrement = 1 - (this.crushAmount / 100) this.bitcrushPhase += phaseIncrement if (this.bitcrushPhase >= 1.0) { this.bitcrushPhase -= 1.0 const crushed = Math.floor(sample / step + 0.5) * step this.lastCrushedValue = Math.max(-1, Math.min(1, crushed)) return this.lastCrushedValue } else { return this.lastCrushedValue } } getInputNode(): AudioNode { return this.inputNode } getOutputNode(): AudioNode { return this.outputNode } setBypass(bypass: boolean): void { if (bypass) { this.wetNode.gain.value = 0 this.dryNode.gain.value = 1 } else { this.wetNode.gain.value = 1 this.dryNode.gain.value = 0 } } updateParams(values: Record): void { if (values.clipMode !== undefined) { const modeIndex = values.clipMode this.clipMode = ['wrap', 'clamp', 'fold'][modeIndex] as ClipMode || 'wrap' } if (values.wavefolderDrive !== undefined) { this.drive = values.wavefolderDrive } if (values.bitcrushDepth !== undefined) { this.bitDepth = values.bitcrushDepth } if (values.bitcrushRate !== undefined) { this.crushAmount = values.bitcrushRate } } dispose(): void { this.processorNode.disconnect() this.wetNode.disconnect() this.dryNode.disconnect() this.inputNode.disconnect() this.outputNode.disconnect() } }