more engines

This commit is contained in:
2025-10-12 11:04:54 +02:00
parent 94a36b1a29
commit 7b99dc0f0d
4371 changed files with 2187 additions and 92 deletions

View File

@ -41,7 +41,9 @@
let isRecording = false;
let isDragOver = false;
const allProcessors = getAllProcessors();
const allProcessors = getAllProcessors().sort((a, b) =>
a.getName().localeCompare(b.getName())
);
$: showDuration = engineType !== 'sample';
$: showRandomButton = engineType === 'generative';

View File

@ -0,0 +1,590 @@
import type { SynthEngine } 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(): AdditiveParams {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
const baseFreq = 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): 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);
}
}
return {
baseFreq: params.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));
}
}

View File

@ -467,6 +467,20 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
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];
}

View File

@ -233,6 +233,23 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
lfoPhaseR = (lfoPhaseR + lfoIncrement) % TAU;
}
// Peak normalization with headroom
let peakL = 0;
let peakR = 0;
for (let i = 0; i < numSamples; i++) {
peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
}
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];
}

View File

@ -189,6 +189,23 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
}
// Peak normalization with headroom
let peakL = 0;
let peakR = 0;
for (let i = 0; i < numSamples; i++) {
peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
}
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];
}

View File

@ -28,7 +28,7 @@ interface KarplusStrongParams {
export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
getName(): string {
return 'Karplus-Strong';
return 'String(s)';
}
getDescription(): string {

View File

@ -51,50 +51,128 @@ export class NoiseDrum implements SynthEngine {
}
randomParams(): NoiseDrumParams {
// Intelligently bias parameter ranges to create diverse percussion types
const filterBias = Math.random();
// Intelligent parameter generation based on correlated characteristics
// Choose a frequency range that defines the drum character
const freqBias = Math.random();
let filterFreq: number;
let noiseColor: number;
let pitchAmount: number;
let bodyAmount: number;
if (freqBias < 0.25) {
// Low frequency - kick/bass territory
filterFreq = 0.05 + Math.random() * 0.12;
noiseColor = Math.random() * 0.3;
pitchAmount = 0.15 + Math.random() * 0.2;
bodyAmount = 0.25 + Math.random() * 0.35;
} else if (freqBias < 0.5) {
// Mid frequency - tom/snare territory
filterFreq = 0.2 + Math.random() * 0.35;
noiseColor = 0.3 + Math.random() * 0.4;
pitchAmount = 0.05 + Math.random() * 0.2;
bodyAmount = 0.15 + Math.random() * 0.3;
} else if (freqBias < 0.75) {
// High frequency - hi-hat/ride territory
filterFreq = 0.6 + Math.random() * 0.3;
noiseColor = 0.65 + Math.random() * 0.35;
pitchAmount = Math.random() * 0.08;
bodyAmount = 0.1 + Math.random() * 0.2;
} else {
// Very high - cymbal/crash territory
filterFreq = 0.75 + Math.random() * 0.2;
noiseColor = 0.8 + Math.random() * 0.2;
pitchAmount = 0;
bodyAmount = 0.15 + Math.random() * 0.25;
}
// Q correlates with frequency - higher freq = higher Q acceptable
const filterQ = 0.3 + Math.random() * 0.4 + filterFreq * 0.3;
// Filter type based on frequency range
const filterType = filterFreq < 0.3 ?
Math.random() * 0.35 : // Low freq prefers lowpass
filterFreq > 0.7 ?
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
Math.random(); // Mid freq - any type
// Decay time inversely correlates with frequency
const decayBias = Math.random();
const tonalBias = Math.random();
const ampDecay = filterFreq < 0.3 ?
0.25 + decayBias * 0.4 : // Low freq can be longer
filterFreq > 0.6 ?
0.08 + decayBias * 0.35 : // High freq shorter
0.2 + decayBias * 0.45; // Mid range
// Attack is generally very short for percussion
const ampAttack = Math.random() < 0.85 ?
Math.random() * 0.008 : // Most drums: instant attack
Math.random() * 0.02; // Some: slight attack
// Punch correlates with attack - shorter attack = more punch available
const ampPunch = ampAttack < 0.01 ?
Math.random() * 0.7 :
Math.random() * 0.4;
// Filter envelope amount and speed
const filterEnvAmount = 0.1 + Math.random() * 0.4;
const filterEnvSpeed = 0.08 + Math.random() * 0.35;
// Burst pattern (for claps/complex sounds)
const useBurst = Math.random() < 0.15; // 15% chance of burst
const noiseBurst = useBurst ? 0.5 + Math.random() * 0.5 : 0;
const burstCount = useBurst ? 0.3 + Math.random() * 0.7 : 0;
// Pitch characteristics
const pitchStart = pitchAmount > 0.1 ?
0.2 + Math.random() * 0.6 : // If pitched, varied start freq
0.5; // If not pitched, doesn't matter
const pitchDecay = pitchAmount > 0.1 ?
0.03 + Math.random() * 0.12 :
0.05;
// Body resonance frequency should relate to filter frequency
const bodyFreq = filterFreq * (0.8 + Math.random() * 0.4);
const bodyDecay = 0.2 + Math.random() * 0.4;
// Noise modulation more common on mid-high frequencies
const useNoiseMod = filterFreq > 0.4 && Math.random() < 0.5;
const noiseMod = useNoiseMod ? 0.15 + Math.random() * 0.4 : Math.random() * 0.15;
const noiseModRate = useNoiseMod ? 0.3 + Math.random() * 0.7 : Math.random();
// Stereo spread - more on high frequencies
const stereoSpread = filterFreq > 0.5 ?
0.15 + Math.random() * 0.3 :
Math.random() * 0.2;
// Drive - more on low frequencies for punch
const drive = filterFreq < 0.3 ?
0.2 + Math.random() * 0.35 :
Math.random() * 0.3;
return {
// Noise characteristics - varied colors and burst patterns
noiseColor: Math.random(),
noiseBurst: Math.random() * 0.7, // probability of burst pattern
burstCount: Math.random(), // 1-4 bursts
// Filter section - wide range from sub to high frequencies
filterFreq: filterBias < 0.3 ? Math.random() * 0.25 : // bass drum range
filterBias < 0.6 ? 0.25 + Math.random() * 0.35 : // snare/tom range
0.6 + Math.random() * 0.4, // hi-hat/cymbal range
filterQ: Math.random() * 0.85,
filterType: Math.random(), // lowpass, bandpass, highpass blend
filterEnvAmount: Math.random() * 0.7, // reduced from 0.9
filterEnvSpeed: 0.1 + Math.random() * 0.4, // min increased from 0.05
// Amplitude envelope - SHORT ATTACK for percussion character
ampAttack: Math.random() < 0.8 ? Math.random() * 0.03 : Math.random() * 0.08, // max 8%, not 25%
ampDecay: decayBias < 0.3 ? 0.15 + Math.random() * 0.25 : // short (hi-hat) - min increased
decayBias < 0.7 ? 0.35 + Math.random() * 0.25 : // medium (snare)
0.55 + Math.random() * 0.35, // long (cymbal/tom) - ensures sound continues
ampPunch: Math.random() * 0.7, // initial transient boost
// Pitch section - REDUCED for subtle tonal accent, not dominant tone
pitchAmount: tonalBias > 0.6 ? Math.random() * 0.35 : Math.random() * 0.15, // max 0.35, not 0.7
pitchStart: 0.3 + Math.random() * 0.7, // start higher in range (200-800Hz)
pitchDecay: 0.03 + Math.random() * 0.12, // slightly faster decay
// Body resonance - adds character and depth
bodyFreq: Math.random(),
bodyDecay: 0.2 + Math.random() * 0.45, // min increased
bodyAmount: Math.random() * 0.5, // reduced from 0.6
// Noise modulation - rhythmic variation
noiseMod: Math.random() * 0.5, // reduced from 0.6
noiseModRate: Math.random(),
// Stereo and character
stereoSpread: Math.random() * 0.4, // reduced from 0.45
drive: Math.random() * 0.45 // reduced from 0.5
noiseColor,
noiseBurst,
burstCount,
filterFreq: Math.max(0, Math.min(1, filterFreq)),
filterQ: Math.max(0, Math.min(1, filterQ)),
filterType: Math.max(0, Math.min(1, filterType)),
filterEnvAmount,
filterEnvSpeed,
ampAttack,
ampDecay,
ampPunch,
pitchAmount: Math.max(0, Math.min(0.4, pitchAmount)),
pitchStart,
pitchDecay,
bodyFreq: Math.max(0, Math.min(1, bodyFreq)),
bodyDecay,
bodyAmount: Math.max(0, Math.min(0.6, bodyAmount)),
noiseMod,
noiseModRate,
stereoSpread,
drive
};
}
@ -145,8 +223,8 @@ export class NoiseDrum implements SynthEngine {
// Filter frequency range: 40Hz to 10kHz (reduced from 12kHz for less harshness)
const baseFilterFreq = 40 + params.filterFreq * 9960;
// Q range: 0.5 to 10 (reduced from 12 for stability)
const filterQ = 0.5 + params.filterQ * 9.5;
// Q range: 0.5 to 6 (reduced further for stability)
const filterQ = 0.5 + params.filterQ * 5.5;
// Pitch envelope: SWEEP DOWN from high to low (classic 808/909 style)
const pitchStartFreq = 60 + params.pitchStart * 540; // 60Hz to 600Hz (not 800Hz)
@ -283,7 +361,7 @@ export class NoiseDrum implements SynthEngine {
const bodyFiltered = this.stateVariableFilter(
sample,
bodyFreq * spreadFactor,
6 + params.bodyAmount * 10, // reduced resonance range
4 + params.bodyAmount * 6, // reduced resonance range further for stability
sampleRate,
bodyState1,
bodyState2
@ -312,8 +390,22 @@ export class NoiseDrum implements SynthEngine {
dcBlockerY = dcBlocked.y;
sample = dcBlocked.output;
// Final output scaling
output[i] = this.softClip(sample * 0.95);
// Final output scaling with soft clipping
output[i] = this.softClip(sample * 0.85);
}
}
// 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;
}
}
@ -380,14 +472,15 @@ export class NoiseDrum implements SynthEngine {
const normalizedFreq = Math.min(cutoff / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const q = Math.max(1 / Math.min(resonance, 15), 0.01);
const q = Math.max(1 / Math.min(resonance, 10), 0.02);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - q * state1;
const bandpass = f * highpass + state1;
const newState1 = Math.abs(bandpass) > 1e-10 ? bandpass : 0;
const newState2 = Math.abs(lowpass) > 1e-10 ? lowpass : 0;
// Clamp filter states to prevent explosion
const newState1 = Math.max(-2, Math.min(2, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
const newState2 = Math.max(-2, Math.min(2, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
return {
output: bandpass,
@ -408,14 +501,15 @@ export class NoiseDrum implements SynthEngine {
const normalizedFreq = Math.min(cutoff / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const q = Math.max(1 / Math.min(resonance, 15), 0.01);
const q = Math.max(1 / Math.min(resonance, 10), 0.02);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - q * state1;
const bandpass = f * highpass + state1;
const newState1 = Math.abs(bandpass) > 1e-10 ? bandpass : 0;
const newState2 = Math.abs(lowpass) > 1e-10 ? lowpass : 0;
// Clamp filter states to prevent explosion
const newState1 = Math.max(-2, Math.min(2, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
const newState2 = Math.max(-2, Math.min(2, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
// Blend between filter types based on filterType parameter
let output: number;

View File

@ -71,7 +71,7 @@ export class Ring implements SynthEngine<RingParams> {
private dcBlockerY1R = 0;
getName(): string {
return 'Ring';
return 'Creepy';
}
getDescription(): string {

View File

@ -0,0 +1,513 @@
import type { SynthEngine } from './SynthEngine';
enum EnvCurve {
Linear,
Exponential,
Logarithmic,
SCurve,
}
enum LFOWaveform {
Sine,
Triangle,
Square,
Saw,
SampleHold,
RandomWalk,
}
enum Algorithm {
Cascade, // 1→2 (classic FM)
Parallel, // 1+2 (additive)
Feedback, // 1→1→2 (self-modulating)
}
interface OperatorParams {
ratio: number;
level: number;
attack: number;
decay: number;
sustain: number;
release: number;
attackCurve: EnvCurve;
decayCurve: EnvCurve;
releaseCurve: EnvCurve;
}
interface LFOParams {
rate: number;
depth: number;
waveform: LFOWaveform;
target: 'pitch' | 'amplitude' | 'modIndex';
}
export interface TwoOpFMParams {
baseFreq: number;
algorithm: Algorithm;
operators: [OperatorParams, OperatorParams];
lfo: LFOParams;
feedback: number;
modIndex: number;
subOscLevel: number;
stereoWidth: number;
}
export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
private lfoSampleHoldValue = 0;
private lfoSampleHoldPhase = 0;
private lfoRandomWalkCurrent = 0;
private lfoRandomWalkTarget = 0;
private dcBlockerL = 0;
private dcBlockerR = 0;
private readonly dcBlockerCutoff = 0.995;
getName(): string {
return '2-OP FM';
}
getDescription(): string {
return 'Classic two-operator FM synthesis with musical ratios and multiple algorithms';
}
getType() {
return 'generative' as const;
}
generate(params: TwoOpFMParams, 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 opPhasesL = [0, Math.PI * params.stereoWidth * 0.1];
const opPhasesR = [0, Math.PI * params.stereoWidth * 0.15];
let subPhaseL = 0;
let subPhaseR = Math.PI * params.stereoWidth * 0.05;
let lfoPhaseL = 0;
let lfoPhaseR = Math.PI * params.stereoWidth * 0.3;
this.lfoSampleHoldValue = Math.random() * 2 - 1;
this.lfoSampleHoldPhase = 0;
this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
let feedbackSampleL = 0;
let feedbackSampleR = 0;
const gainCompensation = this.getAlgorithmGainCompensation(params.algorithm);
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const env1 = this.calculateEnvelope(t, duration, params.operators[0]);
const env2 = this.calculateEnvelope(t, duration, params.operators[1]);
const lfoL = this.generateLFO(lfoPhaseL, params.lfo.waveform, params.lfo.rate, sampleRate);
const lfoR = this.generateLFO(lfoPhaseR, params.lfo.waveform, params.lfo.rate, sampleRate);
const lfoModL = lfoL * params.lfo.depth;
const lfoModR = lfoR * params.lfo.depth;
let pitchModL = 0, pitchModR = 0;
let ampModL = 1, ampModR = 1;
let modIndexMod = 0;
if (params.lfo.target === 'pitch') {
pitchModL = lfoModL * 0.02;
pitchModR = lfoModR * 0.02;
} else if (params.lfo.target === 'amplitude') {
ampModL = 1 + lfoModL * 0.5;
ampModR = 1 + lfoModR * 0.5;
} else {
modIndexMod = lfoModL;
}
const [sampleL, sampleR] = this.processAlgorithm(
params.algorithm,
params.operators,
opPhasesL,
opPhasesR,
[env1, env2],
feedbackSampleL,
feedbackSampleR,
params.feedback,
params.modIndex,
modIndexMod
);
const subOscGainCompensation = 1 / (1 + params.subOscLevel * 0.5);
let outL = sampleL * gainCompensation * ampModL * subOscGainCompensation;
let outR = sampleR * gainCompensation * ampModR * subOscGainCompensation;
if (params.subOscLevel > 0) {
const subL = Math.sin(subPhaseL) * env2 * params.subOscLevel * subOscGainCompensation;
const subR = Math.sin(subPhaseR) * env2 * params.subOscLevel * subOscGainCompensation;
outL += subL;
outR += subR;
}
outL = this.softClip(outL);
outR = this.softClip(outR);
const dcFilteredL = outL - this.dcBlockerL;
this.dcBlockerL += (1 - this.dcBlockerCutoff) * dcFilteredL;
const dcFilteredR = outR - this.dcBlockerR;
this.dcBlockerR += (1 - this.dcBlockerCutoff) * dcFilteredR;
leftBuffer[i] = dcFilteredL;
rightBuffer[i] = dcFilteredR;
feedbackSampleL = outL;
feedbackSampleR = outR;
for (let op = 0; op < 2; op++) {
const opFreqL = leftFreq * params.operators[op].ratio * (1 + pitchModL);
const opFreqR = rightFreq * params.operators[op].ratio * (1 + pitchModR);
opPhasesL[op] += (TAU * opFreqL) / sampleRate;
opPhasesR[op] += (TAU * opFreqR) / sampleRate;
if (opPhasesL[op] > TAU * 1000) opPhasesL[op] -= TAU * 1000;
if (opPhasesR[op] > TAU * 1000) opPhasesR[op] -= TAU * 1000;
}
const subFreqL = leftFreq * 0.5 * (1 + pitchModL);
const subFreqR = rightFreq * 0.5 * (1 + pitchModR);
subPhaseL += (TAU * subFreqL) / sampleRate;
subPhaseR += (TAU * subFreqR) / sampleRate;
if (subPhaseL > TAU * 1000) subPhaseL -= TAU * 1000;
if (subPhaseR > TAU * 1000) subPhaseR -= TAU * 1000;
lfoPhaseL += (TAU * params.lfo.rate) / sampleRate;
lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
}
let peakL = 0;
let peakR = 0;
for (let i = 0; i < numSamples; i++) {
peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
}
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 processAlgorithm(
algorithm: Algorithm,
operators: [OperatorParams, OperatorParams],
phasesL: number[],
phasesR: number[],
envelopes: number[],
feedbackL: number,
feedbackR: number,
feedbackAmount: number,
modIndex: number,
modIndexMod: number
): [number, number] {
const effectiveModIndex = modIndex * (1 + modIndexMod * 2);
switch (algorithm) {
case Algorithm.Cascade: {
// 1→2 - Classic FM synthesis
const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
const outL = Math.sin(phasesL[1] + effectiveModIndex * mod1L) * envelopes[1] * operators[1].level;
const outR = Math.sin(phasesR[1] + effectiveModIndex * mod1R) * envelopes[1] * operators[1].level;
return [outL, outR];
}
case Algorithm.Parallel: {
// 1+2 - Additive synthesis
const osc1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
const osc1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
const osc2L = Math.sin(phasesL[1]) * envelopes[1] * operators[1].level;
const osc2R = Math.sin(phasesR[1]) * envelopes[1] * operators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case Algorithm.Feedback: {
// 1→1→2 - Self-modulating FM
const fbAmountScaled = Math.min(feedbackAmount * 0.8, 1.5);
const mod1L = Math.sin(phasesL[0] + fbAmountScaled * this.softClip(feedbackL * 2)) * envelopes[0] * operators[0].level;
const mod1R = Math.sin(phasesR[0] + fbAmountScaled * this.softClip(feedbackR * 2)) * envelopes[0] * operators[0].level;
const outL = Math.sin(phasesL[1] + effectiveModIndex * mod1L) * envelopes[1] * operators[1].level;
const outR = Math.sin(phasesR[1] + effectiveModIndex * mod1R) * envelopes[1] * operators[1].level;
return [outL, outR];
}
default:
return [0, 0];
}
}
private getAlgorithmGainCompensation(algorithm: Algorithm): number {
switch (algorithm) {
case Algorithm.Cascade:
case Algorithm.Feedback:
return 0.8;
case Algorithm.Parallel:
return 0.7;
default:
return 0.75;
}
}
private softClip(x: number): number {
const absX = Math.abs(x);
if (absX < 0.7) return x;
if (absX > 3) return Math.sign(x) * 0.98;
const x2 = x * x;
return x * (27 + x2) / (27 + 9 * x2);
}
private calculateEnvelope(t: number, duration: number, op: OperatorParams): number {
const attackTime = op.attack * duration;
const decayTime = op.decay * duration;
const releaseTime = op.release * duration;
const sustainStart = attackTime + decayTime;
const releaseStart = duration - releaseTime;
if (t < attackTime) {
const progress = t / attackTime;
return this.applyCurve(progress, op.attackCurve);
} else if (t < sustainStart) {
const progress = (t - attackTime) / decayTime;
const curvedProgress = this.applyCurve(progress, op.decayCurve);
return 1 - curvedProgress * (1 - op.sustain);
} else if (t < releaseStart) {
return op.sustain;
} else {
const progress = (t - releaseStart) / releaseTime;
const curvedProgress = this.applyCurve(progress, op.releaseCurve);
return op.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 generateLFO(phase: number, waveform: LFOWaveform, rate: number, sampleRate: number): number {
const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
switch (waveform) {
case LFOWaveform.Sine:
return Math.sin(phase);
case LFOWaveform.Triangle:
return normalizedPhase < 0.5
? normalizedPhase * 4 - 1
: 3 - normalizedPhase * 4;
case LFOWaveform.Square:
return normalizedPhase < 0.5 ? 1 : -1;
case LFOWaveform.Saw:
return normalizedPhase * 2 - 1;
case LFOWaveform.SampleHold: {
const cyclesSinceLastHold = phase - this.lfoSampleHoldPhase;
if (cyclesSinceLastHold >= Math.PI * 2) {
this.lfoSampleHoldValue = Math.random() * 2 - 1;
this.lfoSampleHoldPhase = phase;
}
return this.lfoSampleHoldValue;
}
case LFOWaveform.RandomWalk: {
const interpolationSpeed = rate / sampleRate * 20;
const diff = this.lfoRandomWalkTarget - this.lfoRandomWalkCurrent;
this.lfoRandomWalkCurrent += diff * interpolationSpeed;
if (Math.abs(diff) < 0.01) {
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
}
return this.lfoRandomWalkCurrent;
}
default:
return 0;
}
}
randomParams(): TwoOpFMParams {
const algorithm = this.randomInt(0, 2) as Algorithm;
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
return {
baseFreq,
algorithm,
operators: [
this.randomOperator(false, algorithm),
this.randomOperator(true, algorithm),
],
lfo: {
rate: this.randomRange(0.1, 10),
depth: this.randomRange(0, 0.4),
waveform: this.randomInt(0, 5) as LFOWaveform,
target: this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const),
},
feedback: this.randomRange(0, 1.2),
modIndex: this.randomRange(0.5, 8),
subOscLevel: this.randomRange(0, 0.8),
stereoWidth: this.randomRange(0.2, 0.7),
};
}
private randomOperator(isCarrier: boolean, algorithm: Algorithm): OperatorParams {
// Musical frequency ratios for 2-OP FM
// These are carefully chosen for tonal, harmonic sounds
const carrierRatios = [0.5, 1, 2, 3, 4];
const modulatorRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8];
// DX7-inspired ratios for specific timbres
const electricPianoRatios = [1, 14]; // Classic EP sound
const bellRatios = [1, 1.4, 3.5, 14]; // Bell-like tones
const brassRatios = [1, 1, 2, 3]; // Brass-like tones
let ratio: number;
if (algorithm === Algorithm.Parallel) {
// For additive, use harmonic ratios
ratio = this.randomChoice([1, 2, 3, 4, 5, 6, 8]);
} else if (isCarrier) {
// Carrier typically at fundamental or low harmonic
ratio = this.randomChoice(carrierRatios);
} else {
// Modulator can be more varied
const specialChance = Math.random();
if (specialChance < 0.2) {
ratio = this.randomChoice(electricPianoRatios);
} else if (specialChance < 0.4) {
ratio = this.randomChoice(bellRatios);
} else if (specialChance < 0.6) {
ratio = this.randomChoice(brassRatios);
} else {
ratio = this.randomChoice(modulatorRatios);
}
}
// Very subtle detuning for richness without losing tonality
ratio *= this.randomRange(0.999, 1.001);
const levelRange = isCarrier ? [0.5, 0.9] : [0.3, 0.8];
return {
ratio,
level: this.randomRange(levelRange[0], levelRange[1]),
attack: this.randomRange(0.001, 0.2),
decay: this.randomRange(0.02, 0.3),
sustain: this.randomRange(0.2, 0.8),
release: this.randomRange(0.05, 0.5),
attackCurve: this.randomInt(0, 3) as EnvCurve,
decayCurve: this.randomInt(0, 3) as EnvCurve,
releaseCurve: this.randomInt(0, 3) as EnvCurve,
};
}
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams {
return {
baseFreq: params.baseFreq,
algorithm: Math.random() < 0.1 ? this.randomInt(0, 2) as Algorithm : params.algorithm,
operators: params.operators.map((op, i) =>
this.mutateOperator(op, mutationAmount, i === 1, params.algorithm)
) as [OperatorParams, OperatorParams],
lfo: {
rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 20),
depth: this.mutateValue(params.lfo.depth, mutationAmount, 0, 0.6),
waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfo.waveform,
target: Math.random() < 0.08 ? this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const) : params.lfo.target,
},
feedback: this.mutateValue(params.feedback, mutationAmount, 0, 2),
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.3, 12),
subOscLevel: this.mutateValue(params.subOscLevel, mutationAmount, 0, 0.8),
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
};
}
private mutateOperator(op: OperatorParams, amount: number, isCarrier: boolean, algorithm: Algorithm): OperatorParams {
const carrierRatios = [0.5, 1, 2, 3, 4];
const modulatorRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8];
const electricPianoRatios = [1, 14];
const bellRatios = [1, 1.4, 3.5, 14];
const brassRatios = [1, 1, 2, 3];
let newRatio = op.ratio;
if (Math.random() < 0.15) {
if (algorithm === Algorithm.Parallel) {
newRatio = this.randomChoice([1, 2, 3, 4, 5, 6, 8]);
} else if (isCarrier) {
newRatio = this.randomChoice(carrierRatios);
} else {
const specialChance = Math.random();
if (specialChance < 0.2) {
newRatio = this.randomChoice(electricPianoRatios);
} else if (specialChance < 0.4) {
newRatio = this.randomChoice(bellRatios);
} else if (specialChance < 0.6) {
newRatio = this.randomChoice(brassRatios);
} else {
newRatio = this.randomChoice(modulatorRatios);
}
}
newRatio *= this.randomRange(0.999, 1.001);
}
return {
ratio: newRatio,
level: this.mutateValue(op.level, amount, 0.1, isCarrier ? 0.95 : 1.0),
attack: this.mutateValue(op.attack, amount, 0.001, 0.3),
decay: this.mutateValue(op.decay, amount, 0.01, 0.4),
sustain: this.mutateValue(op.sustain, amount, 0.05, 0.95),
release: this.mutateValue(op.release, amount, 0.02, 0.6),
attackCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.attackCurve,
decayCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.decayCurve,
releaseCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.releaseCurve,
};
}
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));
}
}

View File

@ -0,0 +1,246 @@
import type { SynthEngine } from './SynthEngine';
interface WavetableParams {
bankIndex: number;
position: number;
baseFreq: number;
filterCutoff: number;
filterResonance: number;
attack: number;
decay: number;
sustain: number;
release: number;
}
interface Wavetable {
name: string;
samples: Float32Array;
}
export class WavetableEngine implements SynthEngine<WavetableParams> {
private wavetables: Wavetable[] = [];
private isLoaded = false;
constructor() {
this.generateBasicWavetables();
this.loadWavetablesAsync();
}
getName(): string {
return 'Wavetable';
}
getDescription(): string {
return 'Classic wavetable synthesis';
}
getType() {
return 'generative' as const;
}
private generateBasicWavetables(): void {
const size = 2048;
const tables = [
{ name: 'Sine', gen: (i: number) => Math.sin((i / size) * Math.PI * 2) },
{ name: 'Triangle', gen: (i: number) => {
const t = i / size;
return t < 0.25 ? t * 4 : t < 0.75 ? 2 - t * 4 : t * 4 - 4;
}},
{ name: 'Saw', gen: (i: number) => (i / size) * 2 - 1 },
{ name: 'Square', gen: (i: number) => i < size / 2 ? 1 : -1 },
{ name: 'Pulse25', gen: (i: number) => i < size / 4 ? 1 : -1 },
];
this.wavetables = tables.map(({ name, gen }) => ({
name,
samples: Float32Array.from({ length: size }, (_, i) => gen(i)),
}));
}
private async loadWavetablesAsync(): Promise<void> {
if (this.isLoaded) return;
try {
const manifestResponse = await fetch('/wavetables/manifest.txt');
const manifestText = await manifestResponse.text();
const filenames = manifestText.trim().split('\n').filter(line => line.endsWith('.wav'));
const loadPromises = filenames.slice(0, 50).map(async (filename) => {
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const response = await fetch(`/wavetables/${filename.trim()}`);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const samples = audioBuffer.getChannelData(0);
let sum = 0;
for (let i = 0; i < samples.length; i++) {
sum += samples[i];
}
const dc = sum / samples.length;
const cleaned = new Float32Array(samples.length);
let peak = 0;
for (let i = 0; i < samples.length; i++) {
cleaned[i] = samples[i] - dc;
peak = Math.max(peak, Math.abs(cleaned[i]));
}
if (peak > 0) {
for (let i = 0; i < cleaned.length; i++) {
cleaned[i] /= peak;
}
}
return {
name: filename.replace('.wav', ''),
samples: cleaned,
};
} catch (error) {
return null;
}
});
const results = await Promise.all(loadPromises);
const loaded = results.filter((wt): wt is Wavetable => wt !== null);
if (loaded.length > 0) {
this.wavetables = loaded;
console.log(`Loaded ${this.wavetables.length} wavetables`);
}
this.isLoaded = true;
} catch (error) {
console.error('Failed to load wavetables:', error);
this.isLoaded = true;
}
}
generate(params: WavetableParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
if (this.wavetables.length < 3) {
return [left, right];
}
const bankSize = 4;
const numBanks = Math.max(1, this.wavetables.length - bankSize + 1);
const bankStart = Math.floor(params.bankIndex * numBanks) % numBanks;
const bank: Wavetable[] = [];
for (let i = 0; i < bankSize; i++) {
bank.push(this.wavetables[(bankStart + i) % this.wavetables.length]);
}
const tableSize = bank[0].samples.length;
const phaseIncrement = params.baseFreq / sampleRate;
let phase = 0;
const attackSamples = params.attack * duration * sampleRate;
const decaySamples = params.decay * duration * sampleRate;
const releaseSamples = params.release * duration * sampleRate;
let filterLP = 0;
let filterBP = 0;
const tablePos = params.position * (bankSize - 1);
const table1Index = Math.floor(tablePos);
const table2Index = Math.min(table1Index + 1, bankSize - 1);
const tableFade = tablePos - table1Index;
const cutoffNorm = Math.min(params.filterCutoff * 0.45, 0.45);
const f = 2 * Math.sin(Math.PI * cutoffNorm);
const q = 1 / Math.max(params.filterResonance * 5 + 0.7, 0.7);
for (let i = 0; i < numSamples; i++) {
let env = 1;
if (i < attackSamples) {
env = i / attackSamples;
} else if (i < attackSamples + decaySamples) {
const t = (i - attackSamples) / decaySamples;
env = 1 - t * (1 - params.sustain);
} else if (i < numSamples - releaseSamples) {
env = params.sustain;
} else {
const t = (i - (numSamples - releaseSamples)) / releaseSamples;
env = params.sustain * (1 - t);
}
const index1 = phase * tableSize;
const i1 = Math.floor(index1);
const i2 = (i1 + 1) % tableSize;
const frac = index1 - i1;
const sample1 = bank[table1Index].samples[i1] * (1 - frac) + bank[table1Index].samples[i2] * frac;
const sample2 = bank[table2Index].samples[i1] * (1 - frac) + bank[table2Index].samples[i2] * frac;
let sample = sample1 * (1 - tableFade) + sample2 * tableFade;
const hp = sample - filterLP - q * filterBP;
filterBP = filterBP + f * hp;
filterLP = filterLP + f * filterBP;
filterBP = Math.max(-2, Math.min(2, filterBP));
filterLP = Math.max(-2, Math.min(2, filterLP));
sample = filterLP * env;
left[i] = sample;
right[i] = sample;
phase += phaseIncrement;
if (phase >= 1) phase -= 1;
}
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 gain = 0.9 / peak;
for (let i = 0; i < numSamples; i++) {
left[i] *= gain;
right[i] *= gain;
}
}
return [left, right];
}
randomParams(): WavetableParams {
const freqs = [110, 146.8, 220, 293.7, 440];
return {
bankIndex: Math.random(),
position: 0.2 + Math.random() * 0.6,
baseFreq: freqs[Math.floor(Math.random() * freqs.length)],
filterCutoff: 0.5 + Math.random() * 0.4,
filterResonance: Math.random() * 0.5,
attack: 0.001 + Math.random() * 0.05,
decay: 0.05 + Math.random() * 0.2,
sustain: 0.5 + Math.random() * 0.4,
release: 0.1 + Math.random() * 0.3,
};
}
mutateParams(params: WavetableParams): WavetableParams {
const mutate = (v: number, amount: number = 0.1) => {
return Math.max(0, Math.min(1, v + (Math.random() - 0.5) * amount));
};
return {
bankIndex: Math.random() < 0.2 ? Math.random() : params.bankIndex,
position: mutate(params.position, 0.2),
baseFreq: params.baseFreq,
filterCutoff: mutate(params.filterCutoff, 0.2),
filterResonance: mutate(params.filterResonance, 0.15),
attack: mutate(params.attack, 0.1),
decay: mutate(params.decay, 0.15),
sustain: mutate(params.sustain, 0.15),
release: mutate(params.release, 0.15),
};
}
}

View File

@ -108,54 +108,552 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
}
}
// Normalize the output with moderate gain
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(leftBuffer[i]), Math.abs(rightBuffer[i]));
}
if (peak > 0.001) {
const normalizationGain = 0.7 / peak;
for (let i = 0; i < numSamples; i++) {
leftBuffer[i] *= normalizationGain;
rightBuffer[i] *= normalizationGain;
}
}
return [leftBuffer, rightBuffer];
}
randomParams(): ZzfxParams {
// Adjusted ranges to produce more audible sounds
return {
volume: 1,
randomness: this.randomRange(0, 0.2),
frequency: this.randomRange(100, 2000), // More audible frequency range
attack: this.randomRange(0, 0.05), // Shorter attack for more immediate sound
sustain: this.randomRange(0.1, 0.4), // Ensure minimum sustain
release: this.randomRange(0.05, 0.3), // Reasonable release
shape: this.randomInt(0, 5),
shapeCurve: this.randomRange(0.5, 2), // Less extreme curve
slide: this.randomRange(-0.3, 0.3), // Less extreme slide
deltaSlide: this.randomRange(-0.1, 0.1),
pitchJump: this.randomRange(-500, 500),
pitchJumpTime: this.randomRange(0, 1),
repeatTime: this.randomRange(0, 0.2),
noise: this.randomRange(0, 0.5), // Less noise
modulation: this.randomRange(0, 10), // Less extreme modulation
bitCrush: this.randomRange(0, 8), // Much less bit crushing
delay: this.randomRange(0, 0.1),
sustainVolume: 1,
decay: this.randomRange(0, 0.1), // Shorter decay
tremolo: this.randomRange(0, 0.3), // Less tremolo
};
const preset = Math.floor(Math.random() * 20);
switch (preset) {
case 0: // Clean tones
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(200, 800),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.2, 0.5),
release: this.randomRange(0.1, 0.3),
shape: this.randomInt(0, 2),
shapeCurve: this.randomRange(0.8, 1.2),
slide: this.randomRange(-0.1, 0.1),
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(0, 2),
bitCrush: 0,
delay: this.randomRange(0, 0.05),
sustainVolume: 1,
decay: this.randomRange(0, 0.05),
tremolo: this.randomRange(0, 0.1),
};
case 1: // Pitched slides
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 1200),
attack: this.randomRange(0, 0.01),
sustain: this.randomRange(0.15, 0.35),
release: this.randomRange(0.05, 0.2),
shape: this.randomInt(0, 3),
shapeCurve: this.randomRange(0.7, 1.5),
slide: this.randomRange(-0.4, 0.4),
deltaSlide: this.randomRange(-0.05, 0.05),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(0, 3),
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: this.randomRange(0, 0.08),
tremolo: 0,
};
case 2: // Pitch jumps
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 1000),
attack: 0,
sustain: this.randomRange(0.2, 0.4),
release: this.randomRange(0.05, 0.15),
shape: this.randomInt(0, 3),
shapeCurve: 1,
slide: 0,
deltaSlide: 0,
pitchJump: this.randomRange(-200, 200),
pitchJumpTime: this.randomRange(0.2, 0.8),
repeatTime: 0,
noise: 0,
modulation: this.randomRange(0, 2),
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 3: // Textured (light noise)
return {
volume: 1,
randomness: this.randomRange(0, 0.1),
frequency: this.randomRange(150, 600),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.15, 0.35),
release: this.randomRange(0.05, 0.2),
shape: this.randomInt(0, 4),
shapeCurve: this.randomRange(0.7, 1.5),
slide: this.randomRange(-0.15, 0.15),
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: this.randomRange(0, 0.15),
modulation: this.randomRange(0, 4),
bitCrush: 0,
delay: this.randomRange(0, 0.08),
sustainVolume: 1,
decay: this.randomRange(0, 0.1),
tremolo: this.randomRange(0, 0.2),
};
case 4: // Retro game sounds
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(200, 1500),
attack: 0,
sustain: this.randomRange(0.1, 0.25),
release: this.randomRange(0.02, 0.1),
shape: this.randomInt(0, 4),
shapeCurve: 1,
slide: this.randomRange(-0.5, 0.3),
deltaSlide: this.randomRange(-0.08, 0.05),
pitchJump: Math.random() > 0.7 ? this.randomRange(-150, 150) : 0,
pitchJumpTime: this.randomRange(0.3, 0.7),
repeatTime: Math.random() > 0.6 ? this.randomRange(0.05, 0.15) : 0,
noise: 0,
modulation: this.randomRange(0, 5),
bitCrush: Math.random() > 0.7 ? this.randomRange(1, 3) : 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 5: // Bass tones
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(40, 200),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.25, 0.5),
release: this.randomRange(0.1, 0.25),
shape: this.randomInt(0, 2),
shapeCurve: this.randomRange(0.8, 1.3),
slide: this.randomRange(-0.15, 0),
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(0, 1),
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: this.randomRange(0, 0.05),
tremolo: 0,
};
case 6: // Melodic with vibrato
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 1200),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.2, 0.4),
release: this.randomRange(0.08, 0.2),
shape: this.randomInt(0, 3),
shapeCurve: this.randomRange(0.8, 1.2),
slide: 0,
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(5, 10),
bitCrush: 0,
delay: this.randomRange(0, 0.06),
sustainVolume: 1,
decay: this.randomRange(0, 0.06),
tremolo: this.randomRange(0.1, 0.3),
};
case 7: // Laser/zap sounds
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(800, 2000),
attack: 0,
sustain: this.randomRange(0.05, 0.15),
release: this.randomRange(0.02, 0.08),
shape: this.randomInt(1, 3),
shapeCurve: 1,
slide: this.randomRange(-0.6, -0.3),
deltaSlide: this.randomRange(-0.05, 0),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(0, 2),
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 8: // Explosion/impact
return {
volume: 1,
randomness: this.randomRange(0.1, 0.15),
frequency: this.randomRange(50, 150),
attack: 0,
sustain: this.randomRange(0.15, 0.3),
release: this.randomRange(0.15, 0.35),
shape: this.randomInt(0, 1),
shapeCurve: this.randomRange(0.5, 0.8),
slide: this.randomRange(-0.4, -0.2),
deltaSlide: this.randomRange(-0.05, 0),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: this.randomRange(0.1, 0.2),
modulation: 0,
bitCrush: Math.random() > 0.5 ? this.randomRange(1, 2) : 0,
delay: 0,
sustainVolume: 1,
decay: this.randomRange(0.05, 0.1),
tremolo: 0,
};
case 9: // Coin/pickup
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(500, 1000),
attack: 0,
sustain: this.randomRange(0.05, 0.1),
release: this.randomRange(0.1, 0.2),
shape: this.randomInt(0, 2),
shapeCurve: 1.2,
slide: this.randomRange(0.2, 0.4),
deltaSlide: 0,
pitchJump: this.randomRange(100, 200),
pitchJumpTime: this.randomRange(0.3, 0.6),
repeatTime: 0,
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 10: // Blip/beep (UI)
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(600, 1400),
attack: 0,
sustain: this.randomRange(0.03, 0.08),
release: this.randomRange(0.02, 0.05),
shape: this.randomInt(0, 2),
shapeCurve: 1,
slide: 0,
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 11: // Power-up
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(200, 500),
attack: 0,
sustain: this.randomRange(0.2, 0.35),
release: this.randomRange(0.1, 0.2),
shape: this.randomInt(0, 3),
shapeCurve: 1.5,
slide: this.randomRange(0.3, 0.6),
deltaSlide: this.randomRange(0.02, 0.05),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(3, 6),
bitCrush: 0,
delay: this.randomRange(0.02, 0.05),
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 12: // Jump
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 600),
attack: 0,
sustain: this.randomRange(0.08, 0.15),
release: this.randomRange(0.05, 0.12),
shape: this.randomInt(0, 2),
shapeCurve: 1,
slide: this.randomRange(0.15, 0.35),
deltaSlide: this.randomRange(-0.03, -0.01),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 13: // Alarm/warning
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 800),
attack: 0,
sustain: this.randomRange(0.3, 0.5),
release: this.randomRange(0.05, 0.1),
shape: 1,
shapeCurve: 1,
slide: 0,
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: this.randomRange(0.1, 0.15),
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: this.randomRange(0.2, 0.4),
};
case 14: // Wobble bass
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(60, 150),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.3, 0.5),
release: this.randomRange(0.1, 0.2),
shape: this.randomInt(0, 2),
shapeCurve: this.randomRange(0.8, 1.2),
slide: 0,
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: this.randomRange(8, 12),
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 15: // Wind-down
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 800),
attack: 0,
sustain: this.randomRange(0.25, 0.4),
release: this.randomRange(0.15, 0.3),
shape: this.randomInt(1, 3),
shapeCurve: 0.7,
slide: this.randomRange(-0.5, -0.3),
deltaSlide: this.randomRange(-0.08, -0.03),
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: this.randomRange(0.05, 0.1),
tremolo: 0,
};
case 16: // Hit/damage
return {
volume: 1,
randomness: this.randomRange(0.05, 0.1),
frequency: this.randomRange(200, 400),
attack: 0,
sustain: this.randomRange(0.05, 0.12),
release: this.randomRange(0.05, 0.15),
shape: this.randomInt(0, 3),
shapeCurve: 0.8,
slide: this.randomRange(-0.4, -0.2),
deltaSlide: 0,
pitchJump: this.randomRange(-100, -50),
pitchJumpTime: this.randomRange(0.4, 0.7),
repeatTime: 0,
noise: this.randomRange(0.05, 0.12),
modulation: 0,
bitCrush: Math.random() > 0.6 ? this.randomRange(1, 2) : 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 17: // Arpeggio
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 1000),
attack: 0,
sustain: this.randomRange(0.2, 0.35),
release: this.randomRange(0.05, 0.12),
shape: this.randomInt(0, 2),
shapeCurve: 1,
slide: 0,
deltaSlide: 0,
pitchJump: this.randomRange(100, 300),
pitchJumpTime: this.randomRange(0.15, 0.4),
repeatTime: this.randomRange(0.08, 0.12),
noise: 0,
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 18: // Percussion/drums
return {
volume: 1,
randomness: this.randomRange(0.08, 0.15),
frequency: this.randomRange(80, 250),
attack: 0,
sustain: this.randomRange(0.03, 0.08),
release: this.randomRange(0.05, 0.15),
shape: this.randomInt(0, 1),
shapeCurve: 0.6,
slide: this.randomRange(-0.3, -0.1),
deltaSlide: 0,
pitchJump: 0,
pitchJumpTime: 0,
repeatTime: 0,
noise: this.randomRange(0.08, 0.15),
modulation: 0,
bitCrush: 0,
delay: 0,
sustainVolume: 1,
decay: 0,
tremolo: 0,
};
case 19: // Sparkle/magic
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(1000, 2000),
attack: 0,
sustain: this.randomRange(0.15, 0.3),
release: this.randomRange(0.15, 0.3),
shape: this.randomInt(0, 2),
shapeCurve: 1.5,
slide: this.randomRange(-0.2, 0.2),
deltaSlide: this.randomRange(-0.03, 0.03),
pitchJump: Math.random() > 0.5 ? this.randomRange(50, 150) : 0,
pitchJumpTime: this.randomRange(0.3, 0.6),
repeatTime: 0,
noise: 0,
modulation: this.randomRange(3, 7),
bitCrush: 0,
delay: this.randomRange(0.03, 0.08),
sustainVolume: 1,
decay: this.randomRange(0.03, 0.08),
tremolo: this.randomRange(0.05, 0.15),
};
default: // Balanced mix
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(200, 1200),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.15, 0.4),
release: this.randomRange(0.05, 0.2),
shape: this.randomInt(0, 4),
shapeCurve: this.randomRange(0.7, 1.5),
slide: this.randomRange(-0.2, 0.2),
deltaSlide: this.randomRange(-0.05, 0.05),
pitchJump: Math.random() > 0.8 ? this.randomRange(-100, 100) : 0,
pitchJumpTime: this.randomRange(0, 0.5),
repeatTime: 0,
noise: this.randomRange(0, 0.1),
modulation: this.randomRange(0, 4),
bitCrush: Math.random() > 0.85 ? this.randomRange(1, 2) : 0,
delay: this.randomRange(0, 0.05),
sustainVolume: 1,
decay: this.randomRange(0, 0.08),
tremolo: this.randomRange(0, 0.15),
};
}
}
mutateParams(params: ZzfxParams, mutationAmount: number = 0.15): ZzfxParams {
return {
volume: 1,
randomness: this.mutateValue(params.randomness, mutationAmount, 0, 0.2),
frequency: this.mutateValue(params.frequency, mutationAmount * 2, 100, 2000),
attack: this.mutateValue(params.attack, mutationAmount, 0, 0.05),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.4),
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.3),
shape: Math.random() < 0.1 ? this.randomInt(0, 5) : params.shape,
shapeCurve: this.mutateValue(params.shapeCurve, mutationAmount, 0.5, 2),
slide: this.mutateValue(params.slide, mutationAmount, -0.3, 0.3),
deltaSlide: this.mutateValue(params.deltaSlide, mutationAmount, -0.1, 0.1),
pitchJump: this.mutateValue(params.pitchJump, mutationAmount * 3, -500, 500),
randomness: this.mutateValue(params.randomness, mutationAmount, 0, 0.1),
frequency: this.mutateValue(params.frequency, mutationAmount * 2, 40, 1500),
attack: this.mutateValue(params.attack, mutationAmount, 0, 0.03),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.5),
release: this.mutateValue(params.release, mutationAmount, 0.02, 0.3),
shape: Math.random() < 0.1 ? this.randomInt(0, 4) : params.shape,
shapeCurve: this.mutateValue(params.shapeCurve, mutationAmount, 0.7, 1.5),
slide: this.mutateValue(params.slide, mutationAmount, -0.5, 0.4),
deltaSlide: this.mutateValue(params.deltaSlide, mutationAmount, -0.08, 0.05),
pitchJump: this.mutateValue(params.pitchJump, mutationAmount * 3, -200, 200),
pitchJumpTime: this.mutateValue(params.pitchJumpTime, mutationAmount, 0, 1),
repeatTime: this.mutateValue(params.repeatTime, mutationAmount, 0, 0.2),
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5),
repeatTime: this.mutateValue(params.repeatTime, mutationAmount, 0, 0.15),
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.15),
modulation: this.mutateValue(params.modulation, mutationAmount, 0, 10),
bitCrush: this.mutateValue(params.bitCrush, mutationAmount, 0, 8),
delay: this.mutateValue(params.delay, mutationAmount, 0, 0.1),
bitCrush: this.mutateValue(params.bitCrush, mutationAmount, 0, 3),
delay: this.mutateValue(params.delay, mutationAmount, 0, 0.08),
sustainVolume: 1,
decay: this.mutateValue(params.decay, mutationAmount, 0, 0.1),
tremolo: this.mutateValue(params.tremolo, mutationAmount, 0, 0.3),

View File

@ -1,5 +1,6 @@
import type { SynthEngine } from './SynthEngine';
import { FourOpFM } from './FourOpFM';
import { TwoOpFM } from './TwoOpFM';
import { DubSiren } from './DubSiren';
import { Benjolin } from './Benjolin';
import { ZzfxEngine } from './ZzfxEngine';
@ -8,15 +9,18 @@ import { Ring } from './Ring';
import { Sample } from './Sample';
import { Input } from './Input';
import { KarplusStrong } from './KarplusStrong';
import { AdditiveEngine } from './AdditiveEngine';
export const engines: SynthEngine[] = [
new Sample(),
new Input(),
new FourOpFM(),
new TwoOpFM(),
new DubSiren(),
new Benjolin(),
new ZzfxEngine(),
new NoiseDrum(),
new Ring(),
new KarplusStrong(),
new AdditiveEngine(),
];