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