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)