281 lines
6.7 KiB
JavaScript
281 lines
6.7 KiB
JavaScript
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)
|