almost stable
This commit is contained in:
527
src/lib/audio/engines/FourOpFM.ts
Normal file
527
src/lib/audio/engines/FourOpFM.ts
Normal file
@ -0,0 +1,527 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
|
||||
enum EnvCurve {
|
||||
Linear,
|
||||
Exponential,
|
||||
Logarithmic,
|
||||
SCurve,
|
||||
}
|
||||
|
||||
enum LFOWaveform {
|
||||
Sine,
|
||||
Triangle,
|
||||
Square,
|
||||
Saw,
|
||||
SampleHold,
|
||||
RandomWalk,
|
||||
}
|
||||
|
||||
enum Algorithm {
|
||||
Cascade, // 1→2→3→4 (deep modulation)
|
||||
DualStack, // (1→2) + (3→4) (two independent stacks)
|
||||
Parallel, // 1+2+3+4 (additive)
|
||||
TripleMod, // 1→2→3 + 4 (complex modulation + pure carrier)
|
||||
Bell, // (1→3, 2→3) + 4 (two modulators converge)
|
||||
Feedback, // 1→1→2→3→4 (self-modulation)
|
||||
}
|
||||
|
||||
interface OperatorParams {
|
||||
ratio: number;
|
||||
level: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
attackCurve: EnvCurve;
|
||||
decayCurve: EnvCurve;
|
||||
releaseCurve: EnvCurve;
|
||||
}
|
||||
|
||||
interface LFOParams {
|
||||
rate: number;
|
||||
depth: number;
|
||||
waveform: LFOWaveform;
|
||||
target: 'pitch' | 'amplitude' | 'modIndex';
|
||||
}
|
||||
|
||||
export interface FourOpFMParams {
|
||||
baseFreq: number;
|
||||
algorithm: Algorithm;
|
||||
operators: [OperatorParams, OperatorParams, OperatorParams, OperatorParams];
|
||||
lfo: LFOParams;
|
||||
feedback: number;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class FourOpFM implements SynthEngine<FourOpFMParams> {
|
||||
private lfoSampleHoldValue = 0;
|
||||
private lfoSampleHoldPhase = 0;
|
||||
private lfoRandomWalkCurrent = 0;
|
||||
private lfoRandomWalkTarget = 0;
|
||||
// DC blocking filters for each channel
|
||||
private dcBlockerL = 0;
|
||||
private dcBlockerR = 0;
|
||||
private readonly dcBlockerCutoff = 0.995;
|
||||
|
||||
getName(): string {
|
||||
return '4-OP FM';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Four-operator FM synthesis with multiple algorithms, envelope curves, and LFO waveforms';
|
||||
}
|
||||
|
||||
generate(params: FourOpFMParams, 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;
|
||||
|
||||
// More subtle stereo detuning
|
||||
const detune = 1 + (params.stereoWidth * 0.001);
|
||||
const leftFreq = params.baseFreq / detune;
|
||||
const rightFreq = params.baseFreq * detune;
|
||||
|
||||
// Initialize operator phases for stereo with more musical offsets
|
||||
const opPhasesL = [0, Math.PI * params.stereoWidth * 0.05, 0, 0];
|
||||
const opPhasesR = [0, Math.PI * params.stereoWidth * 0.08, 0, 0];
|
||||
|
||||
let lfoPhaseL = 0;
|
||||
let lfoPhaseR = Math.PI * params.stereoWidth * 0.25;
|
||||
|
||||
// Reset non-periodic LFO state
|
||||
this.lfoSampleHoldValue = Math.random() * 2 - 1;
|
||||
this.lfoSampleHoldPhase = 0;
|
||||
this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
|
||||
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
|
||||
|
||||
let feedbackSampleL = 0;
|
||||
let feedbackSampleR = 0;
|
||||
|
||||
// Get algorithm-specific gain compensation
|
||||
const gainCompensation = this.getAlgorithmGainCompensation(params.algorithm);
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
// Calculate envelopes for each operator
|
||||
const env1 = this.calculateEnvelope(t, duration, params.operators[0]);
|
||||
const env2 = this.calculateEnvelope(t, duration, params.operators[1]);
|
||||
const env3 = this.calculateEnvelope(t, duration, params.operators[2]);
|
||||
const env4 = this.calculateEnvelope(t, duration, params.operators[3]);
|
||||
|
||||
// Generate LFO modulation
|
||||
const lfoL = this.generateLFO(lfoPhaseL, params.lfo.waveform, params.lfo.rate, sampleRate);
|
||||
const lfoR = this.generateLFO(lfoPhaseR, params.lfo.waveform, params.lfo.rate, sampleRate);
|
||||
const lfoModL = lfoL * params.lfo.depth;
|
||||
const lfoModR = lfoR * params.lfo.depth;
|
||||
|
||||
// Apply LFO to target parameter
|
||||
let pitchModL = 0, pitchModR = 0;
|
||||
let ampModL = 1, ampModR = 1;
|
||||
let modIndexMod = 0;
|
||||
|
||||
if (params.lfo.target === 'pitch') {
|
||||
// More musical pitch modulation range
|
||||
pitchModL = lfoModL * 0.02;
|
||||
pitchModR = lfoModR * 0.02;
|
||||
} else if (params.lfo.target === 'amplitude') {
|
||||
// Tremolo effect
|
||||
ampModL = 1 + lfoModL * 0.5;
|
||||
ampModR = 1 + lfoModR * 0.5;
|
||||
} else {
|
||||
// Modulation index modulation
|
||||
modIndexMod = lfoModL;
|
||||
}
|
||||
|
||||
// Process algorithm - generate left and right samples
|
||||
const [sampleL, sampleR] = this.processAlgorithm(
|
||||
params.algorithm,
|
||||
params.operators,
|
||||
opPhasesL,
|
||||
opPhasesR,
|
||||
[env1, env2, env3, env4],
|
||||
feedbackSampleL,
|
||||
feedbackSampleR,
|
||||
params.feedback,
|
||||
modIndexMod
|
||||
);
|
||||
|
||||
// Apply gain compensation and amplitude modulation
|
||||
let outL = sampleL * gainCompensation * ampModL;
|
||||
let outR = sampleR * gainCompensation * ampModR;
|
||||
|
||||
// Soft clipping for musical saturation
|
||||
outL = this.softClip(outL);
|
||||
outR = this.softClip(outR);
|
||||
|
||||
// DC blocking filter
|
||||
const dcFilteredL = outL - this.dcBlockerL;
|
||||
this.dcBlockerL += (1 - this.dcBlockerCutoff) * dcFilteredL;
|
||||
|
||||
const dcFilteredR = outR - this.dcBlockerR;
|
||||
this.dcBlockerR += (1 - this.dcBlockerCutoff) * dcFilteredR;
|
||||
|
||||
leftBuffer[i] = dcFilteredL;
|
||||
rightBuffer[i] = dcFilteredR;
|
||||
|
||||
// Store feedback samples (after soft clipping)
|
||||
feedbackSampleL = outL;
|
||||
feedbackSampleR = outR;
|
||||
|
||||
// Advance operator phases
|
||||
for (let op = 0; op < 4; op++) {
|
||||
const opFreqL = leftFreq * params.operators[op].ratio * (1 + pitchModL);
|
||||
const opFreqR = rightFreq * params.operators[op].ratio * (1 + pitchModR);
|
||||
opPhasesL[op] += (TAU * opFreqL) / sampleRate;
|
||||
opPhasesR[op] += (TAU * opFreqR) / sampleRate;
|
||||
// Wrap phases to prevent numerical issues
|
||||
if (opPhasesL[op] > TAU * 1000) opPhasesL[op] -= TAU * 1000;
|
||||
if (opPhasesR[op] > TAU * 1000) opPhasesR[op] -= TAU * 1000;
|
||||
}
|
||||
|
||||
// Advance LFO phase
|
||||
lfoPhaseL += (TAU * params.lfo.rate) / sampleRate;
|
||||
lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
|
||||
}
|
||||
|
||||
return [leftBuffer, rightBuffer];
|
||||
}
|
||||
|
||||
private processAlgorithm(
|
||||
algorithm: Algorithm,
|
||||
operators: [OperatorParams, OperatorParams, OperatorParams, OperatorParams],
|
||||
phasesL: number[],
|
||||
phasesR: number[],
|
||||
envelopes: number[],
|
||||
feedbackL: number,
|
||||
feedbackR: number,
|
||||
feedbackAmount: number,
|
||||
modIndexMod: number
|
||||
): [number, number] {
|
||||
// More musical modulation scaling
|
||||
const baseModIndex = 2.5;
|
||||
const modScale = baseModIndex * (1 + modIndexMod * 2);
|
||||
|
||||
switch (algorithm) {
|
||||
case Algorithm.Cascade: {
|
||||
// 1→2→3→4 - Deep FM chain
|
||||
const fbAmountScaled = feedbackAmount * 0.8;
|
||||
const mod1L = Math.sin(phasesL[0] + fbAmountScaled * feedbackL) * envelopes[0] * operators[0].level;
|
||||
const mod1R = Math.sin(phasesR[0] + fbAmountScaled * feedbackR) * envelopes[0] * operators[0].level;
|
||||
const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
|
||||
const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
|
||||
const mod3L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
|
||||
const mod3R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
|
||||
const outL = Math.sin(phasesL[3] + modScale * 0.5 * mod3L) * envelopes[3] * operators[3].level;
|
||||
const outR = Math.sin(phasesR[3] + modScale * 0.5 * mod3R) * envelopes[3] * operators[3].level;
|
||||
return [outL, outR];
|
||||
}
|
||||
|
||||
case Algorithm.DualStack: {
|
||||
// (1→2) + (3→4) - Two parallel FM pairs
|
||||
const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
|
||||
const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
|
||||
const car1L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
|
||||
const car1R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
|
||||
|
||||
const mod2L = Math.sin(phasesL[2]) * envelopes[2] * operators[2].level;
|
||||
const mod2R = Math.sin(phasesR[2]) * envelopes[2] * operators[2].level;
|
||||
const car2L = Math.sin(phasesL[3] + modScale * mod2L) * envelopes[3] * operators[3].level;
|
||||
const car2R = Math.sin(phasesR[3] + modScale * mod2R) * envelopes[3] * operators[3].level;
|
||||
|
||||
// Mix with proper gain staging
|
||||
return [(car1L + car2L) * 0.5, (car1R + car2R) * 0.5];
|
||||
}
|
||||
|
||||
case Algorithm.Parallel: {
|
||||
// 1+2+3+4 - Additive synthesis
|
||||
let sumL = 0, sumR = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
sumL += Math.sin(phasesL[i]) * envelopes[i] * operators[i].level;
|
||||
sumR += Math.sin(phasesR[i]) * envelopes[i] * operators[i].level;
|
||||
}
|
||||
// Scale by 1/sqrt(4) for constant power mixing
|
||||
return [sumL * 0.5, sumR * 0.5];
|
||||
}
|
||||
|
||||
case Algorithm.TripleMod: {
|
||||
// 1→2→3 + 4 - Complex mod chain plus carrier
|
||||
const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
|
||||
const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
|
||||
const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
|
||||
const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
|
||||
const car1L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
|
||||
const car1R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
|
||||
const car2L = Math.sin(phasesL[3]) * envelopes[3] * operators[3].level;
|
||||
const car2R = Math.sin(phasesR[3]) * envelopes[3] * operators[3].level;
|
||||
return [(car1L * 0.7 + car2L * 0.3), (car1R * 0.7 + car2R * 0.3)];
|
||||
}
|
||||
|
||||
case Algorithm.Bell: {
|
||||
// (1→3, 2→3) + 4 - Bell-like tones
|
||||
const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
|
||||
const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
|
||||
const mod2L = Math.sin(phasesL[1]) * envelopes[1] * operators[1].level;
|
||||
const mod2R = Math.sin(phasesR[1]) * envelopes[1] * operators[1].level;
|
||||
const car1L = Math.sin(phasesL[2] + modScale * 0.6 * (mod1L + mod2L)) * envelopes[2] * operators[2].level;
|
||||
const car1R = Math.sin(phasesR[2] + modScale * 0.6 * (mod1R + mod2R)) * envelopes[2] * operators[2].level;
|
||||
const car2L = Math.sin(phasesL[3]) * envelopes[3] * operators[3].level;
|
||||
const car2R = Math.sin(phasesR[3]) * envelopes[3] * operators[3].level;
|
||||
return [(car1L + car2L) * 0.5, (car1R + car2R) * 0.5];
|
||||
}
|
||||
|
||||
case Algorithm.Feedback: {
|
||||
// 1→1→2→3→4 - Self-modulating cascade
|
||||
const fbAmountScaled = Math.min(feedbackAmount * 0.7, 1.5);
|
||||
const mod1L = Math.sin(phasesL[0] + fbAmountScaled * this.softClip(feedbackL * 2)) * envelopes[0] * operators[0].level;
|
||||
const mod1R = Math.sin(phasesR[0] + fbAmountScaled * this.softClip(feedbackR * 2)) * envelopes[0] * operators[0].level;
|
||||
const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
|
||||
const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
|
||||
const mod3L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
|
||||
const mod3R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
|
||||
const outL = Math.sin(phasesL[3] + modScale * 0.5 * mod3L) * envelopes[3] * operators[3].level;
|
||||
const outR = Math.sin(phasesR[3] + modScale * 0.5 * mod3R) * envelopes[3] * operators[3].level;
|
||||
return [outL, outR];
|
||||
}
|
||||
|
||||
default:
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
private getAlgorithmGainCompensation(algorithm: Algorithm): number {
|
||||
// Compensate for different algorithm output levels
|
||||
switch (algorithm) {
|
||||
case Algorithm.Cascade:
|
||||
case Algorithm.Feedback:
|
||||
return 0.7;
|
||||
case Algorithm.DualStack:
|
||||
case Algorithm.TripleMod:
|
||||
case Algorithm.Bell:
|
||||
return 0.8;
|
||||
case Algorithm.Parallel:
|
||||
return 0.6;
|
||||
default:
|
||||
return 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
private softClip(x: number): number {
|
||||
// Musical soft clipping using tanh approximation
|
||||
const absX = Math.abs(x);
|
||||
if (absX < 0.7) return x;
|
||||
if (absX > 3) return Math.sign(x) * 0.98;
|
||||
// Fast tanh approximation for soft saturation
|
||||
const x2 = x * x;
|
||||
return x * (27 + x2) / (27 + 9 * x2);
|
||||
}
|
||||
|
||||
private calculateEnvelope(t: number, duration: number, op: OperatorParams): number {
|
||||
const attackTime = op.attack * duration;
|
||||
const decayTime = op.decay * duration;
|
||||
const releaseTime = op.release * duration;
|
||||
const sustainStart = attackTime + decayTime;
|
||||
const releaseStart = duration - releaseTime;
|
||||
|
||||
if (t < attackTime) {
|
||||
const progress = t / attackTime;
|
||||
return this.applyCurve(progress, op.attackCurve);
|
||||
} else if (t < sustainStart) {
|
||||
const progress = (t - attackTime) / decayTime;
|
||||
const curvedProgress = this.applyCurve(progress, op.decayCurve);
|
||||
return 1 - curvedProgress * (1 - op.sustain);
|
||||
} else if (t < releaseStart) {
|
||||
return op.sustain;
|
||||
} else {
|
||||
const progress = (t - releaseStart) / releaseTime;
|
||||
const curvedProgress = this.applyCurve(progress, op.releaseCurve);
|
||||
return op.sustain * (1 - curvedProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private applyCurve(progress: number, curve: EnvCurve): number {
|
||||
switch (curve) {
|
||||
case EnvCurve.Linear:
|
||||
return progress;
|
||||
case EnvCurve.Exponential:
|
||||
return Math.pow(progress, 3);
|
||||
case EnvCurve.Logarithmic:
|
||||
return Math.pow(progress, 0.33);
|
||||
case EnvCurve.SCurve:
|
||||
return (Math.sin((progress - 0.5) * Math.PI) + 1) / 2;
|
||||
default:
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
private generateLFO(phase: number, waveform: LFOWaveform, rate: number, sampleRate: number): number {
|
||||
const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
|
||||
|
||||
switch (waveform) {
|
||||
case LFOWaveform.Sine:
|
||||
return Math.sin(phase);
|
||||
|
||||
case LFOWaveform.Triangle:
|
||||
return normalizedPhase < 0.5
|
||||
? normalizedPhase * 4 - 1
|
||||
: 3 - normalizedPhase * 4;
|
||||
|
||||
case LFOWaveform.Square:
|
||||
return normalizedPhase < 0.5 ? 1 : -1;
|
||||
|
||||
case LFOWaveform.Saw:
|
||||
return normalizedPhase * 2 - 1;
|
||||
|
||||
case LFOWaveform.SampleHold: {
|
||||
const cyclesSinceLastHold = phase - this.lfoSampleHoldPhase;
|
||||
if (cyclesSinceLastHold >= Math.PI * 2) {
|
||||
this.lfoSampleHoldValue = Math.random() * 2 - 1;
|
||||
this.lfoSampleHoldPhase = phase;
|
||||
}
|
||||
return this.lfoSampleHoldValue;
|
||||
}
|
||||
|
||||
case LFOWaveform.RandomWalk: {
|
||||
const interpolationSpeed = rate / sampleRate * 20;
|
||||
const diff = this.lfoRandomWalkTarget - this.lfoRandomWalkCurrent;
|
||||
this.lfoRandomWalkCurrent += diff * interpolationSpeed;
|
||||
|
||||
if (Math.abs(diff) < 0.01) {
|
||||
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
return this.lfoRandomWalkCurrent;
|
||||
}
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): FourOpFMParams {
|
||||
const algorithm = this.randomInt(0, 5) as Algorithm;
|
||||
|
||||
// More musical frequency ratios including inharmonic ones
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
algorithm,
|
||||
operators: [
|
||||
this.randomOperator(true, algorithm),
|
||||
this.randomOperator(false, algorithm),
|
||||
this.randomOperator(false, algorithm),
|
||||
this.randomOperator(false, algorithm),
|
||||
],
|
||||
lfo: {
|
||||
rate: this.randomRange(0.1, 12),
|
||||
depth: this.randomRange(0, 0.5),
|
||||
waveform: this.randomInt(0, 5) as LFOWaveform,
|
||||
target: this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const),
|
||||
},
|
||||
feedback: this.randomRange(0, 1.5),
|
||||
stereoWidth: this.randomRange(0.2, 0.8),
|
||||
};
|
||||
}
|
||||
|
||||
private randomOperator(isCarrier: boolean, algorithm: Algorithm): OperatorParams {
|
||||
// More musical ratio choices including inharmonic ones
|
||||
const harmonicRatios = [0.5, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const inharmonicRatios = [1.414, 1.732, 2.236, 3.14, 4.19, 5.13, 6.28];
|
||||
const bellRatios = [0.56, 0.92, 1.19, 1.71, 2, 2.74, 3, 3.76, 4.07];
|
||||
|
||||
let ratio: number;
|
||||
if (algorithm === Algorithm.Bell && Math.random() < 0.7) {
|
||||
ratio = this.randomChoice(bellRatios);
|
||||
} else if (Math.random() < 0.3) {
|
||||
ratio = this.randomChoice(inharmonicRatios);
|
||||
} else {
|
||||
ratio = this.randomChoice(harmonicRatios);
|
||||
}
|
||||
|
||||
// Add slight detuning for richness
|
||||
ratio *= this.randomRange(0.998, 1.002);
|
||||
|
||||
// Carriers typically have lower levels in FM
|
||||
const levelRange = isCarrier ? [0.3, 0.7] : [0.2, 0.8];
|
||||
|
||||
return {
|
||||
ratio,
|
||||
level: this.randomRange(levelRange[0], levelRange[1]),
|
||||
attack: this.randomRange(0.001, 0.15),
|
||||
decay: this.randomRange(0.02, 0.25),
|
||||
sustain: this.randomRange(0.1, 0.8),
|
||||
release: this.randomRange(0.05, 0.4),
|
||||
attackCurve: this.randomInt(0, 3) as EnvCurve,
|
||||
decayCurve: this.randomInt(0, 3) as EnvCurve,
|
||||
releaseCurve: this.randomInt(0, 3) as EnvCurve,
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15): FourOpFMParams {
|
||||
return {
|
||||
baseFreq: params.baseFreq,
|
||||
algorithm: Math.random() < 0.08 ? this.randomInt(0, 5) as Algorithm : params.algorithm,
|
||||
operators: params.operators.map((op, i) =>
|
||||
this.mutateOperator(op, mutationAmount, i === 3, params.algorithm)
|
||||
) as [OperatorParams, OperatorParams, OperatorParams, OperatorParams],
|
||||
lfo: {
|
||||
rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 20),
|
||||
depth: this.mutateValue(params.lfo.depth, mutationAmount, 0, 0.7),
|
||||
waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfo.waveform,
|
||||
target: Math.random() < 0.08 ? this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const) : params.lfo.target,
|
||||
},
|
||||
feedback: this.mutateValue(params.feedback, mutationAmount, 0, 2),
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
private mutateOperator(op: OperatorParams, amount: number, isCarrier: boolean, algorithm: Algorithm): OperatorParams {
|
||||
const harmonicRatios = [0.5, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const inharmonicRatios = [1.414, 1.732, 2.236, 3.14, 4.19, 5.13, 6.28];
|
||||
const bellRatios = [0.56, 0.92, 1.19, 1.71, 2, 2.74, 3, 3.76, 4.07];
|
||||
|
||||
let newRatio = op.ratio;
|
||||
if (Math.random() < 0.12) {
|
||||
if (algorithm === Algorithm.Bell && Math.random() < 0.7) {
|
||||
newRatio = this.randomChoice(bellRatios);
|
||||
} else if (Math.random() < 0.3) {
|
||||
newRatio = this.randomChoice(inharmonicRatios);
|
||||
} else {
|
||||
newRatio = this.randomChoice(harmonicRatios);
|
||||
}
|
||||
newRatio *= this.randomRange(0.998, 1.002);
|
||||
}
|
||||
|
||||
return {
|
||||
ratio: newRatio,
|
||||
level: this.mutateValue(op.level, amount, 0.1, isCarrier ? 0.8 : 1.0),
|
||||
attack: this.mutateValue(op.attack, amount, 0.001, 0.25),
|
||||
decay: this.mutateValue(op.decay, amount, 0.01, 0.4),
|
||||
sustain: this.mutateValue(op.sustain, amount, 0.05, 0.95),
|
||||
release: this.mutateValue(op.release, amount, 0.02, 0.6),
|
||||
attackCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.attackCurve,
|
||||
decayCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.decayCurve,
|
||||
releaseCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.releaseCurve,
|
||||
};
|
||||
}
|
||||
|
||||
private randomRange(min: number, max: number): number {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
private randomInt(min: number, max: number): number {
|
||||
return Math.floor(this.randomRange(min, max + 1));
|
||||
}
|
||||
|
||||
private randomChoice<T>(choices: readonly T[]): T {
|
||||
return choices[Math.floor(Math.random() * choices.length)];
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user