595 lines
20 KiB
TypeScript
595 lines
20 KiB
TypeScript
import type { SynthEngine, PitchLock } from './SynthEngine';
|
|
|
|
enum EnvCurve {
|
|
Linear,
|
|
Exponential,
|
|
Logarithmic,
|
|
SCurve,
|
|
}
|
|
|
|
enum HarmonicSeriesType {
|
|
All,
|
|
OddOnly,
|
|
EvenOnly,
|
|
PrimeOnly,
|
|
}
|
|
|
|
enum DistributionStrategy {
|
|
FundamentalFocused,
|
|
OctaveStack,
|
|
FifthBased,
|
|
LowMidCluster,
|
|
HighShimmer,
|
|
SparseScatter,
|
|
DenseLow,
|
|
SubharmonicEmphasis,
|
|
BellCurve,
|
|
RandomWalk,
|
|
}
|
|
|
|
interface PartialParams {
|
|
ratio: number;
|
|
level: number;
|
|
attack: number;
|
|
decay: number;
|
|
sustain: number;
|
|
release: number;
|
|
attackCurve: EnvCurve;
|
|
decayCurve: EnvCurve;
|
|
releaseCurve: EnvCurve;
|
|
vibratoRate: number;
|
|
vibratoDepth: number;
|
|
tremoloRate: number;
|
|
tremoloDepth: number;
|
|
amplitudeLFORate: number;
|
|
amplitudeLFODepth: number;
|
|
frequencyDrift: number;
|
|
}
|
|
|
|
interface PrecomputedPartial {
|
|
partial: PartialParams;
|
|
evolutionOffsetSamples: number;
|
|
rolloff: number;
|
|
leftGain: number;
|
|
rightGain: number;
|
|
inharmonicRatio: number;
|
|
phaseIncrementL: number;
|
|
phaseIncrementR: number;
|
|
vibratoPhaseIncrement: number;
|
|
tremoloPhaseIncrement: number;
|
|
amplitudeLFOPhaseIncrement: number;
|
|
attackSamples: number;
|
|
decaySamples: number;
|
|
releaseSamples: number;
|
|
sustainStartSamples: number;
|
|
releaseStartSamples: number;
|
|
}
|
|
|
|
export interface AdditiveParams {
|
|
baseFreq: number;
|
|
partials: PartialParams[];
|
|
stereoWidth: number;
|
|
brightness: number;
|
|
inharmonicity: number;
|
|
harmonicSeriesType: HarmonicSeriesType;
|
|
distributionStrategy: DistributionStrategy;
|
|
phaseCoherence: number;
|
|
evolutionAmount: number;
|
|
panSpread: number;
|
|
}
|
|
|
|
export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
|
getName(): string {
|
|
return 'Prism';
|
|
}
|
|
|
|
getDescription(): string {
|
|
return 'Spectral synthesis with up to 50 harmonics, individual envelopes, amplitude LFOs, modulation, inharmonicity, and harmonic filtering';
|
|
}
|
|
|
|
getType() {
|
|
return 'generative' as const;
|
|
}
|
|
|
|
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
|
const numSamples = Math.floor(sampleRate * duration);
|
|
const leftBuffer = new Float32Array(numSamples);
|
|
const rightBuffer = new Float32Array(numSamples);
|
|
const TAU = Math.PI * 2;
|
|
|
|
const detune = 1 + (params.stereoWidth * 0.001);
|
|
const leftFreq = params.baseFreq / detune;
|
|
const rightFreq = params.baseFreq * detune;
|
|
|
|
const TAU_DIV_SR = TAU / sampleRate;
|
|
|
|
const precomputed: PrecomputedPartial[] = [];
|
|
const partialPhasesL: number[] = [];
|
|
const partialPhasesR: number[] = [];
|
|
const vibratoPhasesL: number[] = [];
|
|
const vibratoPhasesR: number[] = [];
|
|
const tremoloPhasesL: number[] = [];
|
|
const tremoloPhasesR: number[] = [];
|
|
const amplitudeLFOPhases: number[] = [];
|
|
const driftOffsetsL: number[] = [];
|
|
const driftOffsetsR: number[] = [];
|
|
|
|
for (let p = 0; p < params.partials.length; p++) {
|
|
const partial = params.partials[p];
|
|
|
|
if (!this.shouldIncludePartial(partial.ratio, params.harmonicSeriesType)) {
|
|
continue;
|
|
}
|
|
|
|
const partialPosition = p / Math.max(1, params.partials.length - 1);
|
|
const panPosition = (partialPosition * 2 - 1) * params.panSpread;
|
|
|
|
const inharmonicRatio = partial.ratio * (1 + params.inharmonicity * partial.ratio * partial.ratio * 0.001);
|
|
|
|
const maxFreqL = leftFreq * inharmonicRatio;
|
|
const maxFreqR = rightFreq * inharmonicRatio;
|
|
if (maxFreqL > 16000 || maxFreqR > 16000) {
|
|
continue;
|
|
}
|
|
|
|
const randomPhaseL = Math.random() * TAU;
|
|
const coherentPhaseL = (p * TAU * 0.1) % TAU;
|
|
partialPhasesL.push(randomPhaseL * (1 - params.phaseCoherence) + coherentPhaseL * params.phaseCoherence);
|
|
|
|
const randomPhaseR = Math.random() * TAU;
|
|
const coherentPhaseR = (p * TAU * 0.1) % TAU;
|
|
partialPhasesR.push(randomPhaseR * (1 - params.phaseCoherence) + coherentPhaseR * params.phaseCoherence);
|
|
|
|
vibratoPhasesL.push(0);
|
|
vibratoPhasesR.push(Math.random() * TAU * 0.1);
|
|
tremoloPhasesL.push(0);
|
|
tremoloPhasesR.push(Math.random() * TAU * 0.15);
|
|
amplitudeLFOPhases.push(Math.random() * TAU);
|
|
driftOffsetsL.push(0);
|
|
driftOffsetsR.push(0);
|
|
|
|
const position = p / Math.max(1, params.partials.length - 1);
|
|
const rolloff = 1 - (position * (1 - params.brightness));
|
|
|
|
precomputed.push({
|
|
partial,
|
|
evolutionOffsetSamples: partialPosition * params.evolutionAmount * duration * 0.3 * sampleRate,
|
|
rolloff,
|
|
leftGain: Math.cos((panPosition + 1) * Math.PI / 4),
|
|
rightGain: Math.sin((panPosition + 1) * Math.PI / 4),
|
|
inharmonicRatio,
|
|
phaseIncrementL: TAU_DIV_SR * leftFreq,
|
|
phaseIncrementR: TAU_DIV_SR * rightFreq,
|
|
vibratoPhaseIncrement: TAU_DIV_SR * partial.vibratoRate,
|
|
tremoloPhaseIncrement: TAU_DIV_SR * partial.tremoloRate,
|
|
amplitudeLFOPhaseIncrement: TAU_DIV_SR * partial.amplitudeLFORate,
|
|
attackSamples: partial.attack * duration * sampleRate,
|
|
decaySamples: partial.decay * duration * sampleRate,
|
|
releaseSamples: partial.release * duration * sampleRate,
|
|
sustainStartSamples: (partial.attack + partial.decay) * duration * sampleRate,
|
|
releaseStartSamples: (duration - partial.release * duration) * sampleRate,
|
|
});
|
|
}
|
|
|
|
const invNormFactor = 1.0 / Math.sqrt(precomputed.length);
|
|
let peakL = 0;
|
|
let peakR = 0;
|
|
|
|
for (let i = 0; i < numSamples; i++) {
|
|
let sumL = 0;
|
|
let sumR = 0;
|
|
|
|
for (let p = 0; p < precomputed.length; p++) {
|
|
const pc = precomputed[p];
|
|
const partial = pc.partial;
|
|
|
|
const adjustedSample = Math.max(0, i - pc.evolutionOffsetSamples);
|
|
const envelope = this.calculateEnvelopeFast(
|
|
adjustedSample,
|
|
pc.attackSamples,
|
|
pc.decaySamples,
|
|
pc.sustainStartSamples,
|
|
pc.releaseStartSamples,
|
|
pc.releaseSamples,
|
|
partial.sustain,
|
|
partial.attackCurve,
|
|
partial.decayCurve,
|
|
partial.releaseCurve
|
|
);
|
|
|
|
const amplitudeLFO = 1 + Math.sin(amplitudeLFOPhases[p]) * partial.amplitudeLFODepth;
|
|
|
|
driftOffsetsL[p] += (Math.random() * 2 - 1) * partial.frequencyDrift * 0.00001;
|
|
driftOffsetsR[p] += (Math.random() * 2 - 1) * partial.frequencyDrift * 0.00001;
|
|
driftOffsetsL[p] = Math.max(-partial.frequencyDrift * 0.005, Math.min(partial.frequencyDrift * 0.005, driftOffsetsL[p]));
|
|
driftOffsetsR[p] = Math.max(-partial.frequencyDrift * 0.005, Math.min(partial.frequencyDrift * 0.005, driftOffsetsR[p]));
|
|
|
|
const vibratoL = Math.sin(vibratoPhasesL[p]) * partial.vibratoDepth;
|
|
const vibratoR = Math.sin(vibratoPhasesR[p]) * partial.vibratoDepth;
|
|
|
|
const tremoloL = 1 + Math.sin(tremoloPhasesL[p]) * partial.tremoloDepth;
|
|
const tremoloR = 1 + Math.sin(tremoloPhasesR[p]) * partial.tremoloDepth;
|
|
|
|
const freqL = pc.inharmonicRatio * (1 + vibratoL + driftOffsetsL[p]);
|
|
const freqR = pc.inharmonicRatio * (1 + vibratoR + driftOffsetsR[p]);
|
|
|
|
const levelEnvRolloffL = partial.level * envelope * pc.rolloff * pc.leftGain * amplitudeLFO;
|
|
const levelEnvRolloffR = partial.level * envelope * pc.rolloff * pc.rightGain * amplitudeLFO;
|
|
|
|
const sampleL = Math.sin(partialPhasesL[p]) * levelEnvRolloffL * tremoloL;
|
|
const sampleR = Math.sin(partialPhasesR[p]) * levelEnvRolloffR * tremoloR;
|
|
|
|
sumL += sampleL;
|
|
sumR += sampleR;
|
|
|
|
partialPhasesL[p] += pc.phaseIncrementL * freqL;
|
|
partialPhasesR[p] += pc.phaseIncrementR * freqR;
|
|
vibratoPhasesL[p] += pc.vibratoPhaseIncrement;
|
|
vibratoPhasesR[p] += pc.vibratoPhaseIncrement;
|
|
tremoloPhasesL[p] += pc.tremoloPhaseIncrement;
|
|
tremoloPhasesR[p] += pc.tremoloPhaseIncrement;
|
|
amplitudeLFOPhases[p] += pc.amplitudeLFOPhaseIncrement;
|
|
|
|
if (partialPhasesL[p] > TAU) partialPhasesL[p] %= TAU;
|
|
if (partialPhasesR[p] > TAU) partialPhasesR[p] %= TAU;
|
|
}
|
|
|
|
const sampL = sumL * invNormFactor;
|
|
const sampR = sumR * invNormFactor;
|
|
|
|
leftBuffer[i] = sampL;
|
|
rightBuffer[i] = sampR;
|
|
|
|
const absL = Math.abs(sampL);
|
|
const absR = Math.abs(sampR);
|
|
if (absL > peakL) peakL = absL;
|
|
if (absR > peakR) peakR = absR;
|
|
}
|
|
|
|
const peak = Math.max(peakL, peakR);
|
|
|
|
if (peak > 0.001) {
|
|
const normalizeGain = 0.85 / peak;
|
|
for (let i = 0; i < numSamples; i++) {
|
|
leftBuffer[i] *= normalizeGain;
|
|
rightBuffer[i] *= normalizeGain;
|
|
}
|
|
}
|
|
|
|
return [leftBuffer, rightBuffer];
|
|
}
|
|
|
|
private calculateRolloff(partialIndex: number, totalPartials: number, brightness: number): number {
|
|
const position = partialIndex / (totalPartials - 1);
|
|
const rolloffAmount = 1 - brightness;
|
|
return 1 - (position * rolloffAmount);
|
|
}
|
|
|
|
private shouldIncludePartial(ratio: number, seriesType: HarmonicSeriesType): boolean {
|
|
if (seriesType === HarmonicSeriesType.All) {
|
|
return true;
|
|
}
|
|
|
|
if (ratio < 1) {
|
|
return true;
|
|
}
|
|
|
|
const roundedRatio = Math.round(ratio);
|
|
|
|
switch (seriesType) {
|
|
case HarmonicSeriesType.OddOnly:
|
|
return roundedRatio % 2 === 1 && roundedRatio > 0;
|
|
case HarmonicSeriesType.EvenOnly:
|
|
return roundedRatio % 2 === 0 && roundedRatio > 0;
|
|
case HarmonicSeriesType.PrimeOnly:
|
|
return this.isPrime(roundedRatio);
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private isPrime(n: number): boolean {
|
|
if (n < 2) return false;
|
|
if (n === 2) return true;
|
|
if (n % 2 === 0) return false;
|
|
for (let i = 3; i <= Math.sqrt(n); i += 2) {
|
|
if (n % i === 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private calculateEnvelope(t: number, duration: number, partial: PartialParams): number {
|
|
const attackTime = partial.attack * duration;
|
|
const decayTime = partial.decay * duration;
|
|
const releaseTime = partial.release * duration;
|
|
const sustainStart = attackTime + decayTime;
|
|
const releaseStart = duration - releaseTime;
|
|
|
|
if (t < attackTime) {
|
|
const progress = t / attackTime;
|
|
return this.applyCurve(progress, partial.attackCurve);
|
|
} else if (t < sustainStart) {
|
|
const progress = (t - attackTime) / decayTime;
|
|
const curvedProgress = this.applyCurve(progress, partial.decayCurve);
|
|
return 1 - curvedProgress * (1 - partial.sustain);
|
|
} else if (t < releaseStart) {
|
|
return partial.sustain;
|
|
} else {
|
|
const progress = (t - releaseStart) / releaseTime;
|
|
const curvedProgress = this.applyCurve(progress, partial.releaseCurve);
|
|
return partial.sustain * (1 - curvedProgress);
|
|
}
|
|
}
|
|
|
|
private applyCurve(progress: number, curve: EnvCurve): number {
|
|
switch (curve) {
|
|
case EnvCurve.Linear:
|
|
return progress;
|
|
case EnvCurve.Exponential:
|
|
return Math.pow(progress, 3);
|
|
case EnvCurve.Logarithmic:
|
|
return Math.pow(progress, 0.33);
|
|
case EnvCurve.SCurve:
|
|
return (Math.sin((progress - 0.5) * Math.PI) + 1) / 2;
|
|
default:
|
|
return progress;
|
|
}
|
|
}
|
|
|
|
private applyCurveFast(progress: number, curve: EnvCurve): number {
|
|
switch (curve) {
|
|
case EnvCurve.Linear:
|
|
return progress;
|
|
case EnvCurve.Exponential:
|
|
return progress * progress * progress;
|
|
case EnvCurve.Logarithmic:
|
|
return Math.cbrt(progress);
|
|
case EnvCurve.SCurve:
|
|
const x = (progress - 0.5) * Math.PI;
|
|
return (Math.sin(x) + 1) * 0.5;
|
|
default:
|
|
return progress;
|
|
}
|
|
}
|
|
|
|
private calculateEnvelopeFast(
|
|
sample: number,
|
|
attackSamples: number,
|
|
decaySamples: number,
|
|
sustainStartSamples: number,
|
|
releaseStartSamples: number,
|
|
releaseSamples: number,
|
|
sustain: number,
|
|
attackCurve: EnvCurve,
|
|
decayCurve: EnvCurve,
|
|
releaseCurve: EnvCurve
|
|
): number {
|
|
if (sample < attackSamples) {
|
|
const progress = sample / attackSamples;
|
|
return this.applyCurveFast(progress, attackCurve);
|
|
} else if (sample < sustainStartSamples) {
|
|
const progress = (sample - attackSamples) / decaySamples;
|
|
const curvedProgress = this.applyCurveFast(progress, decayCurve);
|
|
return 1 - curvedProgress * (1 - sustain);
|
|
} else if (sample < releaseStartSamples) {
|
|
return sustain;
|
|
} else {
|
|
const progress = (sample - releaseStartSamples) / releaseSamples;
|
|
const curvedProgress = this.applyCurveFast(progress, releaseCurve);
|
|
return sustain * (1 - curvedProgress);
|
|
}
|
|
}
|
|
|
|
randomParams(pitchLock?: PitchLock): AdditiveParams {
|
|
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
|
const baseFreq = pitchLock?.enabled
|
|
? pitchLock.frequency
|
|
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
|
|
|
const harmonicSeriesType = this.randomInt(0, 3) as HarmonicSeriesType;
|
|
const distributionStrategy = this.randomInt(0, 9) as DistributionStrategy;
|
|
const ratios = this.generateRatiosForStrategy(distributionStrategy);
|
|
|
|
const numPartials = Math.min(ratios.length, this.randomInt(Math.min(4, ratios.length), ratios.length));
|
|
const partials: PartialParams[] = [];
|
|
|
|
for (let i = 0; i < numPartials; i++) {
|
|
partials.push(this.randomPartial(i, ratios, distributionStrategy));
|
|
}
|
|
|
|
return {
|
|
baseFreq,
|
|
partials,
|
|
stereoWidth: this.randomRange(0.2, 0.8),
|
|
brightness: this.randomRange(0.3, 0.9),
|
|
inharmonicity: this.randomRange(0, 0.7),
|
|
harmonicSeriesType,
|
|
distributionStrategy,
|
|
phaseCoherence: this.randomRange(0, 1),
|
|
evolutionAmount: this.randomRange(0, 0.8),
|
|
panSpread: this.randomRange(0, 0.7),
|
|
};
|
|
}
|
|
|
|
private generateRatiosForStrategy(strategy: DistributionStrategy): number[] {
|
|
switch (strategy) {
|
|
case DistributionStrategy.FundamentalFocused:
|
|
return [1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 6, 7, 8];
|
|
|
|
case DistributionStrategy.OctaveStack:
|
|
return [0.5, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24];
|
|
|
|
case DistributionStrategy.FifthBased: {
|
|
const ratios = [1];
|
|
let current = 1;
|
|
for (let i = 0; i < 12; i++) {
|
|
current *= 1.5;
|
|
if (current > 32) current /= 2;
|
|
ratios.push(current);
|
|
}
|
|
return ratios.sort((a, b) => a - b);
|
|
}
|
|
|
|
case DistributionStrategy.LowMidCluster:
|
|
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
|
|
|
case DistributionStrategy.HighShimmer:
|
|
return [8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32];
|
|
|
|
case DistributionStrategy.SparseScatter:
|
|
return [1, 3, 7, 11, 17, 23, 29];
|
|
|
|
case DistributionStrategy.DenseLow:
|
|
return [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6];
|
|
|
|
case DistributionStrategy.SubharmonicEmphasis:
|
|
return [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4];
|
|
|
|
case DistributionStrategy.BellCurve:
|
|
return [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
|
|
|
case DistributionStrategy.RandomWalk: {
|
|
const start = this.randomInt(1, 8);
|
|
const ratios = [];
|
|
let current = start;
|
|
for (let i = 0; i < 15; i++) {
|
|
ratios.push(current);
|
|
current += Math.random() < 0.5 ? 1 : 2;
|
|
if (current > 32) break;
|
|
}
|
|
return ratios;
|
|
}
|
|
|
|
default:
|
|
return [1, 2, 3, 4, 5, 6, 7, 8];
|
|
}
|
|
}
|
|
|
|
private randomPartial(index: number, ratios: number[], strategy: DistributionStrategy): PartialParams {
|
|
const ratio = this.randomChoice(ratios) * this.randomRange(0.998, 1.002);
|
|
|
|
let levelDecay: number;
|
|
switch (strategy) {
|
|
case DistributionStrategy.FundamentalFocused:
|
|
levelDecay = Math.pow(0.65, index * 0.6);
|
|
break;
|
|
case DistributionStrategy.HighShimmer:
|
|
levelDecay = Math.pow(0.88, index * 0.2);
|
|
break;
|
|
case DistributionStrategy.SparseScatter:
|
|
levelDecay = Math.pow(0.85, index * 0.3);
|
|
break;
|
|
case DistributionStrategy.DenseLow:
|
|
levelDecay = Math.pow(0.92, index * 0.4);
|
|
break;
|
|
case DistributionStrategy.SubharmonicEmphasis:
|
|
levelDecay = Math.pow(0.82, Math.abs(index - 3) * 0.5);
|
|
break;
|
|
case DistributionStrategy.BellCurve:
|
|
const centerDist = Math.abs(index - ratios.length / 2);
|
|
levelDecay = Math.pow(0.75, centerDist * 0.5);
|
|
break;
|
|
default:
|
|
levelDecay = Math.pow(0.8, index * 0.4);
|
|
}
|
|
|
|
const baseLevel = this.randomRange(0.3, 1.0);
|
|
const level = baseLevel * levelDecay;
|
|
|
|
return {
|
|
ratio,
|
|
level,
|
|
attack: this.randomRange(0.001, 0.15),
|
|
decay: this.randomRange(0.02, 0.25),
|
|
sustain: this.randomRange(0.2, 0.8),
|
|
release: this.randomRange(0.05, 0.4),
|
|
attackCurve: this.randomInt(0, 3) as EnvCurve,
|
|
decayCurve: this.randomInt(0, 3) as EnvCurve,
|
|
releaseCurve: this.randomInt(0, 3) as EnvCurve,
|
|
vibratoRate: this.randomRange(2, 8),
|
|
vibratoDepth: this.randomRange(0, 0.015),
|
|
tremoloRate: this.randomRange(1, 6),
|
|
tremoloDepth: this.randomRange(0, 0.25),
|
|
amplitudeLFORate: this.randomRange(0.1, 2),
|
|
amplitudeLFODepth: this.randomRange(0, 0.6),
|
|
frequencyDrift: this.randomRange(0, 1),
|
|
};
|
|
}
|
|
|
|
mutateParams(params: AdditiveParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): AdditiveParams {
|
|
const newHarmonicSeriesType = Math.random() < 0.08 ? this.randomInt(0, 3) as HarmonicSeriesType : params.harmonicSeriesType;
|
|
const newDistributionStrategy = Math.random() < 0.08 ? this.randomInt(0, 9) as DistributionStrategy : params.distributionStrategy;
|
|
const ratios = this.generateRatiosForStrategy(newDistributionStrategy);
|
|
|
|
const shouldChangePartialCount = Math.random() < 0.1;
|
|
let newPartials = params.partials.map(p => this.mutatePartial(p, mutationAmount, ratios));
|
|
|
|
if (shouldChangePartialCount) {
|
|
if (Math.random() < 0.5 && newPartials.length < 50) {
|
|
newPartials.push(this.randomPartial(newPartials.length, ratios, newDistributionStrategy));
|
|
} else if (newPartials.length > 3) {
|
|
newPartials = newPartials.slice(0, -1);
|
|
}
|
|
}
|
|
|
|
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
|
|
|
return {
|
|
baseFreq,
|
|
partials: newPartials,
|
|
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
|
|
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
|
inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 1),
|
|
harmonicSeriesType: newHarmonicSeriesType,
|
|
distributionStrategy: newDistributionStrategy,
|
|
phaseCoherence: this.mutateValue(params.phaseCoherence, mutationAmount, 0, 1),
|
|
evolutionAmount: this.mutateValue(params.evolutionAmount, mutationAmount, 0, 1),
|
|
panSpread: this.mutateValue(params.panSpread, mutationAmount, 0, 1),
|
|
};
|
|
}
|
|
|
|
private mutatePartial(partial: PartialParams, amount: number, ratios: number[]): PartialParams {
|
|
let newRatio = partial.ratio;
|
|
|
|
if (Math.random() < 0.1) {
|
|
newRatio = this.randomChoice(ratios) * this.randomRange(0.998, 1.002);
|
|
}
|
|
|
|
return {
|
|
ratio: newRatio,
|
|
level: this.mutateValue(partial.level, amount, 0.05, 1.0),
|
|
attack: this.mutateValue(partial.attack, amount, 0.001, 0.25),
|
|
decay: this.mutateValue(partial.decay, amount, 0.01, 0.4),
|
|
sustain: this.mutateValue(partial.sustain, amount, 0.1, 0.9),
|
|
release: this.mutateValue(partial.release, amount, 0.02, 0.6),
|
|
attackCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.attackCurve,
|
|
decayCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.decayCurve,
|
|
releaseCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.releaseCurve,
|
|
vibratoRate: this.mutateValue(partial.vibratoRate, amount, 1, 15),
|
|
vibratoDepth: this.mutateValue(partial.vibratoDepth, amount, 0, 0.03),
|
|
tremoloRate: this.mutateValue(partial.tremoloRate, amount, 0.5, 10),
|
|
tremoloDepth: this.mutateValue(partial.tremoloDepth, amount, 0, 0.4),
|
|
amplitudeLFORate: this.mutateValue(partial.amplitudeLFORate, amount, 0.1, 3),
|
|
amplitudeLFODepth: this.mutateValue(partial.amplitudeLFODepth, amount, 0, 0.8),
|
|
frequencyDrift: this.mutateValue(partial.frequencyDrift, amount, 0, 1),
|
|
};
|
|
}
|
|
|
|
private randomRange(min: number, max: number): number {
|
|
return min + Math.random() * (max - min);
|
|
}
|
|
|
|
private randomInt(min: number, max: number): number {
|
|
return Math.floor(this.randomRange(min, max + 1));
|
|
}
|
|
|
|
private randomChoice<T>(choices: readonly T[]): T {
|
|
return choices[Math.floor(Math.random() * choices.length)];
|
|
}
|
|
|
|
private mutateValue(value: number, amount: number, min: number, max: number): number {
|
|
const variation = value * amount * (Math.random() * 2 - 1);
|
|
return Math.max(min, Math.min(max, value + variation));
|
|
}
|
|
}
|