482 lines
16 KiB
JavaScript
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)
|