import type { SynthEngine } from './SynthEngine'; export interface TwoOpFMParams { carrierFreq: number; modRatio: number; modIndex: number; attack: number; // 0-1, ratio of total duration decay: number; // 0-1, ratio of total duration sustain: number; // 0-1, amplitude level release: number; // 0-1, ratio of total duration vibratoRate: number; // Hz vibratoDepth: number; // 0-1, pitch modulation depth stereoWidth: number; // 0-1, amount of stereo separation } export class TwoOpFM implements SynthEngine { name = '2-OP FM'; generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); const rightBuffer = new Float32Array(numSamples); const TAU = Math.PI * 2; const detune = 1 + (params.stereoWidth * 0.002); const leftFreq = params.carrierFreq / detune; const rightFreq = params.carrierFreq * detune; const modulatorFreq = params.carrierFreq * params.modRatio; let carrierPhaseL = 0; let carrierPhaseR = Math.PI * params.stereoWidth * 0.1; let modulatorPhaseL = 0; let modulatorPhaseR = 0; let vibratoPhaseL = 0; let vibratoPhaseR = Math.PI * params.stereoWidth * 0.3; for (let i = 0; i < numSamples; i++) { const t = i / sampleRate; const envelope = this.calculateEnvelope(t, duration, params); const vibratoL = Math.sin(vibratoPhaseL) * params.vibratoDepth; const vibratoR = Math.sin(vibratoPhaseR) * params.vibratoDepth; const carrierFreqL = leftFreq * (1 + vibratoL); const carrierFreqR = rightFreq * (1 + vibratoR); const modulatorL = Math.sin(modulatorPhaseL); const modulatorR = Math.sin(modulatorPhaseR); const carrierL = Math.sin(carrierPhaseL + params.modIndex * modulatorL); const carrierR = Math.sin(carrierPhaseR + params.modIndex * modulatorR); leftBuffer[i] = carrierL * envelope; rightBuffer[i] = carrierR * envelope; carrierPhaseL += (TAU * carrierFreqL) / sampleRate; carrierPhaseR += (TAU * carrierFreqR) / sampleRate; modulatorPhaseL += (TAU * modulatorFreq) / sampleRate; modulatorPhaseR += (TAU * modulatorFreq) / sampleRate; vibratoPhaseL += (TAU * params.vibratoRate) / sampleRate; vibratoPhaseR += (TAU * params.vibratoRate) / sampleRate; } return [leftBuffer, rightBuffer]; } private calculateEnvelope(t: number, duration: number, params: TwoOpFMParams): number { const attackTime = params.attack * duration; const decayTime = params.decay * duration; const releaseTime = params.release * duration; const sustainStart = attackTime + decayTime; const releaseStart = duration - releaseTime; if (t < attackTime) { return t / attackTime; } else if (t < sustainStart) { const decayProgress = (t - attackTime) / decayTime; return 1 - decayProgress * (1 - params.sustain); } else if (t < releaseStart) { return params.sustain; } else { const releaseProgress = (t - releaseStart) / releaseTime; return params.sustain * (1 - releaseProgress); } } randomParams(): TwoOpFMParams { return { carrierFreq: this.randomRange(100, 800), modRatio: this.randomRange(0.5, 8), modIndex: this.randomRange(0, 10), attack: this.randomRange(0.01, 0.15), decay: this.randomRange(0.05, 0.2), sustain: this.randomRange(0.3, 0.9), release: this.randomRange(0.1, 0.4), vibratoRate: this.randomRange(3, 8), vibratoDepth: this.randomRange(0, 0.03), stereoWidth: this.randomRange(0.3, 0.8), }; } mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams { return { carrierFreq: this.mutateValue(params.carrierFreq, mutationAmount, 50, 1000), modRatio: this.mutateValue(params.modRatio, mutationAmount, 0.25, 10), modIndex: this.mutateValue(params.modIndex, mutationAmount, 0, 15), attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.3), decay: this.mutateValue(params.decay, mutationAmount, 0.01, 0.4), sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 1.0), release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6), vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 12), vibratoDepth: this.mutateValue(params.vibratoDepth, mutationAmount, 0, 0.05), stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0.0, 1.0), }; } private randomRange(min: number, max: number): number { return min + Math.random() * (max - min); } private mutateValue(value: number, amount: number, min: number, max: number): number { const variation = value * amount * (Math.random() * 2 - 1); return Math.max(min, Math.min(max, value + variation)); } }