Files
rsgp/src/lib/audio/engines/Benjolin.ts

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;
}
}