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.glitchAmount = 0 this.dcBlockerX = 0 this.dcBlockerY = 0 this.dcBlockerCoeff = 0.995 this.preEmphasisLast = 0 this.deEmphasisLast = 0 this.grainBuffer = new Float32Array(256) this.grainBufferIndex = 0 this.grainPlaybackActive = false this.grainPlaybackIndex = 0 this.grainPlaybackStart = 0 this.grainPlaybackLength = 0 this.grainPlaybackRemaining = 0 this.grainReversed = false this.grainInverted = false 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 case 'glitchAmount': this.glitchAmount = value break } } } clamp(x, min, max) { return Math.max(min, Math.min(max, x)) } dcBlocker(x) { this.dcBlockerY = x - this.dcBlockerX + this.dcBlockerCoeff * this.dcBlockerY this.dcBlockerX = x return this.dcBlockerY } preEmphasis(x) { const amount = 0.7 const output = x - amount * this.preEmphasisLast this.preEmphasisLast = x return output } deEmphasis(x) { const amount = 0.7 const output = x + amount * this.deEmphasisLast this.deEmphasisLast = output return output } tube(x, k) { const gain = 1 + k * 2 const biasAmount = 0.1 * k const bias = biasAmount const driven = (x + bias) * gain if (driven > 1.0) { return 1.0 - Math.exp(-(driven - 1.0) * 2) } else if (driven < -1.0) { return -1.0 + Math.exp((driven + 1.0) * 1.5) } else { return Math.tanh(driven * 1.2) } } tape(x, k) { const gain = 1 + k * 1.5 const driven = x * gain const threshold = 0.3 const knee = 0.5 if (Math.abs(driven) < threshold) { return driven } else { const excess = Math.abs(driven) - threshold const compressed = threshold + excess / (1 + excess / knee) return Math.sign(driven) * compressed } } fuzz(x, k) { const gain = 1 + k * 10 const driven = x * gain const fuzzAmount = Math.tanh(driven * 3) const hardClip = this.clamp(driven, -0.9, 0.9) const mix = Math.min(k / 2, 0.7) return fuzzAmount * mix + hardClip * (1 - mix) } fold(x, k) { const gain = 1 + k * 3 let y = x * gain while (y > 1.0 || y < -1.0) { if (y > 1.0) { y = 2.0 - y } else if (y < -1.0) { y = -2.0 - y } } return Math.sin(y * Math.PI / 2) } crush(x, k) { const gain = 1 + k * 4 let driven = x * gain const foldThreshold = 0.8 let folds = 0 while (Math.abs(driven) > foldThreshold && folds < 8) { if (driven > foldThreshold) { driven = 2 * foldThreshold - driven } else if (driven < -foldThreshold) { driven = -2 * foldThreshold - driven } folds++ } return this.clamp(driven + (Math.random() - 0.5) * k * 0.02, -1, 1) } processDistortion(sample) { let processed = this.preEmphasis(sample) switch (this.clipMode) { case 'tube': processed = this.tube(processed, this.drive) break case 'tape': processed = this.tape(processed, this.drive) break case 'fuzz': processed = this.fuzz(processed, this.drive) break case 'fold': processed = this.fold(processed, this.drive) break case 'crush': processed = this.crush(processed, this.drive) break default: processed = this.fold(processed, this.drive) } processed = this.deEmphasis(processed) return this.dcBlocker(processed) } 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 dither = (Math.random() - 0.5) * step * 0.5 const crushed = Math.floor((sample + dither) / step + 0.5) * step this.lastCrushedValue = this.clamp(crushed, -1, 1) return this.lastCrushedValue } else { return this.lastCrushedValue } } processGlitch(sample) { if (this.glitchAmount === 0) { return sample } this.grainBuffer[this.grainBufferIndex] = sample this.grainBufferIndex = (this.grainBufferIndex + 1) % 256 if (this.grainPlaybackActive) { this.grainPlaybackRemaining-- let readIndex if (this.grainReversed) { readIndex = this.grainPlaybackStart + this.grainPlaybackLength - 1 - (this.grainPlaybackIndex % this.grainPlaybackLength) } else { readIndex = this.grainPlaybackStart + (this.grainPlaybackIndex % this.grainPlaybackLength) } readIndex = readIndex % 256 let output = this.grainBuffer[readIndex] if (this.grainInverted) { output = -output } this.grainPlaybackIndex++ if (this.grainPlaybackRemaining <= 0) { this.grainPlaybackActive = false } return output } const glitchIntensity = this.glitchAmount / 100 const triggerProb = glitchIntensity * 0.001 if (Math.random() < triggerProb) { this.grainPlaybackStart = this.grainBufferIndex this.grainPlaybackLength = Math.floor(16 + Math.random() * 48) this.grainPlaybackRemaining = Math.floor(100 + Math.random() * 200 * glitchIntensity) this.grainPlaybackIndex = 0 this.grainPlaybackActive = true this.grainReversed = Math.random() < 0.4 this.grainInverted = Math.random() < 0.2 let readIndex = this.grainPlaybackStart let output = this.grainBuffer[readIndex] if (this.grainInverted) { output = -output } return output } 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.processDistortion(inputChannel[i]) processed = this.processBitcrush(processed) processed = this.processGlitch(processed) outputChannel[i] = processed } } return true } } registerProcessor('fold-crush-processor', FoldCrushProcessor)