more engines
This commit is contained in:
@ -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';
|
||||
|
||||
590
src/lib/audio/engines/AdditiveEngine.ts
Normal file
590
src/lib/audio/engines/AdditiveEngine.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ interface KarplusStrongParams {
|
||||
|
||||
export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
getName(): string {
|
||||
return 'Karplus-Strong';
|
||||
return 'String(s)';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -71,7 +71,7 @@ export class Ring implements SynthEngine<RingParams> {
|
||||
private dcBlockerY1R = 0;
|
||||
|
||||
getName(): string {
|
||||
return 'Ring';
|
||||
return 'Creepy';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
|
||||
513
src/lib/audio/engines/TwoOpFM.ts
Normal file
513
src/lib/audio/engines/TwoOpFM.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
246
src/lib/audio/engines/WavetableEngine.ts
Normal file
246
src/lib/audio/engines/WavetableEngine.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user