clean interface

This commit is contained in:
2025-10-12 11:32:36 +02:00
parent 6c11c5756a
commit fcb784d403
13 changed files with 536 additions and 118 deletions

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
enum EnvCurve {
Linear,
@ -380,9 +380,11 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
}
}
randomParams(): AdditiveParams {
randomParams(pitchLock?: PitchLock): 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 baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
const harmonicSeriesType = this.randomInt(0, 3) as HarmonicSeriesType;
const distributionStrategy = this.randomInt(0, 9) as DistributionStrategy;
@ -514,7 +516,7 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
};
}
mutateParams(params: AdditiveParams, mutationAmount: number = 0.15): AdditiveParams {
mutateParams(params: AdditiveParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): AdditiveParams {
const newHarmonicSeriesType = Math.random() < 0.08 ? this.randomInt(0, 3) as HarmonicSeriesType : params.harmonicSeriesType;
const newDistributionStrategy = Math.random() < 0.08 ? this.randomInt(0, 9) as DistributionStrategy : params.distributionStrategy;
const ratios = this.generateRatiosForStrategy(newDistributionStrategy);
@ -530,8 +532,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
}
}
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq: params.baseFreq,
baseFreq,
partials: newPartials,
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
interface BenjolinParams {
// Core oscillators
@ -808,7 +808,7 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
}
}
randomParams(): BenjolinParams {
randomParams(pitchLock?: PitchLock): BenjolinParams {
// Choose a random preset configuration
const preset = Math.floor(Math.random() * PRESET_COUNT);
@ -818,7 +818,9 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
// Generate full parameter set with preset biases
const params: BenjolinParams = {
// Core oscillators
osc1Freq: presetParams.osc1Freq ?? (20 + Math.random() * 800),
osc1Freq: pitchLock?.enabled
? pitchLock.frequency
: presetParams.osc1Freq ?? (20 + Math.random() * 800),
osc2Freq: presetParams.osc2Freq ?? (30 + Math.random() * 1200),
osc1Wave: presetParams.osc1Wave ?? Math.random(),
osc2Wave: presetParams.osc2Wave ?? Math.random(),
@ -876,17 +878,17 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
return params;
}
mutateParams(params: BenjolinParams): BenjolinParams {
mutateParams(params: BenjolinParams, mutationAmount?: number, pitchLock?: PitchLock): BenjolinParams {
const mutated = { ...params };
// Determine mutation strength based on current "stability"
const stability = (params.crossMod1to2 + params.crossMod2to1) / 2 +
params.runglerChaos + params.evolutionDepth;
const mutationAmount = stability > 1.5 ? 0.05 : stability < 0.5 ? 0.2 : 0.1;
const mutAmount = mutationAmount ?? (stability > 1.5 ? 0.05 : stability < 0.5 ? 0.2 : 0.1);
// Helper for correlated mutations
const mutateValue = (value: number, min: number, max: number, correlation = 1): number => {
const delta = (Math.random() - 0.5) * mutationAmount * (max - min) * correlation;
const delta = (Math.random() - 0.5) * mutAmount * (max - min) * correlation;
return Math.max(min, Math.min(max, value + delta));
};
@ -895,9 +897,15 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
if (strategy < 0.3) {
// Mutate frequency relationships
const freqRatio = mutated.osc2Freq / mutated.osc1Freq;
mutated.osc1Freq = mutateValue(mutated.osc1Freq, 20, 2000);
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
if (pitchLock?.enabled) {
mutated.osc1Freq = pitchLock.frequency;
const freqRatio = mutated.osc2Freq / params.osc1Freq;
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
} else {
const freqRatio = mutated.osc2Freq / mutated.osc1Freq;
mutated.osc1Freq = mutateValue(mutated.osc1Freq, 20, 2000);
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
}
// Correlate cross-mod amounts
const crossModDelta = (Math.random() - 0.5) * mutationAmount;

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
enum OscillatorWaveform {
Sine,
@ -427,22 +427,33 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
return [output, v1Next, v2Next];
}
randomParams(): DubSirenParams {
const freqPairs = [
[100, 1200],
[200, 800],
[300, 2000],
[50, 400],
[500, 3000],
[150, 600],
];
randomParams(pitchLock?: PitchLock): DubSirenParams {
let startFreq: number;
let endFreq: number;
const [startFreq, endFreq] = this.randomChoice(freqPairs);
const shouldReverse = Math.random() < 0.3;
if (pitchLock?.enabled) {
// When pitch locked, sweep around the locked frequency
startFreq = pitchLock.frequency;
endFreq = pitchLock.frequency * (Math.random() < 0.5 ? 0.5 : 2);
} else {
const freqPairs = [
[100, 1200],
[200, 800],
[300, 2000],
[50, 400],
[500, 3000],
[150, 600],
];
const [freq1, freq2] = this.randomChoice(freqPairs);
const shouldReverse = Math.random() < 0.3;
startFreq = shouldReverse ? freq2 : freq1;
endFreq = shouldReverse ? freq1 : freq2;
}
return {
startFreq: shouldReverse ? endFreq : startFreq,
endFreq: shouldReverse ? startFreq : endFreq,
startFreq,
endFreq,
sweepCurve: this.randomInt(0, 4) as SweepCurve,
waveform: this.randomInt(0, 4) as OscillatorWaveform,
pulseWidth: this.randomRange(0.1, 0.9),
@ -464,10 +475,27 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
};
}
mutateParams(params: DubSirenParams, mutationAmount: number = 0.15): DubSirenParams {
mutateParams(params: DubSirenParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DubSirenParams {
let startFreq: number;
let endFreq: number;
if (pitchLock?.enabled) {
// When pitch locked, keep one frequency at the locked value
if (Math.random() < 0.5) {
startFreq = pitchLock.frequency;
endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000);
} else {
startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000);
endFreq = pitchLock.frequency;
}
} else {
startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000);
endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000);
}
return {
startFreq: this.mutateValue(params.startFreq, mutationAmount, 20, 5000),
endFreq: this.mutateValue(params.endFreq, mutationAmount, 20, 5000),
startFreq,
endFreq,
sweepCurve: Math.random() < 0.1 ? this.randomInt(0, 4) as SweepCurve : params.sweepCurve,
waveform: Math.random() < 0.1 ? this.randomInt(0, 4) as OscillatorWaveform : params.waveform,
pulseWidth: this.mutateValue(params.pulseWidth, mutationAmount, 0.05, 0.95),

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
enum EnvCurve {
Linear,
@ -420,12 +420,14 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
}
}
randomParams(): FourOpFMParams {
randomParams(pitchLock?: PitchLock): FourOpFMParams {
const algorithm = this.randomInt(0, 5) as Algorithm;
// More musical frequency ratios including inharmonic ones
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
const baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
return {
baseFreq,
@ -481,9 +483,11 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
};
}
mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15): FourOpFMParams {
mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): FourOpFMParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq: params.baseFreq,
baseFreq,
algorithm: Math.random() < 0.08 ? this.randomInt(0, 5) as Algorithm : params.algorithm,
operators: params.operators.map((op, i) =>
this.mutateOperator(op, mutationAmount, i === 3, params.algorithm)

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
type HarmonicMode =
| 'single' // Just fundamental
@ -323,7 +323,7 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
}
}
randomParams(): KarplusStrongParams {
randomParams(pitchLock?: PitchLock): KarplusStrongParams {
// Musical frequencies (notes from E2 to E5)
const frequencies = [
82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47, 130.81, 138.59,
@ -333,6 +333,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
830.61, 880.00, 932.33, 987.77, 1046.50, 1108.73, 1174.66, 1244.51, 1318.51
];
const frequency = pitchLock?.enabled
? pitchLock.frequency
: frequencies[Math.floor(Math.random() * frequencies.length)];
// Randomly choose harmonic mode
// Weighted selection: favor more consonant intervals
const modes: HarmonicMode[] = [
@ -362,7 +366,7 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
];
return {
frequency: frequencies[Math.floor(Math.random() * frequencies.length)],
frequency,
damping: 0.7 + Math.random() * 0.29, // 0.7 to 0.99
brightness: Math.random(), // 0 to 1
decayCharacter: (Math.random() * 2 - 1) * 0.8, // -0.8 to 0.8 (mostly natural darkening)
@ -378,15 +382,17 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
};
}
mutateParams(params: KarplusStrongParams, mutationAmount: number = 0.15): KarplusStrongParams {
mutateParams(params: KarplusStrongParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): KarplusStrongParams {
const mutate = (value: number, range: number, min: number, max: number) => {
const delta = (Math.random() * 2 - 1) * range * mutationAmount;
return Math.max(min, Math.min(max, value + delta));
};
// Occasionally jump to harmonic/subharmonic
let newFreq = params.frequency;
if (Math.random() < 0.15) {
// Occasionally jump to harmonic/subharmonic (unless pitch locked)
let newFreq: number;
if (pitchLock?.enabled) {
newFreq = pitchLock.frequency;
} else if (Math.random() < 0.15) {
const multipliers = [0.5, 2, 1.5, 3];
newFreq = params.frequency * multipliers[Math.floor(Math.random() * multipliers.length)];
newFreq = Math.max(50, Math.min(2000, newFreq));

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
enum LFOWaveform {
Sine,
@ -386,9 +386,11 @@ export class Ring implements SynthEngine<RingParams> {
}
}
randomParams(): RingParams {
randomParams(pitchLock?: PitchLock): RingParams {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
const carrierFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
const carrierFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
const modulatorRatio = this.randomChoice(ratioChoices);
@ -434,27 +436,29 @@ export class Ring implements SynthEngine<RingParams> {
};
}
mutateParams(params: RingParams, mutationAmount: number = 0.15): RingParams {
mutateParams(params: RingParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): RingParams {
const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
let modulatorFreq = params.modulatorFreq;
let secondModulatorFreq = params.secondModulatorFreq;
const carrierFreq = pitchLock?.enabled ? pitchLock.frequency : params.carrierFreq;
if (Math.random() < 0.1) {
const newRatio = this.randomChoice(ratioChoices);
modulatorFreq = params.carrierFreq * newRatio * this.randomRange(0.98, 1.02);
modulatorFreq = carrierFreq * newRatio * this.randomRange(0.98, 1.02);
} else {
modulatorFreq = this.mutateValue(params.modulatorFreq, mutationAmount, 20, 2000);
}
if (Math.random() < 0.1) {
const newRatio = this.randomChoice(ratioChoices);
secondModulatorFreq = params.carrierFreq * newRatio * this.randomRange(0.97, 1.03);
secondModulatorFreq = carrierFreq * newRatio * this.randomRange(0.97, 1.03);
} else {
secondModulatorFreq = this.mutateValue(params.secondModulatorFreq, mutationAmount, 20, 2000);
}
return {
carrierFreq: params.carrierFreq,
carrierFreq,
modulatorFreq,
secondModulatorFreq,
carrierLevel: this.mutateValue(params.carrierLevel, mutationAmount, 0.3, 1.0),

View File

@ -2,14 +2,20 @@
// The duration parameter should be used to scale time-based parameters (envelopes, LFOs, etc.)
// Time-based parameters should be stored as ratios (0-1) and scaled by duration during generation
// Engines must generate stereo output: [leftChannel, rightChannel]
// When pitch lock is provided, engines must use the locked frequency and preserve it across randomization/mutation
export type EngineType = 'generative' | 'sample' | 'input';
export interface PitchLock {
enabled: boolean;
frequency: number; // Frequency in Hz
}
export interface SynthEngine<T = any> {
getName(): string;
getDescription(): string;
getType(): EngineType;
generate(params: T, sampleRate: number, duration: number): [Float32Array, Float32Array];
randomParams(): T;
mutateParams(params: T, mutationAmount?: number): T;
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array];
randomParams(pitchLock?: PitchLock): T;
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
enum EnvCurve {
Linear,
@ -356,11 +356,16 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
}
}
randomParams(): TwoOpFMParams {
randomParams(pitchLock?: PitchLock): 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);
let baseFreq: number;
if (pitchLock?.enabled) {
baseFreq = pitchLock.frequency;
} else {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
}
return {
baseFreq,
@ -433,9 +438,11 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
};
}
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams {
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): TwoOpFMParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq: params.baseFreq,
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)

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
interface WavetableParams {
bankIndex: number;
@ -211,12 +211,14 @@ export class WavetableEngine implements SynthEngine<WavetableParams> {
return [left, right];
}
randomParams(): WavetableParams {
randomParams(pitchLock?: PitchLock): 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)],
baseFreq: pitchLock?.enabled
? pitchLock.frequency
: 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,
@ -226,15 +228,17 @@ export class WavetableEngine implements SynthEngine<WavetableParams> {
};
}
mutateParams(params: WavetableParams): WavetableParams {
mutateParams(params: WavetableParams, mutationAmount?: number, pitchLock?: PitchLock): WavetableParams {
const mutate = (v: number, amount: number = 0.1) => {
return Math.max(0, Math.min(1, v + (Math.random() - 0.5) * amount));
};
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
bankIndex: Math.random() < 0.2 ? Math.random() : params.bankIndex,
position: mutate(params.position, 0.2),
baseFreq: params.baseFreq,
baseFreq,
filterCutoff: mutate(params.filterCutoff, 0.2),
filterResonance: mutate(params.filterResonance, 0.15),
attack: mutate(params.attack, 0.1),

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine, PitchLock } from './SynthEngine';
// @ts-ignore
import { ZZFX } from 'zzfx';
@ -125,15 +125,18 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return [leftBuffer, rightBuffer];
}
randomParams(): ZzfxParams {
randomParams(pitchLock?: PitchLock): ZzfxParams {
const preset = Math.floor(Math.random() * 20);
const getFrequency = (min: number, max: number) =>
pitchLock?.enabled ? pitchLock.frequency : this.randomRange(min, max);
switch (preset) {
case 0: // Clean tones
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(200, 800),
frequency: getFrequency(200, 800),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.2, 0.5),
release: this.randomRange(0.1, 0.3),
@ -157,7 +160,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 1200),
frequency: getFrequency(400, 1200),
attack: this.randomRange(0, 0.01),
sustain: this.randomRange(0.15, 0.35),
release: this.randomRange(0.05, 0.2),
@ -181,7 +184,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 1000),
frequency: getFrequency(300, 1000),
attack: 0,
sustain: this.randomRange(0.2, 0.4),
release: this.randomRange(0.05, 0.15),
@ -205,7 +208,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0, 0.1),
frequency: this.randomRange(150, 600),
frequency: getFrequency(150, 600),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.15, 0.35),
release: this.randomRange(0.05, 0.2),
@ -229,7 +232,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(200, 1500),
frequency: getFrequency(200, 1500),
attack: 0,
sustain: this.randomRange(0.1, 0.25),
release: this.randomRange(0.02, 0.1),
@ -253,7 +256,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(40, 200),
frequency: getFrequency(40, 200),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.25, 0.5),
release: this.randomRange(0.1, 0.25),
@ -277,7 +280,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 1200),
frequency: getFrequency(300, 1200),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.2, 0.4),
release: this.randomRange(0.08, 0.2),
@ -301,7 +304,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(800, 2000),
frequency: getFrequency(800, 2000),
attack: 0,
sustain: this.randomRange(0.05, 0.15),
release: this.randomRange(0.02, 0.08),
@ -325,7 +328,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0.1, 0.15),
frequency: this.randomRange(50, 150),
frequency: getFrequency(50, 150),
attack: 0,
sustain: this.randomRange(0.15, 0.3),
release: this.randomRange(0.15, 0.35),
@ -349,7 +352,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(500, 1000),
frequency: getFrequency(500, 1000),
attack: 0,
sustain: this.randomRange(0.05, 0.1),
release: this.randomRange(0.1, 0.2),
@ -373,7 +376,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(600, 1400),
frequency: getFrequency(600, 1400),
attack: 0,
sustain: this.randomRange(0.03, 0.08),
release: this.randomRange(0.02, 0.05),
@ -397,7 +400,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(200, 500),
frequency: getFrequency(200, 500),
attack: 0,
sustain: this.randomRange(0.2, 0.35),
release: this.randomRange(0.1, 0.2),
@ -421,7 +424,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(300, 600),
frequency: getFrequency(300, 600),
attack: 0,
sustain: this.randomRange(0.08, 0.15),
release: this.randomRange(0.05, 0.12),
@ -445,7 +448,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 800),
frequency: getFrequency(400, 800),
attack: 0,
sustain: this.randomRange(0.3, 0.5),
release: this.randomRange(0.05, 0.1),
@ -469,7 +472,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(60, 150),
frequency: getFrequency(60, 150),
attack: this.randomRange(0, 0.02),
sustain: this.randomRange(0.3, 0.5),
release: this.randomRange(0.1, 0.2),
@ -493,7 +496,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 800),
frequency: getFrequency(400, 800),
attack: 0,
sustain: this.randomRange(0.25, 0.4),
release: this.randomRange(0.15, 0.3),
@ -517,7 +520,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0.05, 0.1),
frequency: this.randomRange(200, 400),
frequency: getFrequency(200, 400),
attack: 0,
sustain: this.randomRange(0.05, 0.12),
release: this.randomRange(0.05, 0.15),
@ -541,7 +544,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: 0,
frequency: this.randomRange(400, 1000),
frequency: getFrequency(400, 1000),
attack: 0,
sustain: this.randomRange(0.2, 0.35),
release: this.randomRange(0.05, 0.12),
@ -565,7 +568,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0.08, 0.15),
frequency: this.randomRange(80, 250),
frequency: getFrequency(80, 250),
attack: 0,
sustain: this.randomRange(0.03, 0.08),
release: this.randomRange(0.05, 0.15),
@ -589,7 +592,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(1000, 2000),
frequency: getFrequency(1000, 2000),
attack: 0,
sustain: this.randomRange(0.15, 0.3),
release: this.randomRange(0.15, 0.3),
@ -613,7 +616,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return {
volume: 1,
randomness: this.randomRange(0, 0.05),
frequency: this.randomRange(200, 1200),
frequency: getFrequency(200, 1200),
attack: this.randomRange(0, 0.03),
sustain: this.randomRange(0.15, 0.4),
release: this.randomRange(0.05, 0.2),
@ -635,11 +638,15 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
}
}
mutateParams(params: ZzfxParams, mutationAmount: number = 0.15): ZzfxParams {
mutateParams(params: ZzfxParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): ZzfxParams {
const frequency = pitchLock?.enabled
? pitchLock.frequency
: this.mutateValue(params.frequency, mutationAmount * 2, 40, 1500);
return {
volume: 1,
randomness: this.mutateValue(params.randomness, mutationAmount, 0, 0.1),
frequency: this.mutateValue(params.frequency, mutationAmount * 2, 40, 1500),
frequency,
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),

33
src/lib/utils/pitch.ts Normal file
View File

@ -0,0 +1,33 @@
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const A4_FREQUENCY = 440;
const A4_MIDI_NOTE = 69;
export function noteToFrequency(noteName: string): number | null {
const match = noteName.match(/^([A-G]#?)(-?\d+)$/i);
if (!match) return null;
const [, note, octave] = match;
const noteIndex = NOTE_NAMES.indexOf(note.toUpperCase());
if (noteIndex === -1) return null;
const octaveNum = parseInt(octave, 10);
const midiNote = (octaveNum + 1) * 12 + noteIndex;
const semitonesDiff = midiNote - A4_MIDI_NOTE;
return A4_FREQUENCY * Math.pow(2, semitonesDiff / 12);
}
export function parseFrequencyInput(input: string): number | null {
const trimmed = input.trim();
const asNumber = parseFloat(trimmed);
if (!isNaN(asNumber) && asNumber > 0 && asNumber < 20000) {
return asNumber;
}
return noteToFrequency(trimmed);
}
export function formatFrequency(frequency: number): string {
return frequency.toFixed(2);
}

View File

@ -1,9 +1,13 @@
const DEFAULT_VOLUME = 0.7;
const DEFAULT_DURATION = 1.0;
const DEFAULT_PITCH_LOCK_ENABLED = false;
const DEFAULT_PITCH_LOCK_FREQUENCY = 440;
const STORAGE_KEYS = {
VOLUME: 'volume',
DURATION: 'duration',
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
} as const;
export function loadVolume(): number {
@ -23,3 +27,21 @@ export function loadDuration(): number {
export function saveDuration(duration: number): void {
localStorage.setItem(STORAGE_KEYS.DURATION, duration.toString());
}
export function loadPitchLockEnabled(): boolean {
const stored = localStorage.getItem(STORAGE_KEYS.PITCH_LOCK_ENABLED);
return stored ? stored === 'true' : DEFAULT_PITCH_LOCK_ENABLED;
}
export function savePitchLockEnabled(enabled: boolean): void {
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_ENABLED, enabled.toString());
}
export function loadPitchLockFrequency(): number {
const stored = localStorage.getItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY);
return stored ? parseFloat(stored) : DEFAULT_PITCH_LOCK_FREQUENCY;
}
export function savePitchLockFrequency(frequency: number): void {
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
}