class FoldCrushProcessor extends AudioWorkletProcessor { constructor() { super() this.clipMode = 'fold' this.drive = 1 this.bitDepth = 16 this.crushAmount = 0 this.bitcrushPhase = 0 this.lastCrushedValue = 0 this.port.onmessage = (event) => { const { type, value } = event.data switch (type) { case 'clipMode': this.clipMode = value break case 'drive': this.drive = value break case 'bitDepth': this.bitDepth = value break case 'crushAmount': this.crushAmount = value break } } } clamp(x, min, max) { return Math.max(min, Math.min(max, x)) } mod(x, y) { return ((x % y) + y) % y } squash(x) { return x / (1 + Math.abs(x)) } soft(x, k) { return Math.tanh(x * (1 + k)) } hard(x, k) { return this.clamp((1 + k) * x, -1, 1) } fold(x, k) { let y = (1 + 0.5 * k) * x const window = this.mod(y + 1, 4) return 1 - Math.abs(window - 2) } cubic(x, k) { const t = this.squash(Math.log1p(k)) const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3) return this.soft(cubic, k) } diode(x, k) { const g = 1 + 2 * k const t = this.squash(Math.log1p(k)) const bias = 0.07 * t const pos = this.soft(x + bias, 2 * k) const neg = this.soft(-x + bias, 2 * k) const y = pos - neg const sech = 1 / Math.cosh(g * bias) const sech2 = sech * sech const denom = Math.max(1e-8, 2 * g * sech2) return this.soft(y / denom, k) } processWavefolder(sample) { switch (this.clipMode) { case 'soft': return this.soft(sample, this.drive) case 'hard': return this.hard(sample, this.drive) case 'fold': return this.fold(sample, this.drive) case 'cubic': return this.cubic(sample, this.drive) case 'diode': return this.diode(sample, this.drive) default: return sample } } processBitcrush(sample) { 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 } } safetyLimiter(sample) { const threshold = 0.8 if (Math.abs(sample) > threshold) { return Math.tanh(sample * 0.9) / Math.tanh(0.9) } return sample } process(inputs, outputs) { const input = inputs[0] const output = outputs[0] if (input.length > 0 && output.length > 0) { const inputChannel = input[0] const outputChannel = output[0] for (let i = 0; i < inputChannel.length; i++) { let processed = this.processWavefolder(inputChannel[i]) processed = this.processBitcrush(processed) processed = this.safetyLimiter(processed) outputChannel[i] = processed } } return true } } registerProcessor('fold-crush-processor', FoldCrushProcessor)