124 lines
4.9 KiB
TypeScript
124 lines
4.9 KiB
TypeScript
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<TwoOpFMParams> {
|
|
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));
|
|
}
|
|
}
|