diff --git a/public/worklets/chorus-processor.js b/public/worklets/chorus-processor.js
new file mode 100644
index 00000000..4f7a77c9
--- /dev/null
+++ b/public/worklets/chorus-processor.js
@@ -0,0 +1,167 @@
+class ChorusProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super()
+
+ this.mode = 'chorus'
+ this.rate = 0.5
+ this.depth = 0.5
+ this.feedback = 0
+ this.spread = 0.3
+ this.bypassed = false
+
+ this.delayBufferSize = Math.floor(sampleRate * 0.05)
+ this.delayBufferL = new Float32Array(this.delayBufferSize)
+ this.delayBufferR = new Float32Array(this.delayBufferSize)
+ this.writeIndex = 0
+
+ this.lfoPhase = 0
+ this.lfoPhaseRight = 0
+
+ this.port.onmessage = (event) => {
+ const { type, value } = event.data
+ switch (type) {
+ case 'mode':
+ this.mode = value
+ break
+ case 'frequency':
+ this.rate = value
+ break
+ case 'depth':
+ this.depth = value
+ break
+ case 'feedback':
+ this.feedback = value
+ break
+ case 'spread':
+ this.spread = value
+ break
+ case 'bypass':
+ this.bypassed = value
+ break
+ }
+ }
+ }
+
+ processChorus(sampleL, sampleR) {
+ const baseDelay = 15
+ const maxDepth = 8
+
+ this.lfoPhase += this.rate / sampleRate
+ this.lfoPhaseRight += this.rate / sampleRate
+
+ if (this.lfoPhase >= 1) this.lfoPhase -= 1
+ if (this.lfoPhaseRight >= 1) this.lfoPhaseRight -= 1
+
+ const spreadPhase = this.spread * 0.5
+ const lfoL = Math.sin(this.lfoPhase * Math.PI * 2)
+ const lfoR = Math.sin((this.lfoPhaseRight + spreadPhase) * Math.PI * 2)
+
+ const delayTimeL = baseDelay + lfoL * maxDepth * this.depth
+ const delayTimeR = baseDelay + lfoR * maxDepth * this.depth
+
+ const delaySamplesL = (delayTimeL / 1000) * sampleRate
+ const delaySamplesR = (delayTimeR / 1000) * sampleRate
+
+ const readIndexL = (this.writeIndex - delaySamplesL + this.delayBufferSize) % this.delayBufferSize
+ const readIndexR = (this.writeIndex - delaySamplesR + this.delayBufferSize) % this.delayBufferSize
+
+ const readIndexL0 = Math.floor(readIndexL) % this.delayBufferSize
+ const readIndexL1 = (readIndexL0 + 1) % this.delayBufferSize
+ const fracL = readIndexL - Math.floor(readIndexL)
+
+ const readIndexR0 = Math.floor(readIndexR) % this.delayBufferSize
+ const readIndexR1 = (readIndexR0 + 1) % this.delayBufferSize
+ const fracR = readIndexR - Math.floor(readIndexR)
+
+ const delayedL = this.delayBufferL[readIndexL0] * (1 - fracL) + this.delayBufferL[readIndexL1] * fracL
+ const delayedR = this.delayBufferR[readIndexR0] * (1 - fracR) + this.delayBufferR[readIndexR1] * fracR
+
+ this.delayBufferL[this.writeIndex] = sampleL + delayedL * this.feedback
+ this.delayBufferR[this.writeIndex] = sampleR + delayedR * this.feedback
+
+ return [delayedL, delayedR]
+ }
+
+ processFlanger(sampleL, sampleR) {
+ const baseDelay = 1
+ const maxDepth = 5
+
+ this.lfoPhase += this.rate / sampleRate
+ this.lfoPhaseRight += this.rate / sampleRate
+
+ if (this.lfoPhase >= 1) this.lfoPhase -= 1
+ if (this.lfoPhaseRight >= 1) this.lfoPhaseRight -= 1
+
+ const spreadPhase = this.spread * 0.5
+ const lfoL = Math.sin(this.lfoPhase * Math.PI * 2)
+ const lfoR = Math.sin((this.lfoPhaseRight + spreadPhase) * Math.PI * 2)
+
+ const delayTimeL = baseDelay + lfoL * maxDepth * this.depth
+ const delayTimeR = baseDelay + lfoR * maxDepth * this.depth
+
+ const delaySamplesL = (delayTimeL / 1000) * sampleRate
+ const delaySamplesR = (delayTimeR / 1000) * sampleRate
+
+ const readIndexL = (this.writeIndex - delaySamplesL + this.delayBufferSize) % this.delayBufferSize
+ const readIndexR = (this.writeIndex - delaySamplesR + this.delayBufferSize) % this.delayBufferSize
+
+ const readIndexL0 = Math.floor(readIndexL) % this.delayBufferSize
+ const readIndexL1 = (readIndexL0 + 1) % this.delayBufferSize
+ const fracL = readIndexL - Math.floor(readIndexL)
+
+ const readIndexR0 = Math.floor(readIndexR) % this.delayBufferSize
+ const readIndexR1 = (readIndexR0 + 1) % this.delayBufferSize
+ const fracR = readIndexR - Math.floor(readIndexR)
+
+ const delayedL = this.delayBufferL[readIndexL0] * (1 - fracL) + this.delayBufferL[readIndexL1] * fracL
+ const delayedR = this.delayBufferR[readIndexR0] * (1 - fracR) + this.delayBufferR[readIndexR1] * fracR
+
+ this.delayBufferL[this.writeIndex] = sampleL + delayedL * this.feedback * 0.9
+ this.delayBufferR[this.writeIndex] = sampleR + delayedR * this.feedback * 0.9
+
+ return [delayedL, delayedR]
+ }
+
+ process(inputs, outputs) {
+ const input = inputs[0]
+ const output = outputs[0]
+
+ if (!input || input.length === 0 || !output || output.length === 0) {
+ return true
+ }
+
+ const inputL = input[0]
+ const inputR = input[1] || input[0]
+ const outputL = output[0]
+ const outputR = output[1] || output[0]
+
+ if (!inputL || !outputL) {
+ return true
+ }
+
+ for (let i = 0; i < inputL.length; i++) {
+ if (this.bypassed) {
+ outputL[i] = inputL[i]
+ if (outputR) outputR[i] = inputR[i]
+ continue
+ }
+
+ let processedL, processedR
+
+ if (this.mode === 'flanger') {
+ [processedL, processedR] = this.processFlanger(inputL[i], inputR[i])
+ } else {
+ [processedL, processedR] = this.processChorus(inputL[i], inputR[i])
+ }
+
+ outputL[i] = processedL
+ if (outputR) outputR[i] = processedR
+
+ this.writeIndex = (this.writeIndex + 1) % this.delayBufferSize
+ }
+
+ return true
+ }
+}
+
+registerProcessor('chorus-processor', ChorusProcessor)
diff --git a/public/worklets/fm-processor.js b/public/worklets/fm-processor.js
index 0b17450f..245aa64d 100644
--- a/public/worklets/fm-processor.js
+++ b/public/worklets/fm-processor.js
@@ -35,6 +35,16 @@ class FMProcessor extends AudioWorkletProcessor {
this.pitchLFODepth = 0.1
this.pitchLFOBaseRate = 2.0
+ this.sampleHoldValue = 0
+ this.sampleHoldCounter = 0
+ this.sampleHoldInterval = 500
+ this.driftValue = 0
+ this.perlinA = Math.random() * 2 - 1
+ this.perlinB = Math.random() * 2 - 1
+ this.perlinPhase = 0
+ this.perlinInterval = 2000
+ this.chaosX = 0.5
+
this.port.onmessage = (event) => {
const { type, value } = event.data
switch (type) {
@@ -72,6 +82,13 @@ class FMProcessor extends AudioWorkletProcessor {
this.phase4 = 0
this.sampleCount = 0
this.feedbackSample = 0
+ this.sampleHoldValue = 0
+ this.sampleHoldCounter = 0
+ this.driftValue = 0
+ this.perlinA = Math.random() * 2 - 1
+ this.perlinB = Math.random() * 2 - 1
+ this.perlinPhase = 0
+ this.chaosX = 0.5
break
case 'loopLength':
this.loopLength = value
@@ -96,6 +113,32 @@ class FMProcessor extends AudioWorkletProcessor {
return normalizedPhase % 1 < 0.5 ? 1 : -1
case 3: // sawtooth
return 2 * (normalizedPhase % 1) - 1
+ case 4: // sample & hold random (glitchy)
+ this.sampleHoldCounter++
+ if (this.sampleHoldCounter >= this.sampleHoldInterval) {
+ this.sampleHoldValue = Math.random() * 2 - 1
+ this.sampleHoldCounter = 0
+ this.sampleHoldInterval = Math.floor(100 + Math.random() * 900)
+ }
+ return this.sampleHoldValue
+ case 5: // drift (random walk)
+ this.driftValue += (Math.random() - 0.5) * 0.002
+ this.driftValue = Math.max(-1, Math.min(1, this.driftValue))
+ return this.driftValue
+ case 6: // perlin noise (smooth random)
+ this.perlinPhase++
+ if (this.perlinPhase >= this.perlinInterval) {
+ this.perlinA = this.perlinB
+ this.perlinB = Math.random() * 2 - 1
+ this.perlinPhase = 0
+ this.perlinInterval = Math.floor(1000 + Math.random() * 2000)
+ }
+ const t = this.perlinPhase / this.perlinInterval
+ const smoothT = t * t * (3 - 2 * t)
+ return this.perlinA + (this.perlinB - this.perlinA) * smoothT
+ case 7: // chaos (logistic map)
+ this.chaosX = 3.9 * this.chaosX * (1 - this.chaosX)
+ return this.chaosX * 2 - 1
default:
return 0
}
@@ -103,7 +146,7 @@ class FMProcessor extends AudioWorkletProcessor {
synthesize(algorithm) {
const TWO_PI = Math.PI * 2
- const sampleRate = 44100
+ const sampleRate = globalThis.sampleRate || 44100
const avgDiff = (Math.abs(this.opLevel1 - this.opLevel3) + Math.abs(this.opLevel2 - this.opLevel4)) / (2 * 255)
const pitchLFORate = this.pitchLFOBaseRate * (0.3 + avgDiff * 1.4)
@@ -129,6 +172,17 @@ class FMProcessor extends AudioWorkletProcessor {
const level3 = (this.opLevel3 / 255.0) * (1 + this.lfoDepth * lfo3)
const level4 = (this.opLevel4 / 255.0) * (1 + this.lfoDepth * lfo4)
+ const nyquist = sampleRate / 2
+ const maxCarrierFreq = Math.max(
+ modulatedBaseFreq * this.frequencyRatios[0],
+ modulatedBaseFreq * this.frequencyRatios[1],
+ modulatedBaseFreq * this.frequencyRatios[2],
+ modulatedBaseFreq * this.frequencyRatios[3]
+ )
+ const antiAliasFactor = Math.min(1.0, nyquist / (maxCarrierFreq * 5))
+ const modDepth = 10 * antiAliasFactor
+ const modDepthLight = 1.5 * antiAliasFactor
+
this.lfoPhase1 += (this.lfoRate1 * TWO_PI) / sampleRate
this.lfoPhase2 += (this.lfoRate2 * TWO_PI) / sampleRate
this.lfoPhase3 += (this.lfoRate3 * TWO_PI) / sampleRate
@@ -157,12 +211,12 @@ class FMProcessor extends AudioWorkletProcessor {
case 1: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
- const mod3 = op3 * 10
- const op4 = Math.sin(this.phase4 + mod3 + this.feedbackSample * this.feedback * 10) * level4
+ const mod3 = op3 * modDepth
+ const op4 = Math.sin(this.phase4 + mod3 + this.feedbackSample * this.feedback * modDepth) * level4
output = op4
this.feedbackSample = op4
this.phase1 += freq1 * this.playbackRate
@@ -174,10 +228,10 @@ class FMProcessor extends AudioWorkletProcessor {
case 2: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const op3 = Math.sin(this.phase3) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op2 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
@@ -189,9 +243,9 @@ class FMProcessor extends AudioWorkletProcessor {
case 3: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
const op4 = Math.sin(this.phase4) * level4
output = (op3 + op4) * 0.5
@@ -204,11 +258,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 4: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1 + mod2) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
@@ -220,11 +274,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 5: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
@@ -236,7 +290,7 @@ class FMProcessor extends AudioWorkletProcessor {
case 6: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const op3 = Math.sin(this.phase3) * level3
const op4 = Math.sin(this.phase4) * level4
@@ -250,11 +304,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 7: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 1.5
+ const mod2 = op2 * modDepthLight
const op3 = Math.sin(this.phase3 + mod1) * level3
- const mod3 = op3 * 1.5
+ const mod3 = op3 * modDepthLight
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
@@ -266,10 +320,10 @@ class FMProcessor extends AudioWorkletProcessor {
case 8: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
const op2 = Math.sin(this.phase2) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op4 = Math.sin(this.phase4 + mod2) * level4
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
@@ -281,10 +335,10 @@ class FMProcessor extends AudioWorkletProcessor {
case 9: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op4 = Math.sin(this.phase4 + mod1) * level4
const op2 = Math.sin(this.phase2) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
@@ -296,10 +350,10 @@ class FMProcessor extends AudioWorkletProcessor {
case 10: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const op3 = Math.sin(this.phase3) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op2 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
@@ -312,9 +366,9 @@ class FMProcessor extends AudioWorkletProcessor {
case 11: {
const op1 = Math.sin(this.phase1) * level1
const op2 = Math.sin(this.phase2) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op1 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
@@ -326,9 +380,9 @@ class FMProcessor extends AudioWorkletProcessor {
case 12: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op4 = Math.sin(this.phase4 + mod2) * level4
const op3 = Math.sin(this.phase3) * level3
output = (op3 + op4) * 0.5
@@ -341,11 +395,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 13: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
@@ -357,11 +411,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 14: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op3 = Math.sin(this.phase3) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
- const mod4 = op4 * 1.5
+ const mod4 = op4 * modDepthLight
const op2 = Math.sin(this.phase2 + mod1 + mod4) * level2
output = op2
this.phase1 += freq1 * this.playbackRate
@@ -373,11 +427,11 @@ class FMProcessor extends AudioWorkletProcessor {
case 15: {
const op1 = Math.sin(this.phase1) * level1
- const mod1 = op1 * 10
+ const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2) * level2
- const mod2 = op2 * 10
+ const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3) * level3
- const mod3 = op3 * 10
+ const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod1 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
@@ -391,11 +445,10 @@ class FMProcessor extends AudioWorkletProcessor {
output = 0
}
- const TWO_PI_LIMIT = TWO_PI * 10
- if (this.phase1 > TWO_PI_LIMIT) this.phase1 -= TWO_PI_LIMIT
- if (this.phase2 > TWO_PI_LIMIT) this.phase2 -= TWO_PI_LIMIT
- if (this.phase3 > TWO_PI_LIMIT) this.phase3 -= TWO_PI_LIMIT
- if (this.phase4 > TWO_PI_LIMIT) this.phase4 -= TWO_PI_LIMIT
+ this.phase1 = this.phase1 % TWO_PI
+ this.phase2 = this.phase2 % TWO_PI
+ this.phase3 = this.phase3 % TWO_PI
+ this.phase4 = this.phase4 % TWO_PI
return output
}
diff --git a/public/worklets/fold-crush-processor.js b/public/worklets/fold-crush-processor.js
index 6d0ee5ee..500638ad 100644
--- a/public/worklets/fold-crush-processor.js
+++ b/public/worklets/fold-crush-processor.js
@@ -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
}
}
diff --git a/public/worklets/output-limiter.js b/public/worklets/output-limiter.js
index 6e35ee09..feed8718 100644
--- a/public/worklets/output-limiter.js
+++ b/public/worklets/output-limiter.js
@@ -10,9 +10,9 @@ class OutputLimiter extends AudioWorkletProcessor {
},
{
name: 'makeup',
- defaultValue: 1.5,
- minValue: 1.0,
- maxValue: 3.0,
+ defaultValue: 0.5,
+ minValue: 0.1,
+ maxValue: 2.0,
automationRate: 'k-rate'
}
]
@@ -47,8 +47,8 @@ class OutputLimiter extends AudioWorkletProcessor {
const outputChannel = output[channel]
for (let i = 0; i < inputChannel.length; i++) {
- let sample = inputChannel[i] * makeup
- sample = this.softClip(sample, threshold)
+ let sample = this.softClip(inputChannel[i], threshold)
+ sample = sample * makeup
outputChannel[i] = sample
}
}
diff --git a/public/worklets/ring-mod-processor.js b/public/worklets/ring-mod-processor.js
new file mode 100644
index 00000000..0316b403
--- /dev/null
+++ b/public/worklets/ring-mod-processor.js
@@ -0,0 +1,92 @@
+class RingModProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super()
+
+ this.frequency = 200
+ this.shape = 'sine'
+ this.spread = 0
+ this.bypassed = false
+
+ this.phase = 0
+ this.phaseRight = 0
+
+ this.port.onmessage = (event) => {
+ const { type, value } = event.data
+ switch (type) {
+ case 'frequency':
+ this.frequency = value
+ break
+ case 'shape':
+ this.shape = value
+ break
+ case 'spread':
+ this.spread = value
+ break
+ case 'bypass':
+ this.bypassed = value
+ break
+ }
+ }
+ }
+
+ generateWaveform(phase, shape) {
+ switch (shape) {
+ case 'sine':
+ return Math.sin(phase * Math.PI * 2)
+ case 'square':
+ return phase < 0.5 ? 1 : -1
+ case 'saw':
+ return 2 * phase - 1
+ case 'triangle':
+ return phase < 0.5 ? 4 * phase - 1 : 3 - 4 * phase
+ default:
+ return Math.sin(phase * Math.PI * 2)
+ }
+ }
+
+ process(inputs, outputs) {
+ const input = inputs[0]
+ const output = outputs[0]
+
+ if (!input || input.length === 0 || !output || output.length === 0) {
+ return true
+ }
+
+ const inputL = input[0]
+ const inputR = input[1] || input[0]
+ const outputL = output[0]
+ const outputR = output[1] || output[0]
+
+ if (!inputL || !outputL) {
+ return true
+ }
+
+ for (let i = 0; i < inputL.length; i++) {
+ if (this.bypassed) {
+ outputL[i] = inputL[i]
+ if (outputR) outputR[i] = inputR[i]
+ continue
+ }
+
+ const spreadAmount = this.spread * 0.1
+ const freqL = this.frequency * (1 - spreadAmount)
+ const freqR = this.frequency * (1 + spreadAmount)
+
+ this.phase += freqL / sampleRate
+ this.phaseRight += freqR / sampleRate
+
+ if (this.phase >= 1) this.phase -= 1
+ if (this.phaseRight >= 1) this.phaseRight -= 1
+
+ const carrierL = this.generateWaveform(this.phase, this.shape)
+ const carrierR = this.generateWaveform(this.phaseRight, this.shape)
+
+ outputL[i] = inputL[i] * carrierL
+ if (outputR) outputR[i] = inputR[i] * carrierR
+ }
+
+ return true
+ }
+}
+
+registerProcessor('ring-mod-processor', RingModProcessor)
diff --git a/src/components/controls/EffectsBar.tsx b/src/components/controls/EffectsBar.tsx
index b7e9f1ce..a7fb5849 100644
--- a/src/components/controls/EffectsBar.tsx
+++ b/src/components/controls/EffectsBar.tsx
@@ -36,7 +36,7 @@ export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: Effe
return (
{/* Desktop: Grid layout */}
-
+
{EFFECTS.map(effect => {
return (
diff --git a/src/components/ui/Knob.tsx b/src/components/ui/Knob.tsx
index 2d6305bc..37181ae0 100644
--- a/src/components/ui/Knob.tsx
+++ b/src/components/ui/Knob.tsx
@@ -67,6 +67,19 @@ export function Knob({
e.preventDefault()
}
+ const handleTouchStart = (e: React.TouchEvent) => {
+ if (isInMappingMode && paramId && mappingModeState.activeLFO !== null && onMapClick) {
+ onMapClick(paramId, mappingModeState.activeLFO)
+ e.preventDefault()
+ return
+ }
+
+ setIsDragging(true)
+ startYRef.current = e.touches[0].clientY
+ startValueRef.current = value
+ e.preventDefault()
+ }
+
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!isDragging) return
@@ -79,26 +92,48 @@ export function Knob({
onChange(steppedValue)
}, [isDragging, max, min, step, onChange])
+ const handleTouchMove = useCallback((e: TouchEvent) => {
+ if (!isDragging) return
+
+ const deltaY = startYRef.current - e.touches[0].clientY
+ const range = max - min
+ const sensitivity = range / 200
+ const newValue = Math.max(min, Math.min(max, startValueRef.current + deltaY * sensitivity))
+
+ const steppedValue = Math.round(newValue / step) * step
+ onChange(steppedValue)
+ e.preventDefault()
+ }, [isDragging, max, min, step, onChange])
+
const handleMouseUp = useCallback(() => {
setIsDragging(false)
}, [])
+ const handleTouchEnd = useCallback(() => {
+ setIsDragging(false)
+ }, [])
+
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
+ window.addEventListener('touchmove', handleTouchMove, { passive: false })
+ window.addEventListener('touchend', handleTouchEnd)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
+ window.removeEventListener('touchmove', handleTouchMove)
+ window.removeEventListener('touchend', handleTouchEnd)
}
}
- }, [isDragging, handleMouseMove, handleMouseUp])
+ }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd])
return (