This commit is contained in:
2025-10-06 16:36:59 +02:00
parent 90f2f4209c
commit 9d26ea5cd7
15 changed files with 1031 additions and 595 deletions

View File

@ -8,6 +8,24 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
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
@ -24,6 +42,9 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
case 'crushAmount':
this.crushAmount = value
break
case 'glitchAmount':
this.glitchAmount = value
break
}
}
}
@ -32,62 +53,128 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
return Math.max(min, Math.min(max, x))
}
mod(x, y) {
return ((x % y) + y) % y
dcBlocker(x) {
this.dcBlockerY = x - this.dcBlockerX + this.dcBlockerCoeff * this.dcBlockerY
this.dcBlockerX = x
return this.dcBlockerY
}
squash(x) {
return x / (1 + Math.abs(x))
preEmphasis(x) {
const amount = 0.7
const output = x - amount * this.preEmphasisLast
this.preEmphasisLast = x
return output
}
soft(x, k) {
return Math.tanh(x * (1 + k))
deEmphasis(x) {
const amount = 0.7
const output = x + amount * this.deEmphasisLast
this.deEmphasisLast = output
return output
}
hard(x, k) {
return this.clamp((1 + k) * x, -1, 1)
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) {
let y = (1 + 0.5 * k) * x
const window = this.mod(y + 1, 4)
return 1 - Math.abs(window - 2)
}
const gain = 1 + k * 3
let y = x * gain
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
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) {
@ -102,19 +189,71 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
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))
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
}
}
safetyLimiter(sample) {
const threshold = 0.8
if (Math.abs(sample) > threshold) {
return Math.tanh(sample * 0.9) / Math.tanh(0.9)
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
}
@ -127,9 +266,9 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
const outputChannel = output[0]
for (let i = 0; i < inputChannel.length; i++) {
let processed = this.processWavefolder(inputChannel[i])
let processed = this.processDistortion(inputChannel[i])
processed = this.processBitcrush(processed)
processed = this.safetyLimiter(processed)
processed = this.processGlitch(processed)
outputChannel[i] = processed
}
}