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