Files
bruitiste/public/worklets/fm-processor.js
2025-10-06 16:36:59 +02:00

482 lines
16 KiB
JavaScript

class FMProcessor extends AudioWorkletProcessor {
constructor() {
super()
this.phase1 = 0
this.phase2 = 0
this.phase3 = 0
this.phase4 = 0
this.baseFreq = 220
this.algorithm = 0
this.opLevel1 = 128
this.opLevel2 = 128
this.opLevel3 = 128
this.opLevel4 = 128
this.feedback = 0
this.playbackRate = 1.0
this.loopLength = 0
this.sampleCount = 0
this.feedbackSample = 0
this.frequencyRatios = [1.0, 1.0, 1.0, 1.0]
this.lfoPhase1 = 0
this.lfoPhase2 = 0
this.lfoPhase3 = 0
this.lfoPhase4 = 0
this.lfoRate1 = 0.37
this.lfoRate2 = 0.53
this.lfoRate3 = 0.71
this.lfoRate4 = 0.43
this.lfoDepth = 0.3
this.pitchLFOPhase = 0
this.pitchLFOWaveform = 0
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) {
case 'algorithm':
this.algorithm = value.id
this.frequencyRatios = value.ratios
if (value.lfoRates) {
this.lfoRate1 = value.lfoRates[0]
this.lfoRate2 = value.lfoRates[1]
this.lfoRate3 = value.lfoRates[2]
this.lfoRate4 = value.lfoRates[3]
}
if (value.pitchLFO) {
this.pitchLFOWaveform = value.pitchLFO.waveform
this.pitchLFODepth = value.pitchLFO.depth
this.pitchLFOBaseRate = value.pitchLFO.baseRate
}
break
case 'operatorLevels':
this.opLevel1 = value.a
this.opLevel2 = value.b
this.opLevel3 = value.c
this.opLevel4 = value.d
break
case 'baseFreq':
this.baseFreq = value
break
case 'feedback':
this.feedback = value / 100.0
break
case 'reset':
this.phase1 = 0
this.phase2 = 0
this.phase3 = 0
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
break
case 'playbackRate':
this.playbackRate = value
break
}
}
}
generatePitchLFO(phase, waveform) {
const TWO_PI = Math.PI * 2
const normalizedPhase = phase / TWO_PI
switch (waveform) {
case 0: // sine
return Math.sin(phase)
case 1: // triangle
return 2 * Math.abs(2 * (normalizedPhase % 1 - 0.5)) - 1
case 2: // square
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
}
}
synthesize(algorithm) {
const TWO_PI = Math.PI * 2
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)
const pitchLFOValue = this.generatePitchLFO(this.pitchLFOPhase, this.pitchLFOWaveform)
const pitchMod = 1 + pitchLFOValue * this.pitchLFODepth
const modulatedBaseFreq = this.baseFreq * pitchMod
this.pitchLFOPhase += (pitchLFORate * TWO_PI) / sampleRate
if (this.pitchLFOPhase > TWO_PI) this.pitchLFOPhase -= TWO_PI
const freq1 = (modulatedBaseFreq * this.frequencyRatios[0] * TWO_PI) / sampleRate
const freq2 = (modulatedBaseFreq * this.frequencyRatios[1] * TWO_PI) / sampleRate
const freq3 = (modulatedBaseFreq * this.frequencyRatios[2] * TWO_PI) / sampleRate
const freq4 = (modulatedBaseFreq * this.frequencyRatios[3] * TWO_PI) / sampleRate
const lfo1 = Math.sin(this.lfoPhase1)
const lfo2 = Math.sin(this.lfoPhase2)
const lfo3 = Math.sin(this.lfoPhase3)
const lfo4 = Math.sin(this.lfoPhase4)
const level1 = (this.opLevel1 / 255.0) * (1 + this.lfoDepth * lfo1)
const level2 = (this.opLevel2 / 255.0) * (1 + this.lfoDepth * lfo2)
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
this.lfoPhase4 += (this.lfoRate4 * TWO_PI) / sampleRate
if (this.lfoPhase1 > TWO_PI) this.lfoPhase1 -= TWO_PI
if (this.lfoPhase2 > TWO_PI) this.lfoPhase2 -= TWO_PI
if (this.lfoPhase3 > TWO_PI) this.lfoPhase3 -= TWO_PI
if (this.lfoPhase4 > TWO_PI) this.lfoPhase4 -= TWO_PI
let output = 0
switch (algorithm) {
case 0: {
const op1 = Math.sin(this.phase1) * level1
const op2 = Math.sin(this.phase2) * level2
const op3 = Math.sin(this.phase3) * level3
const op4 = Math.sin(this.phase4) * level4
output = (op1 + op2 + op3 + op4) * 0.25
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 1: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
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
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 2: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const op3 = Math.sin(this.phase3) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op2 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 3: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
const op4 = Math.sin(this.phase4) * level4
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 4: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1 + mod2) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 5: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 6: {
const op1 = Math.sin(this.phase1) * level1
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
output = (op2 + op3 + op4) * 0.333
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 7: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepthLight
const op3 = Math.sin(this.phase3 + mod1) * level3
const mod3 = op3 * modDepthLight
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 8: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
const op2 = Math.sin(this.phase2) * level2
const mod2 = op2 * modDepth
const op4 = Math.sin(this.phase4 + mod2) * level4
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 9: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op4 = Math.sin(this.phase4 + mod1) * level4
const op2 = Math.sin(this.phase2) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 10: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const op3 = Math.sin(this.phase3) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op2 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 11: {
const op1 = Math.sin(this.phase1) * level1
const op2 = Math.sin(this.phase2) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod2) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
output = (op1 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 12: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepth
const op4 = Math.sin(this.phase4 + mod2) * level4
const op3 = Math.sin(this.phase3) * level3
output = (op3 + op4) * 0.5
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 13: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2 + mod1) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3 + mod1) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 14: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op3 = Math.sin(this.phase3) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod3) * level4
const mod4 = op4 * modDepthLight
const op2 = Math.sin(this.phase2 + mod1 + mod4) * level2
output = op2
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
case 15: {
const op1 = Math.sin(this.phase1) * level1
const mod1 = op1 * modDepth
const op2 = Math.sin(this.phase2) * level2
const mod2 = op2 * modDepth
const op3 = Math.sin(this.phase3) * level3
const mod3 = op3 * modDepth
const op4 = Math.sin(this.phase4 + mod1 + mod2 + mod3) * level4
output = op4
this.phase1 += freq1 * this.playbackRate
this.phase2 += freq2 * this.playbackRate
this.phase3 += freq3 * this.playbackRate
this.phase4 += freq4 * this.playbackRate
break
}
default:
output = 0
}
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
}
process(inputs, outputs) {
const output = outputs[0]
if (output.length > 0) {
const outputChannel = output[0]
for (let i = 0; i < outputChannel.length; i++) {
outputChannel[i] = this.synthesize(this.algorithm)
this.sampleCount++
if (this.loopLength > 0 && this.sampleCount >= this.loopLength) {
this.phase1 = 0
this.phase2 = 0
this.phase3 = 0
this.phase4 = 0
this.sampleCount = 0
this.feedbackSample = 0
}
}
}
return true
}
}
registerProcessor('fm-processor', FMProcessor)