983 lines
36 KiB
TypeScript
983 lines
36 KiB
TypeScript
import type { SynthEngine, PitchLock } from './base/SynthEngine';
|
|
|
|
interface BenjolinParams {
|
|
// Core oscillators
|
|
osc1Freq: number;
|
|
osc2Freq: number;
|
|
osc1Wave: number; // 0=tri, 1=saw, morphable
|
|
osc2Wave: number; // 0=tri, 1=pulse
|
|
|
|
// Cross modulation matrix
|
|
crossMod1to2: number;
|
|
crossMod2to1: number;
|
|
crossMod1toFilter: number;
|
|
crossMod2toRungler: number;
|
|
|
|
// Rungler parameters
|
|
runglerToOsc1: number;
|
|
runglerToOsc2: number;
|
|
runglerToFilter: number;
|
|
runglerBits: number; // 8, 12, or 16 bit modes
|
|
runglerFeedback: number;
|
|
runglerChaos: number; // Amount of XOR chaos
|
|
|
|
// Filter parameters
|
|
filterCutoff: number;
|
|
filterResonance: number;
|
|
filterMode: number; // 0=LP, 0.5=BP, 1=HP morphable
|
|
filterDrive: number; // Input overdrive
|
|
filterFeedback: number; // Self-oscillation amount
|
|
|
|
// Evolution parameters (new!)
|
|
evolutionRate: number; // How fast parameters drift
|
|
evolutionDepth: number; // How much they drift
|
|
chaosAttractorRate: number; // Secondary chaos source
|
|
chaosAttractorDepth: number;
|
|
|
|
// Modulation LFOs (new!)
|
|
lfo1Rate: number; // Ratio of duration
|
|
lfo1Depth: number;
|
|
lfo1Target: number; // 0=freq, 0.5=filter, 1=chaos
|
|
lfo2Rate: number;
|
|
lfo2Depth: number;
|
|
lfo2Wave: number; // 0=sine, 0.5=tri, 1=S&H
|
|
|
|
// Output shaping
|
|
wavefoldAmount: number;
|
|
distortionType: number; // 0=soft, 0.5=fold, 1=digital
|
|
stereoWidth: number;
|
|
outputGain: number;
|
|
|
|
// Envelope
|
|
envelopeAttack: number;
|
|
envelopeDecay: number;
|
|
envelopeSustain: number;
|
|
envelopeRelease: number;
|
|
envelopeToFilter: number;
|
|
envelopeToFold: number;
|
|
}
|
|
|
|
// Preset configuration count
|
|
const PRESET_COUNT = 18;
|
|
|
|
export class Benjolin implements SynthEngine<BenjolinParams> {
|
|
getName(): string {
|
|
return 'Bubolin';
|
|
}
|
|
|
|
getDescription(): string {
|
|
return 'Some kind of rungler/benjolin inspired generator';
|
|
}
|
|
|
|
getType() {
|
|
return 'generative' as const;
|
|
}
|
|
|
|
getCategory() {
|
|
return 'Experimental' as const;
|
|
}
|
|
|
|
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
|
const numSamples = Math.floor(duration * sampleRate);
|
|
const left = new Float32Array(numSamples);
|
|
const right = new Float32Array(numSamples);
|
|
|
|
// Extract parameters with intelligent defaults
|
|
const osc1Freq = params.osc1Freq ?? 220;
|
|
const osc2Freq = params.osc2Freq ?? 330;
|
|
const osc1Wave = params.osc1Wave ?? 0;
|
|
const osc2Wave = params.osc2Wave ?? 0;
|
|
|
|
const crossMod1to2 = params.crossMod1to2 ?? 0.3;
|
|
const crossMod2to1 = params.crossMod2to1 ?? 0.3;
|
|
const crossMod1toFilter = params.crossMod1toFilter ?? 0.2;
|
|
const crossMod2toRungler = params.crossMod2toRungler ?? 0.2;
|
|
|
|
const runglerToOsc1 = params.runglerToOsc1 ?? 0.2;
|
|
const runglerToOsc2 = params.runglerToOsc2 ?? 0.2;
|
|
const runglerToFilter = params.runglerToFilter ?? 0.3;
|
|
const runglerBits = Math.floor(params.runglerBits ?? 8);
|
|
const runglerFeedback = params.runglerFeedback ?? 0.3;
|
|
const runglerChaos = params.runglerChaos ?? 0.5;
|
|
|
|
const filterCutoff = params.filterCutoff ?? 1000;
|
|
const filterResonance = params.filterResonance ?? 0.8;
|
|
const filterMode = params.filterMode ?? 0;
|
|
const filterDrive = params.filterDrive ?? 1.5;
|
|
const filterFeedback = params.filterFeedback ?? 0;
|
|
|
|
const evolutionRate = params.evolutionRate ?? 0.1;
|
|
const evolutionDepth = params.evolutionDepth ?? 0.2;
|
|
const chaosAttractorRate = params.chaosAttractorRate ?? 0.05;
|
|
const chaosAttractorDepth = params.chaosAttractorDepth ?? 0.3;
|
|
|
|
const lfo1Rate = params.lfo1Rate ?? 0.1;
|
|
const lfo1Depth = params.lfo1Depth ?? 0.2;
|
|
const lfo1Target = params.lfo1Target ?? 0.5;
|
|
const lfo2Rate = params.lfo2Rate ?? 0.3;
|
|
const lfo2Depth = params.lfo2Depth ?? 0.15;
|
|
const lfo2Wave = params.lfo2Wave ?? 0;
|
|
|
|
const wavefoldAmount = params.wavefoldAmount ?? 0;
|
|
const distortionType = params.distortionType ?? 0;
|
|
const stereoWidth = params.stereoWidth ?? 0.5;
|
|
const outputGain = params.outputGain ?? 0.5;
|
|
|
|
const envelopeAttack = params.envelopeAttack ?? 0.05;
|
|
const envelopeDecay = params.envelopeDecay ?? 0.15;
|
|
const envelopeSustain = params.envelopeSustain ?? 0.3;
|
|
const envelopeRelease = params.envelopeRelease ?? 0.5;
|
|
const envelopeToFilter = params.envelopeToFilter ?? 0.3;
|
|
const envelopeToFold = params.envelopeToFold ?? 0.2;
|
|
|
|
// Oscillator states
|
|
let osc1Phase = 0;
|
|
let osc2Phase = 0;
|
|
let osc1LastOutput = 0;
|
|
let osc2LastOutput = 0;
|
|
let osc2LastCrossing = false;
|
|
|
|
// Extended rungler state (up to 16-bit)
|
|
const runglerMask = (1 << runglerBits) - 1;
|
|
let runglerRegister = Math.floor(Math.random() * runglerMask);
|
|
let runglerCV = 0;
|
|
let runglerSmoothed = 0;
|
|
let runglerHistory = 0; // For feedback
|
|
const runglerSmoothFactor = 0.995;
|
|
|
|
// Filter states (dual state-variable filters)
|
|
let filter1LP = 0, filter1HP = 0, filter1BP = 0;
|
|
let filter2LP = 0, filter2HP = 0, filter2BP = 0;
|
|
let filterSelfOsc = 0;
|
|
|
|
// Evolution and chaos states
|
|
let evolutionPhase = 0;
|
|
let evolutionValue = 0;
|
|
let chaosX = 0.1, chaosY = 0.1, chaosZ = 0.1; // Lorenz attractor
|
|
let driftAccumulator = 0;
|
|
|
|
// LFO states
|
|
let lfo1Phase = 0;
|
|
let lfo2Phase = 0;
|
|
let lfo2SampleHold = 0;
|
|
let lfo2LastPhase = 0;
|
|
|
|
// Envelope follower for rungler
|
|
let runglerEnvelope = 0;
|
|
const envelopeFollowRate = 0.99;
|
|
|
|
// Wavefolder state
|
|
let wavefoldIntegrator = 0;
|
|
|
|
// DC blocker states
|
|
let dcBlockerX1L = 0, dcBlockerY1L = 0;
|
|
let dcBlockerX1R = 0, dcBlockerY1R = 0;
|
|
const dcBlockerCutoff = 20 / sampleRate;
|
|
const dcBlockerAlpha = 1 - dcBlockerCutoff;
|
|
|
|
// Envelope
|
|
const attackSamples = Math.floor(envelopeAttack * duration * sampleRate);
|
|
const decaySamples = Math.floor(envelopeDecay * duration * sampleRate);
|
|
const releaseSamples = Math.floor(envelopeRelease * duration * sampleRate);
|
|
const sustainSamples = Math.max(0, numSamples - attackSamples - decaySamples - releaseSamples);
|
|
|
|
// Stereo delay line for width
|
|
const delayLength = Math.floor(0.003 * sampleRate); // 3ms
|
|
const delayBuffer = new Float32Array(delayLength);
|
|
let delayIndex = 0;
|
|
|
|
for (let i = 0; i < numSamples; i++) {
|
|
// Calculate main envelope
|
|
let envelope = 0;
|
|
if (i < attackSamples) {
|
|
envelope = i / attackSamples;
|
|
} else if (i < attackSamples + decaySamples) {
|
|
const decayProgress = (i - attackSamples) / decaySamples;
|
|
envelope = 1 - decayProgress * (1 - envelopeSustain);
|
|
} else if (i < attackSamples + decaySamples + sustainSamples) {
|
|
envelope = envelopeSustain;
|
|
} else {
|
|
const releaseProgress = (i - attackSamples - decaySamples - sustainSamples) / releaseSamples;
|
|
envelope = envelopeSustain * (1 - releaseProgress);
|
|
}
|
|
|
|
// Update evolution (slow parameter drift)
|
|
evolutionPhase += evolutionRate / sampleRate;
|
|
evolutionValue = Math.sin(evolutionPhase * 2 * Math.PI) * evolutionDepth;
|
|
|
|
// Update chaos attractor (Lorenz system) with safety bounds
|
|
const dt = 0.01 * chaosAttractorRate;
|
|
const sigma = 10;
|
|
const rho = 28;
|
|
const beta = 8/3;
|
|
|
|
// Calculate derivatives
|
|
const dx = sigma * (chaosY - chaosX) * dt;
|
|
const dy = (chaosX * (rho - chaosZ) - chaosY) * dt;
|
|
const dz = (chaosX * chaosY - beta * chaosZ) * dt;
|
|
|
|
// Update with bounds checking
|
|
chaosX = Math.max(-50, Math.min(50, chaosX + dx));
|
|
chaosY = Math.max(-50, Math.min(50, chaosY + dy));
|
|
chaosZ = Math.max(-50, Math.min(50, chaosZ + dz));
|
|
|
|
// Check for NaN and reset if necessary
|
|
if (!isFinite(chaosX) || !isFinite(chaosY) || !isFinite(chaosZ)) {
|
|
chaosX = 0.1;
|
|
chaosY = 0.1;
|
|
chaosZ = 0.1;
|
|
}
|
|
|
|
const chaosValue = Math.tanh(chaosZ * 0.01) * chaosAttractorDepth;
|
|
|
|
// Update LFOs
|
|
lfo1Phase += lfo1Rate / sampleRate;
|
|
if (lfo1Phase >= 1) lfo1Phase -= 1;
|
|
const lfo1Value = Math.sin(lfo1Phase * 2 * Math.PI) * lfo1Depth;
|
|
|
|
lfo2Phase += lfo2Rate / sampleRate;
|
|
if (lfo2Phase >= 1) lfo2Phase -= 1;
|
|
|
|
// LFO2 waveform selection
|
|
let lfo2Value = 0;
|
|
if (lfo2Wave < 0.33) {
|
|
// Sine
|
|
lfo2Value = Math.sin(lfo2Phase * 2 * Math.PI);
|
|
} else if (lfo2Wave < 0.67) {
|
|
// Triangle
|
|
lfo2Value = lfo2Phase < 0.5 ? lfo2Phase * 4 - 1 : 3 - lfo2Phase * 4;
|
|
} else {
|
|
// Sample & Hold
|
|
if (lfo2Phase < lfo2LastPhase) {
|
|
lfo2SampleHold = Math.random() * 2 - 1;
|
|
}
|
|
lfo2Value = lfo2SampleHold;
|
|
}
|
|
lfo2LastPhase = lfo2Phase;
|
|
lfo2Value *= lfo2Depth;
|
|
|
|
// Track rungler envelope
|
|
runglerEnvelope = runglerEnvelope * envelopeFollowRate +
|
|
Math.abs(runglerCV) * (1 - envelopeFollowRate);
|
|
|
|
// Smooth rungler CV with drift
|
|
driftAccumulator += (Math.random() - 0.5) * 0.001;
|
|
driftAccumulator *= 0.999; // Decay
|
|
runglerSmoothed += (runglerCV - runglerSmoothed + driftAccumulator) * (1 - runglerSmoothFactor);
|
|
|
|
// Apply LFO1 based on target
|
|
let lfo1Modulation = 0;
|
|
if (lfo1Target < 0.33) {
|
|
lfo1Modulation = lfo1Value; // To frequency
|
|
} else if (lfo1Target < 0.67) {
|
|
lfo1Modulation = lfo1Value * 0.5; // To filter
|
|
} else {
|
|
lfo1Modulation = lfo1Value * 0.3; // To chaos
|
|
}
|
|
|
|
// Calculate oscillator frequencies with complex modulation
|
|
const osc1ModFreq = osc1Freq *
|
|
(1 + osc2LastOutput * crossMod2to1 * (2 + evolutionValue) +
|
|
runglerSmoothed * runglerToOsc1 * (1 + chaosValue) +
|
|
lfo1Modulation * (lfo1Target < 0.5 ? 1 : 0));
|
|
|
|
const osc2ModFreq = osc2Freq *
|
|
(1 + osc1LastOutput * crossMod1to2 * (2 + chaosValue) +
|
|
runglerSmoothed * runglerToOsc2 * (1 + evolutionValue) +
|
|
lfo2Value * 0.5);
|
|
|
|
// Clamp frequencies but allow wider range
|
|
const clampedOsc1Freq = Math.max(1, Math.min(12000, osc1ModFreq));
|
|
const clampedOsc2Freq = Math.max(1, Math.min(12000, osc2ModFreq));
|
|
|
|
// Update oscillator phases
|
|
const osc1PhaseInc = clampedOsc1Freq / sampleRate;
|
|
const osc2PhaseInc = clampedOsc2Freq / sampleRate;
|
|
|
|
osc1Phase += osc1PhaseInc;
|
|
osc2Phase += osc2PhaseInc;
|
|
|
|
if (osc1Phase >= 1) osc1Phase -= 1;
|
|
if (osc2Phase >= 1) osc2Phase -= 1;
|
|
|
|
// Generate morphable oscillator waveforms
|
|
const osc1Tri = this.polyBlepTriangle(osc1Phase, osc1PhaseInc);
|
|
const osc1Saw = this.polyBlepSaw(osc1Phase, osc1PhaseInc);
|
|
const osc1Output = osc1Tri * (1 - osc1Wave) + osc1Saw * osc1Wave;
|
|
|
|
const osc2Tri = this.polyBlepTriangle(osc2Phase, osc2PhaseInc);
|
|
const osc2Pulse = this.polyBlepPulse(osc2Phase, osc2PhaseInc, 0.5 + osc2Wave * 0.45);
|
|
const osc2Output = osc2Tri * (1 - osc2Wave) + osc2Pulse * osc2Wave;
|
|
|
|
// Enhanced rungler with feedback
|
|
const osc2Crossing = osc2Output > 0;
|
|
if (osc2Crossing && !osc2LastCrossing) {
|
|
// Shift with feedback
|
|
runglerRegister = (runglerRegister << 1) & runglerMask;
|
|
|
|
// Sample input with cross-modulation influence
|
|
const runglerInput = osc1Output + crossMod2toRungler * osc2Output;
|
|
if (runglerInput > runglerHistory * runglerFeedback) {
|
|
runglerRegister |= 1;
|
|
}
|
|
runglerHistory = runglerInput;
|
|
|
|
// Enhanced XOR chaos
|
|
let xorResult = 0;
|
|
const numBits = runglerBits - 1;
|
|
for (let bit = 0; bit < numBits; bit++) {
|
|
const bitA = (runglerRegister >> bit) & 1;
|
|
const bitB = (runglerRegister >> ((bit + 1) % runglerBits)) & 1;
|
|
const bitC = (runglerRegister >> ((bit + 3) % runglerBits)) & 1;
|
|
// Three-way XOR for more chaos
|
|
xorResult |= ((bitA ^ bitB ^ bitC) & 1) << bit;
|
|
}
|
|
|
|
// Mix direct register value with XOR chaos
|
|
const directValue = runglerRegister / runglerMask;
|
|
const chaosValueNorm = xorResult / runglerMask;
|
|
runglerCV = (directValue * (1 - runglerChaos) + chaosValueNorm * runglerChaos) * 2 - 1;
|
|
}
|
|
osc2LastCrossing = osc2Crossing;
|
|
|
|
// Dual state-variable filters with self-oscillation
|
|
const modulatedCutoff = filterCutoff *
|
|
(1 + runglerSmoothed * runglerToFilter * 0.5 +
|
|
osc1Output * crossMod1toFilter * 0.3 +
|
|
envelope * envelopeToFilter +
|
|
(lfo1Target > 0.33 && lfo1Target < 0.67 ? lfo1Modulation : 0) +
|
|
chaosValue * 0.2);
|
|
|
|
const clampedCutoff = Math.max(20, Math.min(sampleRate * 0.48, modulatedCutoff));
|
|
const filterFreq = 2 * Math.sin(Math.PI * clampedCutoff / sampleRate);
|
|
|
|
// Allow self-oscillation with safety limits
|
|
const baseQ = 1 / Math.max(0.05, 2 - filterResonance * 1.95);
|
|
const filterQ = Math.max(0.5, Math.min(20, baseQ + filterFeedback * 5));
|
|
|
|
// Drive the filter input with limiting
|
|
const filterInput = (osc1Output * 0.7 + osc2Output * 0.3) * Math.min(3, filterDrive);
|
|
const drivenInput = Math.tanh(filterInput);
|
|
|
|
// First filter stage with stability checks
|
|
const filter1Input = drivenInput - filter1LP - filterQ * filter1BP + filterSelfOsc * filterFeedback * 0.5;
|
|
filter1HP = Math.max(-5, Math.min(5, filter1Input));
|
|
filter1BP += filterFreq * filter1HP;
|
|
filter1LP += filterFreq * filter1BP;
|
|
|
|
// Prevent filter state explosion
|
|
filter1BP = Math.max(-2, Math.min(2, filter1BP));
|
|
filter1LP = Math.max(-2, Math.min(2, filter1LP));
|
|
|
|
// Check for NaN and reset if necessary
|
|
if (!isFinite(filter1LP) || !isFinite(filter1BP) || !isFinite(filter1HP)) {
|
|
filter1LP = 0;
|
|
filter1BP = 0;
|
|
filter1HP = 0;
|
|
}
|
|
|
|
// Second filter stage (in series for 24dB/oct)
|
|
const filter1Out = filter1LP * (1 - filterMode) +
|
|
filter1BP * (filterMode * 2 * (filterMode < 0.5 ? 1 : 0)) +
|
|
filter1HP * (filterMode > 0.5 ? (filterMode - 0.5) * 2 : 0);
|
|
|
|
const filter2Input = filter1Out - filter2LP - filterQ * 0.7 * filter2BP;
|
|
filter2HP = Math.max(-5, Math.min(5, filter2Input));
|
|
filter2BP += filterFreq * filter2HP;
|
|
filter2LP += filterFreq * filter2BP;
|
|
|
|
// Prevent filter state explosion
|
|
filter2BP = Math.max(-2, Math.min(2, filter2BP));
|
|
filter2LP = Math.max(-2, Math.min(2, filter2LP));
|
|
|
|
// Check for NaN and reset if necessary
|
|
if (!isFinite(filter2LP) || !isFinite(filter2BP) || !isFinite(filter2HP)) {
|
|
filter2LP = 0;
|
|
filter2BP = 0;
|
|
filter2HP = 0;
|
|
}
|
|
|
|
// Mix filter outputs with morphing
|
|
let filterOut = filter2LP * (1 - filterMode) +
|
|
filter2BP * (filterMode * 2 * (filterMode < 0.5 ? 1 : 0)) +
|
|
filter2HP * (filterMode > 0.5 ? (filterMode - 0.5) * 2 : 0);
|
|
|
|
// Track filter self-oscillation with limiting
|
|
filterSelfOsc = Math.tanh(filter2BP * 0.1);
|
|
|
|
// Wavefolding stage
|
|
let foldedSignal = filterOut;
|
|
if (wavefoldAmount > 0) {
|
|
const foldGain = 1 + wavefoldAmount * (4 + envelope * envelopeToFold * 2);
|
|
foldedSignal = this.wavefold(filterOut * foldGain, 2 + wavefoldAmount * 3) / foldGain;
|
|
|
|
// Integrate for smoother folding
|
|
wavefoldIntegrator += (foldedSignal - wavefoldIntegrator) * 0.1;
|
|
foldedSignal = foldedSignal * (1 - wavefoldAmount * 0.3) + wavefoldIntegrator * wavefoldAmount * 0.3;
|
|
}
|
|
|
|
// Distortion stage
|
|
let output = foldedSignal;
|
|
if (distortionType < 0.33) {
|
|
// Soft saturation
|
|
output = Math.tanh(output * (1 + distortionType * 3));
|
|
} else if (distortionType < 0.67) {
|
|
// Wavefolder
|
|
const foldAmount = (distortionType - 0.33) * 3;
|
|
output = this.wavefold(output * (1 + foldAmount * 2), 3);
|
|
} else {
|
|
// Digital decimation
|
|
const bitDepth = Math.floor(16 - (distortionType - 0.67) * 3 * 12);
|
|
const scale = Math.pow(2, bitDepth);
|
|
output = Math.floor(output * scale) / scale;
|
|
}
|
|
|
|
// Apply envelope and gain
|
|
output *= envelope * outputGain;
|
|
|
|
// Final soft limiting
|
|
output = Math.tanh(output * 0.9) * 0.95;
|
|
|
|
// DC blocking for left channel
|
|
let dcBlockerYL = output - dcBlockerX1L + dcBlockerAlpha * dcBlockerY1L;
|
|
dcBlockerX1L = output;
|
|
dcBlockerY1L = dcBlockerYL;
|
|
|
|
// Stereo processing
|
|
const monoSignal = dcBlockerYL;
|
|
|
|
// Create stereo width with micro-delay and phase differences
|
|
delayBuffer[delayIndex] = monoSignal + filter1BP * 0.05;
|
|
const delayedSignal = delayBuffer[(delayIndex + delayLength - Math.floor(stereoWidth * delayLength)) % delayLength];
|
|
delayIndex = (delayIndex + 1) % delayLength;
|
|
|
|
// Add evolving stereo movement
|
|
const stereoPan = Math.sin(i / sampleRate * 0.7 + runglerEnvelope * 3) * stereoWidth * 0.3;
|
|
|
|
left[i] = monoSignal * (1 - stereoPan * 0.5) + delayedSignal * stereoWidth * 0.2;
|
|
|
|
// Right channel with different filtering for width
|
|
const rightSignal = monoSignal * 0.7 + filter2BP * 0.15 + filterSelfOsc * 0.15;
|
|
|
|
// DC blocking for right channel
|
|
let dcBlockerYR = rightSignal - dcBlockerX1R + dcBlockerAlpha * dcBlockerY1R;
|
|
dcBlockerX1R = rightSignal;
|
|
dcBlockerY1R = dcBlockerYR;
|
|
|
|
right[i] = dcBlockerYR * (1 + stereoPan * 0.5) + delayedSignal * stereoWidth * 0.3;
|
|
|
|
// Store states for next iteration
|
|
osc1LastOutput = osc1Output;
|
|
osc2LastOutput = osc2Output;
|
|
}
|
|
|
|
// Normalize the output to use full dynamic range
|
|
let peak = 0;
|
|
for (let i = 0; i < numSamples; i++) {
|
|
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
|
|
}
|
|
|
|
if (peak > 0.001) {
|
|
const normalizationGain = 0.95 / peak;
|
|
for (let i = 0; i < numSamples; i++) {
|
|
left[i] *= normalizationGain;
|
|
right[i] *= normalizationGain;
|
|
}
|
|
}
|
|
|
|
return [left, right];
|
|
}
|
|
|
|
// Oscillator generation methods
|
|
private polyBlepSaw(phase: number, phaseInc: number): number {
|
|
let value = 2 * phase - 1;
|
|
value -= this.polyBlep(phase, phaseInc);
|
|
return value;
|
|
}
|
|
|
|
private polyBlepPulse(phase: number, phaseInc: number, pulseWidth: number): number {
|
|
let value = phase < pulseWidth ? 1 : -1;
|
|
value += this.polyBlep(phase, phaseInc);
|
|
value -= this.polyBlep((phase - pulseWidth + 1) % 1, phaseInc);
|
|
return value;
|
|
}
|
|
|
|
private polyBlepTriangle(phase: number, phaseInc: number): number {
|
|
let value = phase < 0.5 ? phase * 4 - 1 : 3 - phase * 4;
|
|
const polyBlepCorrection = this.polyBlepIntegral(phase, phaseInc) -
|
|
this.polyBlepIntegral((phase + 0.5) % 1, phaseInc);
|
|
value += polyBlepCorrection * 4;
|
|
return value;
|
|
}
|
|
|
|
private polyBlep(t: number, dt: number): number {
|
|
if (t < dt) {
|
|
t /= dt;
|
|
return t + t - t * t - 1;
|
|
} else if (t > 1 - dt) {
|
|
t = (t - 1) / dt;
|
|
return t * t + t + t + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private polyBlepIntegral(t: number, dt: number): number {
|
|
if (t < dt) {
|
|
t /= dt;
|
|
return (t * t * t) / 3 - t * t / 2 - t / 2;
|
|
} else if (t > 1 - dt) {
|
|
t = (t - 1) / dt;
|
|
return t * t * t / 3 + t * t / 2 + t / 2;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Wavefolding function - safe implementation
|
|
private wavefold(input: number, folds: number): number {
|
|
// Limit input to prevent numerical issues
|
|
const clampedInput = Math.max(-10, Math.min(10, input));
|
|
const threshold = 1 / Math.max(1, folds);
|
|
|
|
let folded = clampedInput;
|
|
// Maximum iterations to prevent infinite loop
|
|
const maxIterations = 10;
|
|
let iterations = 0;
|
|
|
|
while (Math.abs(folded) > threshold && iterations < maxIterations) {
|
|
if (folded > threshold) {
|
|
folded = threshold * 2 - folded;
|
|
} else if (folded < -threshold) {
|
|
folded = -threshold * 2 - folded;
|
|
}
|
|
iterations++;
|
|
}
|
|
|
|
// Final clamp to ensure output is bounded
|
|
return Math.max(-1, Math.min(1, folded * folds));
|
|
}
|
|
|
|
// Configuration-based parameter generation
|
|
private generatePresetParams(preset: number): Partial<BenjolinParams> {
|
|
switch (preset) {
|
|
case 0:
|
|
return {
|
|
osc1Freq: 50 + Math.random() * 150,
|
|
osc2Freq: 80 + Math.random() * 200,
|
|
crossMod1to2: 0.3 + Math.random() * 0.3,
|
|
crossMod2to1: 0.3 + Math.random() * 0.3,
|
|
runglerChaos: 0.6 + Math.random() * 0.3,
|
|
filterResonance: 0.7 + Math.random() * 0.25,
|
|
filterMode: Math.random() * 0.3,
|
|
evolutionRate: 0.1 + Math.random() * 0.2,
|
|
lfo1Rate: 0.05 + Math.random() * 0.1
|
|
};
|
|
|
|
case 1:
|
|
return {
|
|
osc1Freq: 200 + Math.random() * 800,
|
|
osc2Freq: 300 + Math.random() * 1200,
|
|
crossMod1to2: 0.6 + Math.random() * 0.4,
|
|
crossMod2to1: 0.6 + Math.random() * 0.4,
|
|
runglerToFilter: 0.5 + Math.random() * 0.5,
|
|
filterResonance: 0.8 + Math.random() * 0.15,
|
|
filterFeedback: 0.3 + Math.random() * 0.4,
|
|
filterDrive: 2 + Math.random() * 2,
|
|
wavefoldAmount: 0.3 + Math.random() * 0.4
|
|
};
|
|
|
|
case 2: {
|
|
const baseFreq = 50 + Math.random() * 100;
|
|
return {
|
|
osc1Freq: baseFreq,
|
|
osc2Freq: baseFreq * (Math.floor(Math.random() * 4) + 1.5),
|
|
runglerBits: Math.random() > 0.5 ? 8 : 4,
|
|
runglerChaos: 0.7 + Math.random() * 0.3,
|
|
lfo2Rate: 0.2 + Math.random() * 0.5,
|
|
lfo2Wave: 0.8 + Math.random() * 0.2,
|
|
envelopeAttack: 0.001 + Math.random() * 0.01,
|
|
envelopeDecay: 0.01 + Math.random() * 0.05
|
|
};
|
|
}
|
|
|
|
case 3:
|
|
return {
|
|
osc1Freq: 30 + Math.random() * 100,
|
|
osc2Freq: 40 + Math.random() * 120,
|
|
crossMod1to2: 0.1 + Math.random() * 0.2,
|
|
crossMod2to1: 0.1 + Math.random() * 0.2,
|
|
evolutionRate: 0.01 + Math.random() * 0.05,
|
|
evolutionDepth: 0.3 + Math.random() * 0.4,
|
|
filterMode: Math.random() * 0.5,
|
|
envelopeAttack: 0.3 + Math.random() * 0.4,
|
|
envelopeRelease: 0.4 + Math.random() * 0.4,
|
|
stereoWidth: 0.6 + Math.random() * 0.4
|
|
};
|
|
|
|
case 4: {
|
|
const metalFreq = 100 + Math.random() * 500;
|
|
return {
|
|
osc1Freq: metalFreq,
|
|
osc2Freq: metalFreq * (1.41 + Math.random() * 0.2),
|
|
osc1Wave: 0.7 + Math.random() * 0.3,
|
|
filterResonance: 0.85 + Math.random() * 0.1,
|
|
filterDrive: 1.5 + Math.random(),
|
|
distortionType: 0.5 + Math.random() * 0.5,
|
|
wavefoldAmount: 0.2 + Math.random() * 0.3
|
|
};
|
|
}
|
|
|
|
case 5:
|
|
return {
|
|
osc1Freq: 60 + Math.random() * 200,
|
|
osc2Freq: 90 + Math.random() * 300,
|
|
evolutionRate: 0.05 + Math.random() * 0.15,
|
|
chaosAttractorRate: 0.02 + Math.random() * 0.08,
|
|
chaosAttractorDepth: 0.2 + Math.random() * 0.3,
|
|
lfo1Depth: 0.1 + Math.random() * 0.2,
|
|
filterMode: 0.2 + Math.random() * 0.3,
|
|
envelopeToFilter: 0.3 + Math.random() * 0.4
|
|
};
|
|
|
|
case 6:
|
|
return {
|
|
runglerBits: Math.floor(4 + Math.random() * 12),
|
|
runglerFeedback: 0.5 + Math.random() * 0.5,
|
|
runglerChaos: 0.8 + Math.random() * 0.2,
|
|
distortionType: 0.7 + Math.random() * 0.3,
|
|
lfo2Wave: 0.7 + Math.random() * 0.3,
|
|
filterDrive: 3 + Math.random() * 2,
|
|
wavefoldAmount: 0.4 + Math.random() * 0.4
|
|
};
|
|
|
|
case 7:
|
|
return {
|
|
osc1Freq: 40 + Math.random() * 150,
|
|
osc2Freq: 60 + Math.random() * 200,
|
|
crossMod1to2: 0.05 + Math.random() * 0.15,
|
|
crossMod2to1: 0.05 + Math.random() * 0.15,
|
|
evolutionRate: 0.02 + Math.random() * 0.08,
|
|
filterMode: Math.random() * 0.4,
|
|
stereoWidth: 0.7 + Math.random() * 0.3,
|
|
envelopeAttack: 0.2 + Math.random() * 0.3,
|
|
outputGain: 0.3 + Math.random() * 0.3
|
|
};
|
|
|
|
case 8:
|
|
return {
|
|
osc1Freq: 20 + Math.random() * 40,
|
|
osc2Freq: 25 + Math.random() * 50,
|
|
crossMod1to2: 0.4 + Math.random() * 0.3,
|
|
crossMod2to1: 0.2 + Math.random() * 0.2,
|
|
filterCutoff: 80 + Math.random() * 200,
|
|
filterResonance: 0.6 + Math.random() * 0.3,
|
|
filterDrive: 2 + Math.random() * 2,
|
|
envelopeAttack: 0.001,
|
|
envelopeDecay: 0.05 + Math.random() * 0.1,
|
|
outputGain: 0.6 + Math.random() * 0.3
|
|
};
|
|
|
|
case 9: {
|
|
const bellFreq = 200 + Math.random() * 600;
|
|
return {
|
|
osc1Freq: bellFreq,
|
|
osc2Freq: bellFreq * (2.71 + Math.random() * 0.3),
|
|
osc1Wave: 0.9,
|
|
osc2Wave: 0.1,
|
|
filterResonance: 0.9 + Math.random() * 0.05,
|
|
filterMode: 0.7 + Math.random() * 0.3,
|
|
envelopeAttack: 0.001,
|
|
envelopeDecay: 0.02 + Math.random() * 0.03,
|
|
envelopeSustain: 0,
|
|
envelopeRelease: 0.3 + Math.random() * 0.4,
|
|
stereoWidth: 0.8 + Math.random() * 0.2
|
|
};
|
|
}
|
|
|
|
case 10:
|
|
return {
|
|
osc1Freq: 500 + Math.random() * 1500,
|
|
osc2Freq: 800 + Math.random() * 2000,
|
|
crossMod1to2: 0.8 + Math.random() * 0.2,
|
|
crossMod2to1: 0.8 + Math.random() * 0.2,
|
|
runglerChaos: 0.9 + Math.random() * 0.1,
|
|
runglerBits: 4,
|
|
filterDrive: 3 + Math.random() * 2,
|
|
distortionType: 0.8 + Math.random() * 0.2,
|
|
envelopeAttack: 0.001,
|
|
envelopeDecay: 0.01 + Math.random() * 0.02
|
|
};
|
|
|
|
case 11: {
|
|
const carrier = 100 + Math.random() * 300;
|
|
return {
|
|
osc1Freq: carrier,
|
|
osc2Freq: carrier * (Math.floor(Math.random() * 5) + 1),
|
|
crossMod1to2: 0.7 + Math.random() * 0.3,
|
|
crossMod2to1: 0.1 + Math.random() * 0.2,
|
|
osc1Wave: 0,
|
|
osc2Wave: 0,
|
|
filterMode: 0.1 + Math.random() * 0.2,
|
|
lfo1Target: 0,
|
|
lfo1Depth: 0.3 + Math.random() * 0.3,
|
|
lfo1Rate: 0.1 + Math.random() * 0.3
|
|
};
|
|
}
|
|
|
|
case 12:
|
|
return {
|
|
osc1Freq: 150 + Math.random() * 350,
|
|
osc2Freq: 200 + Math.random() * 400,
|
|
runglerBits: 16,
|
|
runglerToOsc1: 0.4 + Math.random() * 0.3,
|
|
runglerToOsc2: 0.4 + Math.random() * 0.3,
|
|
evolutionRate: 0.2 + Math.random() * 0.3,
|
|
chaosAttractorRate: 0.1 + Math.random() * 0.2,
|
|
envelopeAttack: 0.01 + Math.random() * 0.03,
|
|
envelopeDecay: 0.02 + Math.random() * 0.05,
|
|
wavefoldAmount: 0.1 + Math.random() * 0.2
|
|
};
|
|
|
|
case 13:
|
|
return {
|
|
osc1Freq: 80 + Math.random() * 200,
|
|
osc2Freq: 120 + Math.random() * 300,
|
|
filterCutoff: 200 + Math.random() * 800,
|
|
filterResonance: 0.85 + Math.random() * 0.1,
|
|
filterFeedback: 0.4 + Math.random() * 0.3,
|
|
lfo1Target: 0.5,
|
|
lfo1Rate: 0.3 + Math.random() * 0.4,
|
|
lfo1Depth: 0.5 + Math.random() * 0.3,
|
|
envelopeToFilter: 0.6 + Math.random() * 0.3
|
|
};
|
|
|
|
case 14:
|
|
return {
|
|
osc1Freq: 60 + Math.random() * 150,
|
|
osc2Freq: 100 + Math.random() * 250,
|
|
osc2Wave: 0.8 + Math.random() * 0.2,
|
|
filterDrive: 2.5 + Math.random() * 1.5,
|
|
filterMode: 0.6 + Math.random() * 0.4,
|
|
distortionType: 0.4 + Math.random() * 0.3,
|
|
envelopeAttack: 0.001,
|
|
envelopeDecay: 0.01 + Math.random() * 0.03,
|
|
envelopeSustain: 0.1 + Math.random() * 0.2,
|
|
envelopeRelease: 0.05 + Math.random() * 0.1
|
|
};
|
|
|
|
case 15: {
|
|
const aliasFreq = 5000 + Math.random() * 7000;
|
|
return {
|
|
osc1Freq: aliasFreq,
|
|
osc2Freq: aliasFreq * (1.01 + Math.random() * 0.02),
|
|
osc1Wave: 1,
|
|
osc2Wave: 0.5,
|
|
filterCutoff: 8000 + Math.random() * 4000,
|
|
filterMode: 0.8 + Math.random() * 0.2,
|
|
distortionType: 0.85 + Math.random() * 0.15,
|
|
outputGain: 0.3 + Math.random() * 0.2
|
|
};
|
|
}
|
|
|
|
case 16:
|
|
return {
|
|
osc1Freq: 100 + Math.random() * 200,
|
|
osc2Freq: 150 + Math.random() * 250,
|
|
crossMod1to2: 0.2 + Math.random() * 0.2,
|
|
crossMod2to1: 0.2 + Math.random() * 0.2,
|
|
lfo1Rate: 0.01 + Math.random() * 0.03,
|
|
lfo2Rate: 0.02 + Math.random() * 0.04,
|
|
lfo1Depth: 0.3 + Math.random() * 0.4,
|
|
lfo2Depth: 0.3 + Math.random() * 0.4,
|
|
evolutionRate: 0.005 + Math.random() * 0.02,
|
|
evolutionDepth: 0.4 + Math.random() * 0.4
|
|
};
|
|
|
|
case 17:
|
|
return {
|
|
osc1Freq: 100 + Math.random() * 400,
|
|
osc2Freq: 150 + Math.random() * 600,
|
|
crossMod1to2: 0.7 + Math.random() * 0.3,
|
|
crossMod2to1: 0.7 + Math.random() * 0.3,
|
|
runglerFeedback: 0.8 + Math.random() * 0.2,
|
|
filterFeedback: 0.6 + Math.random() * 0.3,
|
|
filterResonance: 0.9 + Math.random() * 0.05,
|
|
filterDrive: 3 + Math.random() * 2,
|
|
wavefoldAmount: 0.5 + Math.random() * 0.4,
|
|
distortionType: 0.3 + Math.random() * 0.4
|
|
};
|
|
|
|
default:
|
|
// Fallback to fully random
|
|
return {};
|
|
}
|
|
}
|
|
|
|
randomParams(pitchLock?: PitchLock): BenjolinParams {
|
|
// Choose a random preset configuration
|
|
const preset = Math.floor(Math.random() * PRESET_COUNT);
|
|
|
|
// Get preset-specific parameters
|
|
const presetParams = this.generatePresetParams(preset);
|
|
|
|
// Generate full parameter set with preset biases
|
|
const params: BenjolinParams = {
|
|
// Core oscillators
|
|
osc1Freq: pitchLock?.enabled
|
|
? pitchLock.frequency
|
|
: presetParams.osc1Freq ?? (20 + Math.random() * 800),
|
|
osc2Freq: presetParams.osc2Freq ?? (30 + Math.random() * 1200),
|
|
osc1Wave: presetParams.osc1Wave ?? Math.random(),
|
|
osc2Wave: presetParams.osc2Wave ?? Math.random(),
|
|
|
|
// Cross modulation
|
|
crossMod1to2: presetParams.crossMod1to2 ?? Math.random() * 0.8,
|
|
crossMod2to1: presetParams.crossMod2to1 ?? Math.random() * 0.8,
|
|
crossMod1toFilter: presetParams.crossMod1toFilter ?? Math.random() * 0.5,
|
|
crossMod2toRungler: presetParams.crossMod2toRungler ?? Math.random() * 0.4,
|
|
|
|
// Rungler
|
|
runglerToOsc1: presetParams.runglerToOsc1 ?? Math.random() * 0.6,
|
|
runglerToOsc2: presetParams.runglerToOsc2 ?? Math.random() * 0.6,
|
|
runglerToFilter: presetParams.runglerToFilter ?? Math.random() * 0.8,
|
|
runglerBits: presetParams.runglerBits ?? (Math.random() > 0.7 ? 16 : Math.random() > 0.4 ? 12 : 8),
|
|
runglerFeedback: presetParams.runglerFeedback ?? Math.random() * 0.7,
|
|
runglerChaos: presetParams.runglerChaos ?? (0.3 + Math.random() * 0.7),
|
|
|
|
// Filter
|
|
filterCutoff: presetParams.filterCutoff ?? (100 + Math.random() * 3000),
|
|
filterResonance: presetParams.filterResonance ?? (0.3 + Math.random() * 0.65),
|
|
filterMode: presetParams.filterMode ?? Math.random(),
|
|
filterDrive: presetParams.filterDrive ?? (0.5 + Math.random() * 2.5),
|
|
filterFeedback: presetParams.filterFeedback ?? Math.random() * 0.5,
|
|
|
|
// Evolution
|
|
evolutionRate: presetParams.evolutionRate ?? Math.random() * 0.3,
|
|
evolutionDepth: presetParams.evolutionDepth ?? Math.random() * 0.5,
|
|
chaosAttractorRate: presetParams.chaosAttractorRate ?? Math.random() * 0.2,
|
|
chaosAttractorDepth: presetParams.chaosAttractorDepth ?? Math.random() * 0.5,
|
|
|
|
// LFOs
|
|
lfo1Rate: presetParams.lfo1Rate ?? Math.random() * 0.5,
|
|
lfo1Depth: presetParams.lfo1Depth ?? Math.random() * 0.4,
|
|
lfo1Target: presetParams.lfo1Target ?? Math.random(),
|
|
lfo2Rate: presetParams.lfo2Rate ?? Math.random() * 0.8,
|
|
lfo2Depth: presetParams.lfo2Depth ?? Math.random() * 0.3,
|
|
lfo2Wave: presetParams.lfo2Wave ?? Math.random(),
|
|
|
|
// Output shaping
|
|
wavefoldAmount: presetParams.wavefoldAmount ?? Math.random() * 0.6,
|
|
distortionType: presetParams.distortionType ?? Math.random(),
|
|
stereoWidth: presetParams.stereoWidth ?? (0.2 + Math.random() * 0.8),
|
|
outputGain: presetParams.outputGain ?? (0.2 + Math.random() * 0.5),
|
|
|
|
// Envelope
|
|
envelopeAttack: presetParams.envelopeAttack ?? (0.001 + Math.random() * 0.2),
|
|
envelopeDecay: presetParams.envelopeDecay ?? (0.01 + Math.random() * 0.3),
|
|
envelopeSustain: presetParams.envelopeSustain ?? Math.random(),
|
|
envelopeRelease: presetParams.envelopeRelease ?? (0.05 + Math.random() * 0.5),
|
|
envelopeToFilter: presetParams.envelopeToFilter ?? Math.random() * 0.6,
|
|
envelopeToFold: presetParams.envelopeToFold ?? Math.random() * 0.4
|
|
};
|
|
|
|
return params;
|
|
}
|
|
|
|
mutateParams(params: BenjolinParams, mutationAmount?: number, pitchLock?: PitchLock): BenjolinParams {
|
|
const mutated = { ...params };
|
|
|
|
// Determine mutation strength based on current "stability"
|
|
const stability = (params.crossMod1to2 + params.crossMod2to1) / 2 +
|
|
params.runglerChaos + params.evolutionDepth;
|
|
const mutAmount = mutationAmount ?? (stability > 1.5 ? 0.05 : stability < 0.5 ? 0.2 : 0.1);
|
|
|
|
// Helper for correlated mutations
|
|
const mutateValue = (value: number, min: number, max: number, correlation = 1): number => {
|
|
const delta = (Math.random() - 0.5) * mutAmount * (max - min) * correlation;
|
|
return Math.max(min, Math.min(max, value + delta));
|
|
};
|
|
|
|
// Decide mutation strategy
|
|
const strategy = Math.random();
|
|
|
|
if (strategy < 0.3) {
|
|
// Mutate frequency relationships
|
|
if (pitchLock?.enabled) {
|
|
mutated.osc1Freq = pitchLock.frequency;
|
|
const freqRatio = mutated.osc2Freq / params.osc1Freq;
|
|
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
|
|
} else {
|
|
const freqRatio = mutated.osc2Freq / mutated.osc1Freq;
|
|
mutated.osc1Freq = mutateValue(mutated.osc1Freq, 20, 2000);
|
|
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
|
|
}
|
|
|
|
// Correlate cross-mod amounts
|
|
const crossModDelta = (Math.random() - 0.5) * mutAmount;
|
|
mutated.crossMod1to2 = Math.max(0, Math.min(1, mutated.crossMod1to2 + crossModDelta));
|
|
mutated.crossMod2to1 = Math.max(0, Math.min(1, mutated.crossMod2to1 + crossModDelta * 0.7));
|
|
|
|
} else if (strategy < 0.6) {
|
|
// Mutate filter characteristics
|
|
mutated.filterCutoff = mutateValue(mutated.filterCutoff, 100, 4000);
|
|
mutated.filterResonance = mutateValue(mutated.filterResonance, 0, 0.95);
|
|
mutated.filterMode = mutateValue(mutated.filterMode, 0, 1);
|
|
|
|
// Correlate filter drive and feedback
|
|
if (Math.random() > 0.5) {
|
|
mutated.filterDrive = mutateValue(mutated.filterDrive, 0.5, 4);
|
|
mutated.filterFeedback = mutateValue(mutated.filterFeedback, 0, 0.7, 0.5);
|
|
}
|
|
|
|
} else if (strategy < 0.8) {
|
|
// Mutate evolution and chaos
|
|
mutated.evolutionRate = mutateValue(mutated.evolutionRate, 0, 0.5);
|
|
mutated.evolutionDepth = mutateValue(mutated.evolutionDepth, 0, 0.8);
|
|
mutated.chaosAttractorRate = mutateValue(mutated.chaosAttractorRate, 0, 0.3);
|
|
mutated.chaosAttractorDepth = mutateValue(mutated.chaosAttractorDepth, 0, 0.8);
|
|
|
|
// Maybe change rungler behavior
|
|
if (Math.random() > 0.7) {
|
|
mutated.runglerChaos = mutateValue(mutated.runglerChaos, 0, 1);
|
|
mutated.runglerFeedback = mutateValue(mutated.runglerFeedback, 0, 0.9);
|
|
}
|
|
|
|
} else {
|
|
// Mutate output characteristics
|
|
mutated.wavefoldAmount = mutateValue(mutated.wavefoldAmount, 0, 0.8);
|
|
mutated.distortionType = mutateValue(mutated.distortionType, 0, 1);
|
|
mutated.stereoWidth = mutateValue(mutated.stereoWidth, 0, 1);
|
|
|
|
// Adjust envelope for new sound
|
|
const envMutation = Math.random();
|
|
if (envMutation < 0.5) {
|
|
mutated.envelopeAttack = mutateValue(mutated.envelopeAttack, 0.001, 0.5);
|
|
mutated.envelopeDecay = mutateValue(mutated.envelopeDecay, 0.01, 0.5);
|
|
} else {
|
|
mutated.envelopeSustain = mutateValue(mutated.envelopeSustain, 0, 1);
|
|
mutated.envelopeRelease = mutateValue(mutated.envelopeRelease, 0.01, 0.8);
|
|
}
|
|
}
|
|
|
|
// Always apply small random mutations to 2-3 other parameters
|
|
const paramNames = Object.keys(params) as (keyof BenjolinParams)[];
|
|
const numExtraMutations = 2 + Math.floor(Math.random() * 2);
|
|
|
|
for (let i = 0; i < numExtraMutations; i++) {
|
|
const paramName = paramNames[Math.floor(Math.random() * paramNames.length)];
|
|
const currentValue = mutated[paramName];
|
|
|
|
if (typeof currentValue === 'number') {
|
|
// Apply subtle mutation based on parameter type
|
|
if (paramName.includes('Freq')) {
|
|
mutated[paramName] = mutateValue(currentValue, 20, 2000, 0.5) as any;
|
|
} else if (paramName.includes('envelope')) {
|
|
mutated[paramName] = mutateValue(currentValue, 0, 1, 0.3) as any;
|
|
} else {
|
|
mutated[paramName] = mutateValue(currentValue, 0, 1, 0.2) as any;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mutated;
|
|
}
|
|
} |