phase distortion
This commit is contained in:
@ -65,4 +65,5 @@ Opens on http://localhost:8080
|
||||
|
||||
## Credits
|
||||
|
||||
Wavetables from [Adventure Kid Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) by Kristoffer Ekstrand
|
||||
- Wavetables from [Adventure Kid Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) by Kristoffer Ekstrand
|
||||
- [Garten Salat](https://garten.salat.dev/) by Felix Roos for drum synthesis inspiration.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
|
||||
interface BassDrum909Params {
|
||||
interface BassDrumParams {
|
||||
// Core frequency (base pitch of the kick)
|
||||
baseFreq: number;
|
||||
|
||||
@ -47,9 +47,15 @@ interface BassDrum909Params {
|
||||
|
||||
// Tuning offset
|
||||
tuning: number;
|
||||
|
||||
// Envelope curve parameters
|
||||
attackTime: number; // Attack duration (0 = 0.5ms, 1 = 50ms)
|
||||
attackCurve: number; // Attack curve shape (0 = soft/slow, 1 = sharp/fast)
|
||||
ampDecayCurve: number; // Amplitude decay curve (0 = loose/boomy, 1 = tight/punchy)
|
||||
pitchDecayCurve: number; // Pitch envelope curve (0 = loose, 1 = tight)
|
||||
}
|
||||
|
||||
export class BassDrum909 implements SynthEngine {
|
||||
export class BassDrum implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Kick';
|
||||
}
|
||||
@ -62,7 +68,7 @@ export class BassDrum909 implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): BassDrum909Params {
|
||||
randomParams(pitchLock?: PitchLock): BassDrumParams {
|
||||
// Choose a kick character/style
|
||||
const styleRoll = Math.random();
|
||||
|
||||
@ -71,6 +77,7 @@ export class BassDrum909 implements SynthEngine {
|
||||
let bodyResonance: number, bodyFreq: number, bodyDecay: number;
|
||||
let noise: number, noiseDecay: number, harmonics: number;
|
||||
let waveShape: number, phaseDistortion: number, distortion: number;
|
||||
let attackTime: number, attackCurve: number, ampDecayCurve: number, pitchDecayCurve: number;
|
||||
|
||||
if (styleRoll < 0.08) {
|
||||
// Tom/bongo style (starts HIGH, decays VERY fast - rare!)
|
||||
@ -90,6 +97,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = 0.3 + Math.random() * 0.4; // Triangle-ish
|
||||
phaseDistortion = 0.1 + Math.random() * 0.4; // Some phase distortion
|
||||
distortion = 0; // Clean
|
||||
attackTime = 0.0 + Math.random() * 0.2; // Very fast attack (0.5-10ms)
|
||||
attackCurve = 0.6 + Math.random() * 0.3; // Sharp attack (0.6-0.9)
|
||||
ampDecayCurve = 0.7 + Math.random() * 0.3; // Very tight decay (0.7-1.0)
|
||||
pitchDecayCurve = 0.6 + Math.random() * 0.3; // Fast pitch sweep (0.6-0.9)
|
||||
} else if (styleRoll < 0.25) {
|
||||
// Tight, punchy 808-style kick (short, lots of pitch, CLEAN)
|
||||
baseFreq = 0.2 + Math.random() * 0.4; // 56-88 Hz (mid-high kicks)
|
||||
@ -108,6 +119,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = 0.2 + Math.random() * 0.5; // Sine to triangle
|
||||
phaseDistortion = Math.random() * 0.3; // Light phase distortion
|
||||
distortion = 0; // Clean
|
||||
attackTime = 0.0 + Math.random() * 0.3; // Fast attack (0.5-15ms)
|
||||
attackCurve = 0.3 + Math.random() * 0.3; // Medium-sharp attack (0.3-0.6)
|
||||
ampDecayCurve = 0.5 + Math.random() * 0.3; // Medium-tight decay (0.5-0.8)
|
||||
pitchDecayCurve = 0.4 + Math.random() * 0.3; // Medium pitch sweep (0.4-0.7)
|
||||
} else if (styleRoll < 0.43) {
|
||||
// Deep, smooth kick (long, clean, lots of body)
|
||||
baseFreq = 0.0 + Math.random() * 0.35; // 40-68 Hz (low to mid kicks)
|
||||
@ -126,6 +141,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = Math.random() * 0.4; // Sine to triangle
|
||||
phaseDistortion = Math.random() * 0.25; // Subtle phase distortion
|
||||
distortion = 0; // Clean
|
||||
attackTime = 0.1 + Math.random() * 0.4; // Slower attack (5-25ms)
|
||||
attackCurve = 0.2 + Math.random() * 0.3; // Soft attack (0.2-0.5)
|
||||
ampDecayCurve = 0.2 + Math.random() * 0.3; // Loose, boomy decay (0.2-0.5)
|
||||
pitchDecayCurve = 0.2 + Math.random() * 0.3; // Slow pitch sweep (0.2-0.5)
|
||||
} else if (styleRoll < 0.63) {
|
||||
// Sub/electronic kick (very low, pure sine-like)
|
||||
baseFreq = 0.0 + Math.random() * 0.25; // 40-60 Hz (sub bass kicks)
|
||||
@ -144,6 +163,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = Math.random() * 0.3; // Mostly sine
|
||||
phaseDistortion = Math.random() * 0.2; // Very subtle
|
||||
distortion = 0; // Clean
|
||||
attackTime = 0.2 + Math.random() * 0.4; // Medium attack (10-30ms)
|
||||
attackCurve = 0.3 + Math.random() * 0.3; // Medium attack (0.3-0.6)
|
||||
ampDecayCurve = 0.1 + Math.random() * 0.3; // Very loose, sustaining (0.1-0.4)
|
||||
pitchDecayCurve = 0.1 + Math.random() * 0.3; // Gentle pitch sweep (0.1-0.4)
|
||||
} else if (styleRoll < 0.78) {
|
||||
// Snappy, clicky kick (fast attack, short)
|
||||
baseFreq = 0.3 + Math.random() * 0.5; // 64-100 Hz (high kicks)
|
||||
@ -162,6 +185,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = 0.4 + Math.random() * 0.6; // Triangle to square
|
||||
phaseDistortion = 0.2 + Math.random() * 0.5; // More phase distortion
|
||||
distortion = 0; // Clean
|
||||
attackTime = 0.0 + Math.random() * 0.15; // Instant attack (0.5-8ms)
|
||||
attackCurve = 0.7 + Math.random() * 0.3; // Very sharp attack (0.7-1.0)
|
||||
ampDecayCurve = 0.7 + Math.random() * 0.3; // Very tight (0.7-1.0)
|
||||
pitchDecayCurve = 0.7 + Math.random() * 0.3; // Fast pitch sweep (0.7-1.0)
|
||||
} else if (styleRoll < 0.88) {
|
||||
// Weird/experimental kick
|
||||
baseFreq = Math.random() * 0.7; // Full range 35-96 Hz
|
||||
@ -180,6 +207,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = Math.random(); // Any shape
|
||||
phaseDistortion = Math.random() * 0.7; // Any amount
|
||||
distortion = 0; // Clean
|
||||
attackTime = Math.random(); // Full range (0.5-50ms)
|
||||
attackCurve = Math.random(); // Full range (0.0-1.0)
|
||||
ampDecayCurve = Math.random(); // Full range (0.0-1.0)
|
||||
pitchDecayCurve = Math.random(); // Full range (0.0-1.0)
|
||||
} else {
|
||||
// Dirty/distorted kick (only 15% of the time!)
|
||||
baseFreq = 0.1 + Math.random() * 0.5; // 44-84 Hz (low to mid)
|
||||
@ -198,6 +229,10 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveShape = 0.3 + Math.random() * 0.5; // Varied shapes
|
||||
phaseDistortion = 0.2 + Math.random() * 0.6; // Good amount
|
||||
distortion = 0.3 + Math.random() * 0.5; // Only this style has distortion
|
||||
attackTime = 0.1 + Math.random() * 0.4; // Medium attack (5-25ms)
|
||||
attackCurve = 0.4 + Math.random() * 0.4; // Medium-sharp attack (0.4-0.8)
|
||||
ampDecayCurve = 0.3 + Math.random() * 0.4; // Medium varied (0.3-0.7)
|
||||
pitchDecayCurve = 0.3 + Math.random() * 0.4; // Medium varied (0.3-0.7)
|
||||
}
|
||||
|
||||
return {
|
||||
@ -218,10 +253,14 @@ export class BassDrum909 implements SynthEngine {
|
||||
phaseDistortion,
|
||||
distortion,
|
||||
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
|
||||
attackTime,
|
||||
attackCurve,
|
||||
ampDecayCurve,
|
||||
pitchDecayCurve,
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: BassDrum909Params, mutationAmount: number = 0.15, pitchLock?: PitchLock): BassDrum909Params {
|
||||
mutateParams(params: BassDrumParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): BassDrumParams {
|
||||
const mutate = (value: number, amount: number = mutationAmount): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
@ -244,11 +283,15 @@ export class BassDrum909 implements SynthEngine {
|
||||
phaseDistortion: mutate(params.phaseDistortion, 0.25),
|
||||
distortion: mutate(params.distortion, 0.2),
|
||||
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
|
||||
attackTime: mutate(params.attackTime, 0.15),
|
||||
attackCurve: mutate(params.attackCurve, 0.2),
|
||||
ampDecayCurve: mutate(params.ampDecayCurve, 0.2),
|
||||
pitchDecayCurve: mutate(params.pitchDecayCurve, 0.2),
|
||||
};
|
||||
}
|
||||
|
||||
generate(
|
||||
params: BassDrum909Params,
|
||||
params: BassDrumParams,
|
||||
sampleRate: number,
|
||||
duration: number,
|
||||
pitchLock?: PitchLock
|
||||
@ -272,7 +315,7 @@ export class BassDrum909 implements SynthEngine {
|
||||
// Pitch decay time scaled by duration (50ms to 200ms)
|
||||
const pitchDecayTime = (0.05 + params.pitchDecay * 0.15) * duration;
|
||||
|
||||
// Amplitude decay time scaled by duration (300ms default in original)
|
||||
// Amplitude decay time scaled by duration
|
||||
const ampDecayTime = (0.2 + params.decay * 1.8) * duration;
|
||||
|
||||
// Noise decay time
|
||||
@ -282,8 +325,8 @@ export class BassDrum909 implements SynthEngine {
|
||||
const bodyFreq = 40 + params.bodyFreq * 160;
|
||||
const bodyDecayTime = (0.1 + params.bodyDecay * 0.4) * duration; // 100ms to 500ms
|
||||
|
||||
// Attack time (5ms ramp like original)
|
||||
const attackTime = 0.005;
|
||||
// Attack time (0.5ms to 50ms, scaled by duration)
|
||||
const attackTime = (0.0005 + params.attackTime * 0.0495) * duration;
|
||||
|
||||
// Click decay (very fast for transient)
|
||||
const clickDecayTime = 0.003 + params.click * 0.007; // 3ms to 10ms
|
||||
@ -321,8 +364,8 @@ export class BassDrum909 implements SynthEngine {
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
// Pitch envelope: exponential decay
|
||||
const pitchEnv = Math.exp(-t / pitchDecayTime);
|
||||
// Pitch envelope: curved decay
|
||||
const pitchEnv = this.applyDecayCurve(t, pitchDecayTime, params.pitchDecayCurve);
|
||||
|
||||
// When pitch locked: start at locked freq, decay down
|
||||
// When not locked: start higher, decay to base
|
||||
@ -369,13 +412,15 @@ export class BassDrum909 implements SynthEngine {
|
||||
waveform = triangle * (1 - mix) + square * mix;
|
||||
}
|
||||
|
||||
// Attack envelope (5ms linear ramp)
|
||||
const attackEnv = t < attackTime ? t / attackTime : 1.0;
|
||||
// Attack envelope (curved)
|
||||
const attackEnv = t < attackTime
|
||||
? this.applyAttackCurve(t, attackTime, params.attackCurve)
|
||||
: 1.0;
|
||||
|
||||
// Amplitude envelope: exponential decay with attack
|
||||
const ampEnv = Math.exp(-t / ampDecayTime) * attackEnv;
|
||||
// Amplitude envelope: curved decay with attack
|
||||
const ampEnv = this.applyDecayCurve(t, ampDecayTime, params.ampDecayCurve) * attackEnv;
|
||||
|
||||
// 1. Morphed oscillator (main tone) - reduced level
|
||||
// 1. Morphed oscillator (main tone)
|
||||
let signal = waveform * ampEnv * 0.6;
|
||||
|
||||
// 1b. Add harmonics (2nd and 3rd) for more character
|
||||
@ -438,7 +483,7 @@ export class BassDrum909 implements SynthEngine {
|
||||
|
||||
// 6. Optional distortion/saturation (only if distortion > 0.2)
|
||||
if (params.distortion > 0.2) {
|
||||
const distAmount = 1 + params.distortion * 3; // Reduced range
|
||||
const distAmount = 1 + params.distortion * 3;
|
||||
signal = this.waveshaper(signal * distAmount, distAmount) / distAmount;
|
||||
}
|
||||
|
||||
@ -469,6 +514,19 @@ export class BassDrum909 implements SynthEngine {
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
private applyDecayCurve(t: number, decayTime: number, curveParam: number): number {
|
||||
const normalized = Math.min(t / decayTime, 5);
|
||||
const baseEnv = Math.exp(-normalized);
|
||||
const exponent = 0.4 + curveParam * 2.1;
|
||||
return Math.pow(baseEnv, exponent);
|
||||
}
|
||||
|
||||
private applyAttackCurve(t: number, attackTime: number, curveParam: number): number {
|
||||
const normalized = Math.min(t / attackTime, 1);
|
||||
const exponent = 0.4 + curveParam * 2.1;
|
||||
return Math.pow(normalized, exponent);
|
||||
}
|
||||
|
||||
private bandpassFilter(
|
||||
input: number,
|
||||
freq: number,
|
||||
@ -526,9 +584,8 @@ export class BassDrum909 implements SynthEngine {
|
||||
}
|
||||
|
||||
private waveshaper(x: number, amount: number): number {
|
||||
// Original 909 distortion curve: ((π + amount) * x) / (π + amount * |x|)
|
||||
const PI = Math.PI;
|
||||
if (Math.abs(x) < 0.001) return x; // Avoid division issues
|
||||
if (Math.abs(x) < 0.001) return x;
|
||||
return ((PI + amount) * x) / (PI + amount * Math.abs(x));
|
||||
}
|
||||
|
||||
179
src/lib/audio/engines/HiHat.ts
Normal file
179
src/lib/audio/engines/HiHat.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
|
||||
interface HiHatParams {
|
||||
// Decay time (0 = closed/tight, 1 = open/long)
|
||||
decay: number;
|
||||
|
||||
// Tone/brightness (filter cutoff frequency)
|
||||
tone: number;
|
||||
|
||||
// Noise mix (0 = pure metallic, 1 = more noise)
|
||||
noise: number;
|
||||
|
||||
// Base pitch
|
||||
pitch: number;
|
||||
|
||||
// Timbre (shifts frequency ratios for different metallic characters)
|
||||
timbre: number;
|
||||
}
|
||||
|
||||
export class HiHat implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Hi-Hat';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Metallic hi-hat from square wave oscillators and noise';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): HiHatParams {
|
||||
return {
|
||||
decay: Math.random(),
|
||||
tone: 0.4 + Math.random() * 0.6,
|
||||
noise: 0.3 + Math.random() * 0.5,
|
||||
pitch: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.4 + Math.random() * 0.2,
|
||||
timbre: Math.random(),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: HiHatParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): HiHatParams {
|
||||
const mutate = (value: number, amount: number = mutationAmount): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
return {
|
||||
decay: mutate(params.decay, 0.25),
|
||||
tone: mutate(params.tone, 0.25),
|
||||
noise: mutate(params.noise, 0.2),
|
||||
pitch: pitchLock ? params.pitch : mutate(params.pitch, 0.15),
|
||||
timbre: mutate(params.timbre, 0.25),
|
||||
};
|
||||
}
|
||||
|
||||
generate(
|
||||
params: HiHatParams,
|
||||
sampleRate: number,
|
||||
duration: number,
|
||||
pitchLock?: PitchLock
|
||||
): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const left = new Float32Array(numSamples);
|
||||
const right = new Float32Array(numSamples);
|
||||
|
||||
// Decay time: 30ms (tight closed) to 800ms (long open)
|
||||
const decayTime = (0.03 + params.decay * 0.77) * duration;
|
||||
|
||||
// Bandpass filter frequencies for metallic character (based on classic hi-hat synthesis)
|
||||
// Pitch shifts all frequencies, timbre changes the spread
|
||||
const basePitch = pitchLock ? pitchLock.frequency : 600 + params.pitch * 800;
|
||||
const pitchScale = basePitch / 1000; // normalize to ~1000Hz center
|
||||
|
||||
// Timbre controls frequency spread: 0 = tight/focused, 1 = wide/trashy
|
||||
const spread = 0.7 + params.timbre * 0.6;
|
||||
|
||||
const filterFreqs = [
|
||||
4500 * pitchScale * spread,
|
||||
6200 * pitchScale * spread,
|
||||
7800 * pitchScale * spread,
|
||||
9600 * pitchScale * spread,
|
||||
11400 * pitchScale * spread,
|
||||
13200 * pitchScale * spread,
|
||||
];
|
||||
|
||||
// Tone controls overall brightness via highpass cutoff
|
||||
const hpFreq = 4000 + params.tone * 6000; // 4kHz to 10kHz
|
||||
|
||||
for (let channel = 0; channel < 2; channel++) {
|
||||
const output = channel === 0 ? left : right;
|
||||
|
||||
// Bandpass filter states (6 filters, 2 states each)
|
||||
const bpStates1 = new Array(6).fill(0);
|
||||
const bpStates2 = new Array(6).fill(0);
|
||||
|
||||
// Highpass filter state
|
||||
let hp1 = 0, hp2 = 0;
|
||||
|
||||
// Stereo variation in filter frequencies
|
||||
const stereoShift = channel === 0 ? 0.995 : 1.005;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
// Exponential decay envelope
|
||||
const env = Math.exp(-t / decayTime);
|
||||
|
||||
// White noise source
|
||||
const noise = Math.random() * 2 - 1;
|
||||
|
||||
// Pass noise through 6 bandpass filters at different frequencies
|
||||
let filtered = 0;
|
||||
for (let j = 0; j < filterFreqs.length; j++) {
|
||||
const freq = filterFreqs[j] * stereoShift;
|
||||
const q = 8 + j * 2; // Higher Q for higher frequencies
|
||||
|
||||
const freqNorm = Math.min(freq / sampleRate, 0.48);
|
||||
const f = 2 * Math.sin(Math.PI * freqNorm);
|
||||
const qRecip = 1 / q;
|
||||
|
||||
const lp = bpStates2[j] + f * bpStates1[j];
|
||||
const hp = noise - lp - qRecip * bpStates1[j];
|
||||
const bp = f * hp + bpStates1[j];
|
||||
|
||||
bpStates1[j] = Math.max(-2, Math.min(2, bp));
|
||||
bpStates2[j] = Math.max(-2, Math.min(2, lp));
|
||||
|
||||
filtered += bp;
|
||||
}
|
||||
|
||||
// Average the bandpass outputs
|
||||
let sample = filtered / filterFreqs.length;
|
||||
|
||||
// Add raw noise for more character (controlled by noise param)
|
||||
sample = sample * (1 - params.noise * 0.5) + noise * params.noise * 0.3;
|
||||
|
||||
// Apply envelope
|
||||
sample *= env;
|
||||
|
||||
// Highpass filter for brightness control
|
||||
const hpNorm = Math.min(hpFreq / sampleRate, 0.48);
|
||||
const hpA = 1 - hpNorm * 2;
|
||||
const hpFiltered = hpA * (hp1 + sample - hp2);
|
||||
hp2 = sample;
|
||||
hp1 = hpFiltered;
|
||||
|
||||
sample = hpFiltered;
|
||||
|
||||
// Soft clip
|
||||
if (sample > 0.7) sample = 0.7 + (sample - 0.7) * 0.3;
|
||||
if (sample < -0.7) sample = -0.7 + (sample + 0.7) * 0.3;
|
||||
|
||||
output[i] = sample;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
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 normGain = 0.5 / peak;
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
left[i] *= normGain;
|
||||
right[i] *= normGain;
|
||||
}
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
private freqToParam(freq: number): number {
|
||||
// Map frequency to 0-1 range (600-1400 Hz)
|
||||
return Math.max(0, Math.min(1, (freq - 600) / 800));
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ interface NoiseDrumParams {
|
||||
|
||||
export class NoiseDrum implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Noise Drum';
|
||||
return 'NPerc';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
|
||||
523
src/lib/audio/engines/PhaseDistortionFM.ts
Normal file
523
src/lib/audio/engines/PhaseDistortionFM.ts
Normal file
@ -0,0 +1,523 @@
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum PDWaveform {
|
||||
Sine,
|
||||
SawUp,
|
||||
SawDown,
|
||||
Pulse,
|
||||
DoubleSine,
|
||||
ResonantSaw,
|
||||
}
|
||||
|
||||
enum PDAlgorithm {
|
||||
Single, // Single oscillator with PD
|
||||
Dual, // Two oscillators, different waveforms
|
||||
Detune, // Two slightly detuned oscillators
|
||||
Octave, // Oscillator + octave up
|
||||
}
|
||||
|
||||
enum LFOWaveform {
|
||||
Sine,
|
||||
Triangle,
|
||||
Square,
|
||||
Saw,
|
||||
SampleHold,
|
||||
RandomWalk,
|
||||
}
|
||||
|
||||
interface PDOscillatorParams {
|
||||
waveform: PDWaveform;
|
||||
level: number;
|
||||
detune: number; // in cents
|
||||
dcw: number; // Digitally Controlled Waveshaping (0-1, controls brightness)
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
dcwAttack: number;
|
||||
dcwDecay: number;
|
||||
dcwSustain: number;
|
||||
dcwRelease: number;
|
||||
}
|
||||
|
||||
interface LFOParams {
|
||||
rate: number;
|
||||
depth: number;
|
||||
waveform: LFOWaveform;
|
||||
target: 'pitch' | 'dcw';
|
||||
}
|
||||
|
||||
export interface PhaseDistortionFMParams {
|
||||
baseFreq: number;
|
||||
algorithm: PDAlgorithm;
|
||||
oscillators: [PDOscillatorParams, PDOscillatorParams];
|
||||
lfo: LFOParams;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
||||
private static workletLoaded = false;
|
||||
private static workletURL: string | null = null;
|
||||
|
||||
getName(): string {
|
||||
return 'PD';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Casio CZ-style Phase Distortion synthesis with DCW envelopes';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
generate(params: PhaseDistortionFMParams, 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;
|
||||
|
||||
let osc1PhaseL = 0;
|
||||
let osc1PhaseR = 0;
|
||||
let osc2PhaseL = 0;
|
||||
let osc2PhaseR = 0;
|
||||
|
||||
let lfoPhaseL = 0;
|
||||
let lfoPhaseR = Math.PI * params.stereoWidth * 0.3;
|
||||
|
||||
let lfoSampleHoldValue = Math.random() * 2 - 1;
|
||||
let lfoSampleHoldCounter = 0;
|
||||
const lfoSampleHoldInterval = Math.max(1, Math.floor(sampleRate / (params.lfo.rate * 4)));
|
||||
let lfoRandomWalkCurrent = Math.random() * 2 - 1;
|
||||
let lfoRandomWalkTarget = Math.random() * 2 - 1;
|
||||
let lfoRandomWalkCounter = 0;
|
||||
const lfoRandomWalkInterval = Math.max(1, Math.floor(sampleRate / (params.lfo.rate * 2)));
|
||||
|
||||
let dcBlockerPrevL = 0;
|
||||
let dcBlockerAccL = 0;
|
||||
let dcBlockerPrevR = 0;
|
||||
let dcBlockerAccR = 0;
|
||||
const dcBlockerCutoff = 0.995;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
const env1 = this.calculateEnvelope(t, duration, params.oscillators[0]);
|
||||
const env2 = this.calculateEnvelope(t, duration, params.oscillators[1]);
|
||||
|
||||
const dcw1 = this.calculateDCWEnvelope(t, duration, params.oscillators[0]);
|
||||
const dcw2 = this.calculateDCWEnvelope(t, duration, params.oscillators[1]);
|
||||
|
||||
// Update LFO states
|
||||
lfoSampleHoldCounter++;
|
||||
if (lfoSampleHoldCounter >= lfoSampleHoldInterval) {
|
||||
lfoSampleHoldValue = Math.random() * 2 - 1;
|
||||
lfoSampleHoldCounter = 0;
|
||||
}
|
||||
|
||||
lfoRandomWalkCounter++;
|
||||
if (lfoRandomWalkCounter >= lfoRandomWalkInterval) {
|
||||
lfoRandomWalkTarget = Math.random() * 2 - 1;
|
||||
lfoRandomWalkCounter = 0;
|
||||
}
|
||||
const walkSpeed = 0.01;
|
||||
lfoRandomWalkCurrent += (lfoRandomWalkTarget - lfoRandomWalkCurrent) * walkSpeed;
|
||||
|
||||
const lfoL = this.generateLFO(lfoPhaseL, params.lfo.waveform, lfoSampleHoldValue, lfoRandomWalkCurrent);
|
||||
const lfoR = this.generateLFO(lfoPhaseR, params.lfo.waveform, lfoSampleHoldValue, lfoRandomWalkCurrent);
|
||||
|
||||
const lfoModL = lfoL * params.lfo.depth;
|
||||
const lfoModR = lfoR * params.lfo.depth;
|
||||
|
||||
let pitchModL = 0, pitchModR = 0;
|
||||
let dcwMod1 = 0, dcwMod2 = 0;
|
||||
|
||||
if (params.lfo.target === 'pitch') {
|
||||
pitchModL = lfoModL * 0.02;
|
||||
pitchModR = lfoModR * 0.02;
|
||||
} else {
|
||||
dcwMod1 = lfoModL * 0.3;
|
||||
dcwMod2 = lfoModL * 0.3;
|
||||
}
|
||||
|
||||
const [sampleL, sampleR] = this.processAlgorithm(
|
||||
params.algorithm,
|
||||
params.oscillators,
|
||||
[osc1PhaseL, osc2PhaseL],
|
||||
[osc1PhaseR, osc2PhaseR],
|
||||
[env1, env2],
|
||||
[Math.max(0, Math.min(1, dcw1 + dcwMod1)), Math.max(0, Math.min(1, dcw2 + dcwMod2))]
|
||||
);
|
||||
|
||||
let outL = sampleL;
|
||||
let outR = sampleR;
|
||||
|
||||
outL = this.softClip(outL);
|
||||
outR = this.softClip(outR);
|
||||
|
||||
// DC blocker
|
||||
const dcFilteredL = outL - dcBlockerPrevL + dcBlockerCutoff * dcBlockerAccL;
|
||||
dcBlockerPrevL = outL;
|
||||
dcBlockerAccL = dcFilteredL;
|
||||
|
||||
const dcFilteredR = outR - dcBlockerPrevR + dcBlockerCutoff * dcBlockerAccR;
|
||||
dcBlockerPrevR = outR;
|
||||
dcBlockerAccR = dcFilteredR;
|
||||
|
||||
leftBuffer[i] = dcFilteredL;
|
||||
rightBuffer[i] = dcFilteredR;
|
||||
|
||||
// Advance oscillator phases
|
||||
const detune1 = Math.pow(2, params.oscillators[0].detune / 1200);
|
||||
const detune2 = Math.pow(2, params.oscillators[1].detune / 1200);
|
||||
|
||||
const osc1FreqL = leftFreq * detune1 * (1 + pitchModL);
|
||||
const osc1FreqR = rightFreq * detune1 * (1 + pitchModR);
|
||||
const osc2FreqL = leftFreq * detune2 * (1 + pitchModL);
|
||||
const osc2FreqR = rightFreq * detune2 * (1 + pitchModR);
|
||||
|
||||
osc1PhaseL += (TAU * osc1FreqL) / sampleRate;
|
||||
osc1PhaseR += (TAU * osc1FreqR) / sampleRate;
|
||||
osc2PhaseL += (TAU * osc2FreqL) / sampleRate;
|
||||
osc2PhaseR += (TAU * osc2FreqR) / sampleRate;
|
||||
|
||||
while (osc1PhaseL >= TAU) osc1PhaseL -= TAU;
|
||||
while (osc1PhaseR >= TAU) osc1PhaseR -= TAU;
|
||||
while (osc2PhaseL >= TAU) osc2PhaseL -= TAU;
|
||||
while (osc2PhaseR >= TAU) osc2PhaseR -= TAU;
|
||||
|
||||
lfoPhaseL += (TAU * params.lfo.rate) / sampleRate;
|
||||
lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
|
||||
while (lfoPhaseL >= TAU) lfoPhaseL -= TAU;
|
||||
while (lfoPhaseR >= TAU) lfoPhaseR -= TAU;
|
||||
}
|
||||
|
||||
// Normalize
|
||||
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: PDAlgorithm,
|
||||
oscillators: [PDOscillatorParams, PDOscillatorParams],
|
||||
phasesL: number[],
|
||||
phasesR: number[],
|
||||
envelopes: number[],
|
||||
dcws: number[]
|
||||
): [number, number] {
|
||||
switch (algorithm) {
|
||||
case PDAlgorithm.Single: {
|
||||
// Single oscillator
|
||||
const outL = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const outR = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
return [outL, outR];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Dual: {
|
||||
// Two oscillators with different waveforms
|
||||
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc2L = this.generatePDWaveform(phasesL[1], oscillators[1].waveform, dcws[1])
|
||||
* envelopes[1] * oscillators[1].level;
|
||||
const osc2R = this.generatePDWaveform(phasesR[1], oscillators[1].waveform, dcws[1])
|
||||
* envelopes[1] * oscillators[1].level;
|
||||
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Detune: {
|
||||
// Two slightly detuned oscillators (chorus effect)
|
||||
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc2L = this.generatePDWaveform(phasesL[1], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[1].level;
|
||||
const osc2R = this.generatePDWaveform(phasesR[1], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[1].level;
|
||||
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Octave: {
|
||||
// Oscillator + octave up
|
||||
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0])
|
||||
* envelopes[0] * oscillators[0].level;
|
||||
const osc2L = this.generatePDWaveform(phasesL[1] * 2, oscillators[1].waveform, dcws[1])
|
||||
* envelopes[1] * oscillators[1].level;
|
||||
const osc2R = this.generatePDWaveform(phasesR[1] * 2, oscillators[1].waveform, dcws[1])
|
||||
* envelopes[1] * oscillators[1].level;
|
||||
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
|
||||
}
|
||||
|
||||
default:
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
private generatePDWaveform(phase: number, waveform: PDWaveform, dcw: number): number {
|
||||
const TAU = Math.PI * 2;
|
||||
let normalizedPhase = phase / TAU;
|
||||
normalizedPhase = normalizedPhase - Math.floor(normalizedPhase);
|
||||
|
||||
// Apply phase distortion based on DCW
|
||||
const distortedPhase = this.applyPhaseDistortion(normalizedPhase, dcw);
|
||||
|
||||
switch (waveform) {
|
||||
case PDWaveform.Sine:
|
||||
return Math.sin(distortedPhase * TAU);
|
||||
|
||||
case PDWaveform.SawUp:
|
||||
return distortedPhase * 2 - 1;
|
||||
|
||||
case PDWaveform.SawDown:
|
||||
return 1 - distortedPhase * 2;
|
||||
|
||||
case PDWaveform.Pulse:
|
||||
return distortedPhase < 0.5 ? 1 : -1;
|
||||
|
||||
case PDWaveform.DoubleSine:
|
||||
return Math.sin(distortedPhase * TAU * 2);
|
||||
|
||||
case PDWaveform.ResonantSaw:
|
||||
return this.resonantSaw(distortedPhase, dcw);
|
||||
|
||||
default:
|
||||
return Math.sin(distortedPhase * TAU);
|
||||
}
|
||||
}
|
||||
|
||||
private applyPhaseDistortion(phase: number, dcw: number): number {
|
||||
// Casio CZ-style phase distortion
|
||||
// DCW = 0: no distortion (linear phase)
|
||||
// DCW = 1: maximum distortion (transforms waveform)
|
||||
|
||||
if (dcw < 0.01) return phase;
|
||||
|
||||
// Add a triangle wave to the phase - this is the classic CZ method
|
||||
// The triangle modulates how fast we read through the waveform
|
||||
const trianglePhase = phase < 0.5 ? phase * 2 : 2 - phase * 2;
|
||||
|
||||
// Mix between linear and distorted phase
|
||||
return phase + (trianglePhase - 0.5) * dcw;
|
||||
}
|
||||
|
||||
private resonantSaw(phase: number, brightness: number): number {
|
||||
// Band-limited sawtooth with fewer harmonics for cleaner sound
|
||||
const maxHarmonics = Math.min(4, Math.floor(1 + brightness * 3));
|
||||
let sum = 0;
|
||||
let normalization = 0;
|
||||
for (let n = 1; n <= maxHarmonics; n++) {
|
||||
sum += Math.sin(Math.PI * 2 * phase * n) / n;
|
||||
normalization += 1 / n;
|
||||
}
|
||||
return normalization > 0 ? (sum / normalization) * 0.7 : 0;
|
||||
}
|
||||
|
||||
private calculateEnvelope(t: number, duration: number, osc: PDOscillatorParams): number {
|
||||
const attackTime = Math.max(0.0001, osc.attack * duration);
|
||||
const decayTime = Math.max(0.0001, osc.decay * duration);
|
||||
const releaseTime = Math.max(0.0001, osc.release * duration);
|
||||
const sustainStart = attackTime + decayTime;
|
||||
const releaseStart = duration - releaseTime;
|
||||
|
||||
if (t < attackTime) {
|
||||
return t / attackTime;
|
||||
} else if (t < sustainStart) {
|
||||
const progress = (t - attackTime) / decayTime;
|
||||
return 1 - progress * (1 - osc.sustain);
|
||||
} else if (t < releaseStart) {
|
||||
return osc.sustain;
|
||||
} else {
|
||||
const progress = (t - releaseStart) / releaseTime;
|
||||
return osc.sustain * (1 - progress);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateDCWEnvelope(t: number, duration: number, osc: PDOscillatorParams): number {
|
||||
const attackTime = Math.max(0.0001, osc.dcwAttack * duration);
|
||||
const decayTime = Math.max(0.0001, osc.dcwDecay * duration);
|
||||
const releaseTime = Math.max(0.0001, osc.dcwRelease * duration);
|
||||
const sustainStart = attackTime + decayTime;
|
||||
const releaseStart = duration - releaseTime;
|
||||
|
||||
if (t < attackTime) {
|
||||
return (t / attackTime) * osc.dcw;
|
||||
} else if (t < sustainStart) {
|
||||
const progress = (t - attackTime) / decayTime;
|
||||
return osc.dcw * (1 - progress * (1 - osc.dcwSustain));
|
||||
} else if (t < releaseStart) {
|
||||
return osc.dcw * osc.dcwSustain;
|
||||
} else {
|
||||
const progress = (t - releaseStart) / releaseTime;
|
||||
return osc.dcw * osc.dcwSustain * (1 - progress);
|
||||
}
|
||||
}
|
||||
|
||||
private generateLFO(phase: number, waveform: LFOWaveform, sampleHoldValue: number, randomWalkValue: number): number {
|
||||
const TAU = Math.PI * 2;
|
||||
let normalizedPhase = phase / TAU;
|
||||
normalizedPhase = normalizedPhase - Math.floor(normalizedPhase);
|
||||
|
||||
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:
|
||||
return sampleHoldValue;
|
||||
|
||||
case LFOWaveform.RandomWalk:
|
||||
return randomWalkValue;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): PhaseDistortionFMParams {
|
||||
const algorithm = this.randomInt(0, 3) as PDAlgorithm;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
algorithm,
|
||||
oscillators: [
|
||||
this.randomOscillator(algorithm),
|
||||
this.randomOscillator(algorithm),
|
||||
],
|
||||
lfo: {
|
||||
rate: this.randomRange(0.5, 8),
|
||||
depth: this.randomRange(0, 0.4),
|
||||
waveform: this.randomInt(0, 5) as LFOWaveform,
|
||||
target: this.randomChoice(['pitch', 'dcw'] as const),
|
||||
},
|
||||
stereoWidth: this.randomRange(0.1, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
private randomOscillator(algorithm: PDAlgorithm): PDOscillatorParams {
|
||||
const waveform = this.randomInt(0, 5) as PDWaveform;
|
||||
|
||||
let detune = 0;
|
||||
if (algorithm === PDAlgorithm.Detune) {
|
||||
detune = this.randomRange(-10, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
waveform,
|
||||
level: this.randomRange(0.5, 0.9),
|
||||
detune,
|
||||
dcw: this.randomRange(0.2, 0.8),
|
||||
attack: this.randomRange(0.001, 0.1),
|
||||
decay: this.randomRange(0.02, 0.2),
|
||||
sustain: this.randomRange(0.3, 0.8),
|
||||
release: this.randomRange(0.05, 0.3),
|
||||
dcwAttack: this.randomRange(0.001, 0.08),
|
||||
dcwDecay: this.randomRange(0.02, 0.15),
|
||||
dcwSustain: this.randomRange(0.2, 0.7),
|
||||
dcwRelease: this.randomRange(0.05, 0.25),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: PhaseDistortionFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): PhaseDistortionFMParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
algorithm: Math.random() < 0.1 ? this.randomInt(0, 3) as PDAlgorithm : params.algorithm,
|
||||
oscillators: params.oscillators.map(osc =>
|
||||
this.mutateOscillator(osc, mutationAmount)
|
||||
) as [PDOscillatorParams, PDOscillatorParams],
|
||||
lfo: {
|
||||
rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 15),
|
||||
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', 'dcw'] as const) : params.lfo.target,
|
||||
},
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.7),
|
||||
};
|
||||
}
|
||||
|
||||
private mutateOscillator(osc: PDOscillatorParams, amount: number): PDOscillatorParams {
|
||||
return {
|
||||
waveform: Math.random() < 0.1 ? this.randomInt(0, 5) as PDWaveform : osc.waveform,
|
||||
level: this.mutateValue(osc.level, amount, 0.3, 1.0),
|
||||
detune: this.mutateValue(osc.detune, amount, -20, 20),
|
||||
dcw: this.mutateValue(osc.dcw, amount, 0, 1),
|
||||
attack: this.mutateValue(osc.attack, amount, 0.001, 0.2),
|
||||
decay: this.mutateValue(osc.decay, amount, 0.01, 0.3),
|
||||
sustain: this.mutateValue(osc.sustain, amount, 0.1, 0.95),
|
||||
release: this.mutateValue(osc.release, amount, 0.02, 0.5),
|
||||
dcwAttack: this.mutateValue(osc.dcwAttack, amount, 0.001, 0.15),
|
||||
dcwDecay: this.mutateValue(osc.dcwDecay, amount, 0.01, 0.25),
|
||||
dcwSustain: this.mutateValue(osc.dcwSustain, amount, 0.1, 0.9),
|
||||
dcwRelease: this.mutateValue(osc.dcwRelease, amount, 0.02, 0.4),
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
|
||||
interface Snare909Params {
|
||||
interface SnareParams {
|
||||
// Core frequency (base pitch of the snare)
|
||||
baseFreq: number;
|
||||
|
||||
@ -21,20 +21,20 @@ interface Snare909Params {
|
||||
tuning: number;
|
||||
}
|
||||
|
||||
export class Snare909 implements SynthEngine {
|
||||
export class Snare implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Classic 909-style snare drum with triangle wave and noise';
|
||||
return 'Classic snare drum with triangle wave and noise';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Snare909Params {
|
||||
randomParams(pitchLock?: PitchLock): SnareParams {
|
||||
return {
|
||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
||||
tone: 0.4 + Math.random() * 0.4,
|
||||
@ -46,7 +46,7 @@ export class Snare909 implements SynthEngine {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Snare909Params, mutationAmount: number = 0.15, pitchLock?: PitchLock): Snare909Params {
|
||||
mutateParams(params: SnareParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): SnareParams {
|
||||
const mutate = (value: number, amount: number = mutationAmount): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
@ -63,7 +63,7 @@ export class Snare909 implements SynthEngine {
|
||||
}
|
||||
|
||||
generate(
|
||||
params: Snare909Params,
|
||||
params: SnareParams,
|
||||
sampleRate: number,
|
||||
duration: number,
|
||||
pitchLock?: PitchLock
|
||||
@ -79,7 +79,7 @@ export class Snare909 implements SynthEngine {
|
||||
const tuningFactor = 0.88 + params.tuning * 0.24;
|
||||
const tunedFreq = baseFreq * tuningFactor;
|
||||
|
||||
// Pitch modulation parameters (classic 909: starts 4x higher, drops over 10ms)
|
||||
// Pitch modulation parameters (starts 4x higher, drops over 10ms)
|
||||
const pitchMultiplier = 1 + params.snap * 3; // 1x to 4x
|
||||
const modDuration = 0.008 + params.snap * 0.007; // 8ms to 15ms
|
||||
|
||||
@ -87,14 +87,14 @@ export class Snare909 implements SynthEngine {
|
||||
const tonalDecayTime = (0.05 + params.tonalDecay * 0.15) * duration; // 50ms to 200ms
|
||||
const noiseDecayTime = (0.15 + params.noiseDecay * 0.35) * duration; // 150ms to 500ms
|
||||
|
||||
// Volume and gain staging (matching original 909 implementation)
|
||||
const baseVolume = 0.5; // original default volume
|
||||
const accentGain = baseVolume * (1 + params.accent); // 0.5 to 1.0 (accent can double)
|
||||
// Volume and gain staging
|
||||
const baseVolume = 0.5;
|
||||
const accentGain = baseVolume * (1 + params.accent);
|
||||
|
||||
// Attack time (5ms ramp like original)
|
||||
// Attack time (5ms ramp)
|
||||
const attackTime = 0.005;
|
||||
|
||||
// Notch filter parameters for noise (fixed at 1000Hz like the original)
|
||||
// Notch filter parameters for noise (fixed at 1000Hz)
|
||||
const notchFreq = 1000;
|
||||
const notchQ = 5;
|
||||
|
||||
@ -129,7 +129,7 @@ export class Snare909 implements SynthEngine {
|
||||
? -1 + (2 * phase) / Math.PI
|
||||
: 3 - (2 * phase) / Math.PI;
|
||||
|
||||
// Attack envelope (5ms linear ramp like original)
|
||||
// Attack envelope (5ms linear ramp)
|
||||
const attackEnv = t < attackTime ? t / attackTime : 1.0;
|
||||
|
||||
// Tonal envelope: exponential decay
|
||||
@ -155,16 +155,14 @@ export class Snare909 implements SynthEngine {
|
||||
const noiseEnv = Math.exp(-t / noiseDecayTime);
|
||||
const noiseSignal = notchFiltered.output * noiseEnv;
|
||||
|
||||
// Mix tonal and noise using original 909 gain staging
|
||||
// Original: input (tonal) at volume (0.5), noise at tone (0.25)
|
||||
// tone param controls noise amount: 0 = less noise, 1 = more noise
|
||||
// Mix tonal and noise
|
||||
const tonalGain = baseVolume * (1 - params.tone * 0.5); // 0.5 to 0.25
|
||||
const noiseGain = 0.15 + params.tone * 0.2; // 0.15 to 0.35
|
||||
|
||||
let sample = tonalSignal * tonalGain + noiseSignal * noiseGain;
|
||||
|
||||
// Apply accent
|
||||
sample *= (0.5 + params.accent * 0.5); // additional accent multiplier
|
||||
sample *= (0.5 + params.accent * 0.5);
|
||||
|
||||
// Soft clipping
|
||||
sample = this.softClip(sample);
|
||||
@ -1,6 +1,7 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import { FourOpFM } from './FourOpFM';
|
||||
import { TwoOpFM } from './TwoOpFM';
|
||||
import { PhaseDistortionFM } from './PhaseDistortionFM';
|
||||
import { DubSiren } from './DubSiren';
|
||||
import { Benjolin } from './Benjolin';
|
||||
import { ZzfxEngine } from './ZzfxEngine';
|
||||
@ -10,20 +11,23 @@ import { Sample } from './Sample';
|
||||
import { Input } from './Input';
|
||||
import { KarplusStrong } from './KarplusStrong';
|
||||
import { AdditiveEngine } from './AdditiveEngine';
|
||||
import { Snare909 } from './Snare909';
|
||||
import { BassDrum909 } from './BassDrum909';
|
||||
import { Snare } from './Snare';
|
||||
import { BassDrum } from './BassDrum';
|
||||
import { HiHat } from './HiHat';
|
||||
|
||||
export const engines: SynthEngine[] = [
|
||||
new Sample(),
|
||||
new Input(),
|
||||
new FourOpFM(),
|
||||
new TwoOpFM(),
|
||||
new PhaseDistortionFM(),
|
||||
new DubSiren(),
|
||||
new Benjolin(),
|
||||
new ZzfxEngine(),
|
||||
new NoiseDrum(),
|
||||
new Snare909(),
|
||||
new BassDrum909(),
|
||||
new Snare(),
|
||||
new BassDrum(),
|
||||
new HiHat(),
|
||||
new Ring(),
|
||||
new KarplusStrong(),
|
||||
new AdditiveEngine(),
|
||||
|
||||
449
src/lib/audio/engines/worklets/phase-distortion-fm-processor.js
Normal file
449
src/lib/audio/engines/worklets/phase-distortion-fm-processor.js
Normal file
@ -0,0 +1,449 @@
|
||||
// Phase Distortion FM AudioWorklet Processor
|
||||
// 3-operator FM synthesis with phase distortion and DCW envelopes
|
||||
|
||||
const PDWaveform = {
|
||||
Sawtooth: 0,
|
||||
Square: 1,
|
||||
Resonant1: 2,
|
||||
Resonant2: 3,
|
||||
Resonant3: 4,
|
||||
};
|
||||
|
||||
const PDAlgorithm = {
|
||||
Stack: 0, // 1→2→3
|
||||
Split: 1, // 1→(2+3)
|
||||
Ring: 2, // (1×2)→3
|
||||
Parallel: 3, // 1+2+3
|
||||
HarmonicStack: 4 // 1→2, 1→3
|
||||
};
|
||||
|
||||
const LFOWaveform = {
|
||||
Sine: 0,
|
||||
Triangle: 1,
|
||||
Square: 2,
|
||||
Saw: 3,
|
||||
SampleHold: 4,
|
||||
RandomWalk: 5,
|
||||
};
|
||||
|
||||
class PhaseDistortionFMProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.opPhasesL = [0, 0, 0];
|
||||
this.opPhasesR = [0, 0, 0];
|
||||
this.lfoPhaseL = 0;
|
||||
this.lfoPhaseR = 0;
|
||||
this.lfoSampleHoldValue = 0;
|
||||
this.lfoSampleHoldPhase = 0;
|
||||
this.lfoRandomWalkCurrent = 0;
|
||||
this.lfoRandomWalkTarget = 0;
|
||||
this.dcBlockerL = 0;
|
||||
this.dcBlockerR = 0;
|
||||
this.dcBlockerCutoff = 0.995;
|
||||
this.sampleCount = 0;
|
||||
this.totalSamples = 0;
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
if (e.data.type === 'init') {
|
||||
this.params = e.data.params;
|
||||
this.duration = e.data.duration;
|
||||
this.totalSamples = Math.floor(sampleRate * this.duration);
|
||||
|
||||
this.lfoSampleHoldValue = Math.random() * 2 - 1;
|
||||
this.lfoSampleHoldPhase = 0;
|
||||
this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
|
||||
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
|
||||
|
||||
const stereoOffset = this.params.stereoWidth * 0.1;
|
||||
this.opPhasesL = [0, Math.PI * stereoOffset, 0];
|
||||
this.opPhasesR = [0, Math.PI * stereoOffset * 1.5, 0];
|
||||
this.lfoPhaseL = 0;
|
||||
this.lfoPhaseR = Math.PI * this.params.stereoWidth * 0.3;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
if (!this.params || this.sampleCount >= this.totalSamples) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const output = outputs[0];
|
||||
const leftChannel = output[0];
|
||||
const rightChannel = output[1];
|
||||
const TAU = Math.PI * 2;
|
||||
|
||||
const detune = 1 + (this.params.stereoWidth * 0.001);
|
||||
const leftFreq = this.params.baseFreq / detune;
|
||||
const rightFreq = this.params.baseFreq * detune;
|
||||
|
||||
for (let i = 0; i < leftChannel.length; i++) {
|
||||
if (this.sampleCount >= this.totalSamples) {
|
||||
leftChannel[i] = 0;
|
||||
rightChannel[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const t = this.sampleCount / sampleRate;
|
||||
|
||||
const env1 = this.calculateEnvelope(t, this.params.operators[0]);
|
||||
const env2 = this.calculateEnvelope(t, this.params.operators[1]);
|
||||
const env3 = this.calculateEnvelope(t, this.params.operators[2]);
|
||||
|
||||
const dcw1 = this.calculateDCWEnvelope(t, this.params.operators[0]);
|
||||
const dcw2 = this.calculateDCWEnvelope(t, this.params.operators[1]);
|
||||
const dcw3 = this.calculateDCWEnvelope(t, this.params.operators[2]);
|
||||
|
||||
const lfoL = this.generateLFO(this.lfoPhaseL, this.params.lfo.waveform, this.params.lfo.rate);
|
||||
const lfoR = this.generateLFO(this.lfoPhaseR, this.params.lfo.waveform, this.params.lfo.rate);
|
||||
const lfoModL = lfoL * this.params.lfo.depth;
|
||||
const lfoModR = lfoR * this.params.lfo.depth;
|
||||
|
||||
let pitchModL = 0, pitchModR = 0;
|
||||
let ampModL = 1, ampModR = 1;
|
||||
let modIndexMod = 0;
|
||||
|
||||
if (this.params.lfo.target === 'pitch') {
|
||||
pitchModL = lfoModL * 0.02;
|
||||
pitchModR = lfoModR * 0.02;
|
||||
} else if (this.params.lfo.target === 'amplitude') {
|
||||
ampModL = 1 + lfoModL * 0.5;
|
||||
ampModR = 1 + lfoModR * 0.5;
|
||||
} else {
|
||||
modIndexMod = lfoModL;
|
||||
}
|
||||
|
||||
const [sampleL, sampleR] = this.processAlgorithm(
|
||||
this.params.algorithm,
|
||||
this.opPhasesL,
|
||||
this.opPhasesR,
|
||||
[env1, env2, env3],
|
||||
[dcw1, dcw2, dcw3],
|
||||
modIndexMod
|
||||
);
|
||||
|
||||
const gainCompensation = this.getAlgorithmGainCompensation(this.params.algorithm);
|
||||
let outL = sampleL * gainCompensation * ampModL;
|
||||
let outR = sampleR * gainCompensation * ampModR;
|
||||
|
||||
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;
|
||||
|
||||
leftChannel[i] = dcFilteredL * 0.85;
|
||||
rightChannel[i] = dcFilteredR * 0.85;
|
||||
|
||||
for (let op = 0; op < 3; op++) {
|
||||
const opFreqL = leftFreq * this.params.operators[op].ratio * (1 + pitchModL);
|
||||
const opFreqR = rightFreq * this.params.operators[op].ratio * (1 + pitchModR);
|
||||
this.opPhasesL[op] += (TAU * opFreqL) / sampleRate;
|
||||
this.opPhasesR[op] += (TAU * opFreqR) / sampleRate;
|
||||
if (this.opPhasesL[op] > TAU * 1000) this.opPhasesL[op] -= TAU * 1000;
|
||||
if (this.opPhasesR[op] > TAU * 1000) this.opPhasesR[op] -= TAU * 1000;
|
||||
}
|
||||
|
||||
this.lfoPhaseL += (TAU * this.params.lfo.rate) / sampleRate;
|
||||
this.lfoPhaseR += (TAU * this.params.lfo.rate) / sampleRate;
|
||||
|
||||
this.sampleCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
processAlgorithm(algorithm, phasesL, phasesR, envelopes, dcwEnvs, modIndexMod) {
|
||||
const baseModIndex = 3.0;
|
||||
const modScale = baseModIndex * (1 + modIndexMod * 2);
|
||||
const ops = this.params.operators;
|
||||
|
||||
switch (algorithm) {
|
||||
case PDAlgorithm.Stack: {
|
||||
// 1→2→3
|
||||
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
|
||||
const mod2PhaseL = phasesL[1] + modScale * mod1L;
|
||||
const mod2PhaseR = phasesR[1] + modScale * mod1R;
|
||||
const mod2L = this.generateWaveform(mod2PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
const mod2R = this.generateWaveform(mod2PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
|
||||
const outPhaseL = phasesL[2] + modScale * 0.7 * mod2L;
|
||||
const outPhaseR = phasesR[2] + modScale * 0.7 * mod2R;
|
||||
const outL = this.generateWaveform(outPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
const outR = this.generateWaveform(outPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
|
||||
return [outL, outR];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Split: {
|
||||
// 1→(2+3)
|
||||
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
|
||||
const car1PhaseL = phasesL[1] + modScale * mod1L;
|
||||
const car1PhaseR = phasesR[1] + modScale * mod1R;
|
||||
const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
|
||||
const car2PhaseL = phasesL[2] + modScale * mod1L;
|
||||
const car2PhaseR = phasesR[2] + modScale * mod1R;
|
||||
const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
|
||||
return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Ring: {
|
||||
// (1×2)→3
|
||||
const osc1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
const osc1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
|
||||
const osc2L = this.generateWaveform(phasesL[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
const osc2R = this.generateWaveform(phasesR[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
|
||||
const ringModL = osc1L * osc2L * this.params.ringModAmount;
|
||||
const ringModR = osc1R * osc2R * this.params.ringModAmount;
|
||||
|
||||
const carPhaseL = phasesL[2] + modScale * ringModL;
|
||||
const carPhaseR = phasesR[2] + modScale * ringModR;
|
||||
const outL = this.generateWaveform(carPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
const outR = this.generateWaveform(carPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
|
||||
return [outL, outR];
|
||||
}
|
||||
|
||||
case PDAlgorithm.Parallel: {
|
||||
// 1+2+3
|
||||
let sumL = 0, sumR = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sumL += this.generateWaveform(phasesL[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
|
||||
* envelopes[i] * ops[i].level;
|
||||
sumR += this.generateWaveform(phasesR[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
|
||||
* envelopes[i] * ops[i].level;
|
||||
}
|
||||
return [sumL * 0.577, sumR * 0.577];
|
||||
}
|
||||
|
||||
case PDAlgorithm.HarmonicStack: {
|
||||
// 1→2, 1→3
|
||||
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
|
||||
* envelopes[0] * ops[0].level;
|
||||
|
||||
const car1PhaseL = phasesL[1] + modScale * mod1L;
|
||||
const car1PhaseR = phasesR[1] + modScale * mod1R;
|
||||
const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
|
||||
* envelopes[1] * ops[1].level;
|
||||
|
||||
const car2PhaseL = phasesL[2] + modScale * 0.8 * mod1L;
|
||||
const car2PhaseR = phasesR[2] + modScale * 0.8 * mod1R;
|
||||
const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
|
||||
* envelopes[2] * ops[2].level;
|
||||
|
||||
return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
|
||||
}
|
||||
|
||||
default:
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
generateWaveform(phase, waveform, distortion, dcw) {
|
||||
const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
|
||||
const warpedPhase = this.applyPhaseDistortion(normalizedPhase, distortion, dcw);
|
||||
|
||||
switch (waveform) {
|
||||
case PDWaveform.Sawtooth:
|
||||
return warpedPhase * 2 - 1;
|
||||
|
||||
case PDWaveform.Square:
|
||||
return warpedPhase < 0.5 ? 1 : -1;
|
||||
|
||||
case PDWaveform.Resonant1:
|
||||
return this.resonantSaw(warpedPhase, dcw);
|
||||
|
||||
case PDWaveform.Resonant2:
|
||||
return this.resonantPulse(warpedPhase, dcw);
|
||||
|
||||
case PDWaveform.Resonant3:
|
||||
return this.doubleResonant(warpedPhase, dcw);
|
||||
|
||||
default:
|
||||
return warpedPhase * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
applyPhaseDistortion(phase, distortion, dcw) {
|
||||
const warpAmount = distortion * dcw;
|
||||
|
||||
if (phase < 0.5) {
|
||||
return phase * (1 + warpAmount * (1 - 2 * phase));
|
||||
} else {
|
||||
return 0.5 + (phase - 0.5) * (1 + warpAmount * (2 * phase - 1));
|
||||
}
|
||||
}
|
||||
|
||||
resonantSaw(phase, brightness) {
|
||||
const harmonics = Math.floor(1 + brightness * 8);
|
||||
let sum = 0;
|
||||
for (let n = 1; n <= harmonics; n++) {
|
||||
sum += Math.sin(Math.PI * 2 * phase * n) / n;
|
||||
}
|
||||
return sum * 0.5;
|
||||
}
|
||||
|
||||
resonantPulse(phase, brightness) {
|
||||
const pulseWidth = 0.5 - brightness * 0.3;
|
||||
const harmonics = Math.floor(1 + brightness * 6);
|
||||
let sum = 0;
|
||||
for (let n = 1; n <= harmonics; n += 2) {
|
||||
sum += Math.sin(Math.PI * 2 * phase * n) * Math.cos(Math.PI * n * pulseWidth) / n;
|
||||
}
|
||||
return sum * 0.6;
|
||||
}
|
||||
|
||||
doubleResonant(phase, brightness) {
|
||||
const harm1 = this.resonantSaw(phase, brightness);
|
||||
const harm2 = this.resonantSaw((phase + 0.5) % 1, brightness * 0.7);
|
||||
return (harm1 + harm2 * 0.5) * 0.6;
|
||||
}
|
||||
|
||||
calculateEnvelope(t, op) {
|
||||
const attackTime = op.attack * this.duration;
|
||||
const decayTime = op.decay * this.duration;
|
||||
const releaseTime = op.release * this.duration;
|
||||
const sustainStart = attackTime + decayTime;
|
||||
const releaseStart = this.duration - releaseTime;
|
||||
|
||||
if (t < attackTime) {
|
||||
return t / attackTime;
|
||||
} else if (t < sustainStart) {
|
||||
const progress = (t - attackTime) / decayTime;
|
||||
return 1 - progress * (1 - op.sustain);
|
||||
} else if (t < releaseStart) {
|
||||
return op.sustain;
|
||||
} else {
|
||||
const progress = (t - releaseStart) / releaseTime;
|
||||
return op.sustain * (1 - progress);
|
||||
}
|
||||
}
|
||||
|
||||
calculateDCWEnvelope(t, op) {
|
||||
const attackTime = op.dcwAttack * this.duration;
|
||||
const decayTime = op.dcwDecay * this.duration;
|
||||
const releaseTime = op.dcwRelease * this.duration;
|
||||
const sustainStart = attackTime + decayTime;
|
||||
const releaseStart = this.duration - releaseTime;
|
||||
|
||||
if (t < attackTime) {
|
||||
return t / attackTime;
|
||||
} else if (t < sustainStart) {
|
||||
const progress = (t - attackTime) / decayTime;
|
||||
return 1 - progress * (1 - op.dcwSustain);
|
||||
} else if (t < releaseStart) {
|
||||
return op.dcwSustain;
|
||||
} else {
|
||||
const progress = (t - releaseStart) / releaseTime;
|
||||
return op.dcwSustain * (1 - progress);
|
||||
}
|
||||
}
|
||||
|
||||
generateLFO(phase, waveform, rate) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
softClip(x) {
|
||||
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);
|
||||
}
|
||||
|
||||
getAlgorithmGainCompensation(algorithm) {
|
||||
switch (algorithm) {
|
||||
case PDAlgorithm.Stack:
|
||||
return 0.75;
|
||||
case PDAlgorithm.Split:
|
||||
case PDAlgorithm.HarmonicStack:
|
||||
return 0.8;
|
||||
case PDAlgorithm.Ring:
|
||||
return 0.7;
|
||||
case PDAlgorithm.Parallel:
|
||||
return 0.65;
|
||||
default:
|
||||
return 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('phase-distortion-fm-processor', PhaseDistortionFMProcessor);
|
||||
Reference in New Issue
Block a user