current kick generator
This commit is contained in:
547
src/lib/audio/engines/BassDrum909.ts
Normal file
547
src/lib/audio/engines/BassDrum909.ts
Normal file
@ -0,0 +1,547 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
|
||||
interface BassDrum909Params {
|
||||
// Core frequency (base pitch of the kick)
|
||||
baseFreq: number;
|
||||
|
||||
// Pitch envelope (how much frequency sweeps down)
|
||||
pitchMod: number;
|
||||
|
||||
// Pitch envelope speed
|
||||
pitchDecay: number;
|
||||
|
||||
// Amplitude decay time
|
||||
decay: number;
|
||||
|
||||
// Click/snap amount (high freq transient)
|
||||
click: number;
|
||||
|
||||
// Attack noise burst (sharp transient)
|
||||
attackNoise: number;
|
||||
attackNoiseFreq: number; // Filter frequency for attack noise
|
||||
|
||||
// Body resonance
|
||||
bodyResonance: number;
|
||||
bodyFreq: number;
|
||||
|
||||
// Body tone variation
|
||||
bodyDecay: number;
|
||||
|
||||
// Noise amount
|
||||
noise: number;
|
||||
|
||||
// Noise decay
|
||||
noiseDecay: number;
|
||||
|
||||
// Harmonic content
|
||||
harmonics: number;
|
||||
|
||||
// Wave shape (0 = sine, 0.5 = triangle, 1 = square)
|
||||
waveShape: number;
|
||||
|
||||
// Phase distortion amount
|
||||
phaseDistortion: number;
|
||||
|
||||
// Distortion amount (0 = clean, 1 = heavy)
|
||||
distortion: number;
|
||||
|
||||
// Tuning offset
|
||||
tuning: number;
|
||||
}
|
||||
|
||||
export class BassDrum909 implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Kick';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Versatile kick drum synthesizer with varied styles from sub to tom';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): BassDrum909Params {
|
||||
// Choose a kick character/style
|
||||
const styleRoll = Math.random();
|
||||
|
||||
let baseFreq: number, pitchMod: number, pitchDecay: number, decay: number;
|
||||
let click: number, attackNoise: number, attackNoiseFreq: number;
|
||||
let bodyResonance: number, bodyFreq: number, bodyDecay: number;
|
||||
let noise: number, noiseDecay: number, harmonics: number;
|
||||
let waveShape: number, phaseDistortion: number, distortion: number;
|
||||
|
||||
if (styleRoll < 0.08) {
|
||||
// Tom/bongo style (starts HIGH, decays VERY fast - rare!)
|
||||
baseFreq = 0.6 + Math.random() * 0.4; // 83-115 Hz (very high!)
|
||||
pitchMod = 0.8 + Math.random() * 0.2; // Extreme pitch sweep
|
||||
pitchDecay = 0.01 + Math.random() * 0.08; // Ultra fast pitch decay (10-90ms)
|
||||
decay = 0.05 + Math.random() * 0.25; // Very short decay
|
||||
click = 0.3 + Math.random() * 0.5; // Good click
|
||||
attackNoise = 0.4 + Math.random() * 0.6; // Strong attack
|
||||
attackNoiseFreq = 0.7 + Math.random() * 0.3; // High filtered
|
||||
bodyResonance = 0.1 + Math.random() * 0.3;
|
||||
bodyFreq = 0.5 + Math.random() * 0.5;
|
||||
bodyDecay = 0.05 + Math.random() * 0.25; // Short body decay
|
||||
noise = Math.random() * 0.08;
|
||||
noiseDecay = 0.02 + Math.random() * 0.15; // Fast noise decay
|
||||
harmonics = 0.2 + Math.random() * 0.5; // Lots of harmonics
|
||||
waveShape = 0.3 + Math.random() * 0.4; // Triangle-ish
|
||||
phaseDistortion = 0.1 + Math.random() * 0.4; // Some phase distortion
|
||||
distortion = 0; // Clean
|
||||
} 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)
|
||||
pitchMod = 0.7 + Math.random() * 0.3; // High pitch sweep
|
||||
pitchDecay = 0.05 + Math.random() * 0.4; // Fast to medium pitch decay
|
||||
decay = 0.1 + Math.random() * 0.35; // Short to medium decay
|
||||
click = 0.2 + Math.random() * 0.4; // Good click
|
||||
attackNoise = 0.3 + Math.random() * 0.5; // Sharp attack
|
||||
attackNoiseFreq = 0.5 + Math.random() * 0.5; // Mid-high filtered
|
||||
bodyResonance = 0.05 + Math.random() * 0.2;
|
||||
bodyFreq = 0.3 + Math.random() * 0.5;
|
||||
bodyDecay = 0.1 + Math.random() * 0.5; // Varied body decay
|
||||
noise = Math.random() * 0.05; // Very minimal
|
||||
noiseDecay = 0.05 + Math.random() * 0.4; // Varied noise decay
|
||||
harmonics = 0.1 + Math.random() * 0.3;
|
||||
waveShape = 0.2 + Math.random() * 0.5; // Sine to triangle
|
||||
phaseDistortion = Math.random() * 0.3; // Light phase distortion
|
||||
distortion = 0; // Clean
|
||||
} 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)
|
||||
pitchMod = 0.3 + Math.random() * 0.4; // Moderate pitch sweep
|
||||
pitchDecay = 0.2 + Math.random() * 0.6; // Medium to slow pitch decay
|
||||
decay = 0.4 + Math.random() * 0.6; // Long decay
|
||||
click = Math.random() * 0.2; // Minimal click
|
||||
attackNoise = 0.1 + Math.random() * 0.4; // Some attack
|
||||
attackNoiseFreq = 0.3 + Math.random() * 0.4; // Mid filtered
|
||||
bodyResonance = 0.2 + Math.random() * 0.5; // Lots of body
|
||||
bodyFreq = 0.1 + Math.random() * 0.4;
|
||||
bodyDecay = 0.3 + Math.random() * 0.7; // Long body decay
|
||||
noise = Math.random() * 0.08;
|
||||
noiseDecay = 0.2 + Math.random() * 0.6; // Varied noise decay
|
||||
harmonics = Math.random() * 0.2;
|
||||
waveShape = Math.random() * 0.4; // Sine to triangle
|
||||
phaseDistortion = Math.random() * 0.25; // Subtle phase distortion
|
||||
distortion = 0; // Clean
|
||||
} 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)
|
||||
pitchMod = 0.1 + Math.random() * 0.4; // Low to moderate pitch sweep
|
||||
pitchDecay = 0.15 + Math.random() * 0.5; // Varied pitch decay
|
||||
decay = 0.4 + Math.random() * 0.6; // Long decay
|
||||
click = Math.random() * 0.15; // Almost no click
|
||||
attackNoise = Math.random() * 0.3; // Variable attack
|
||||
attackNoiseFreq = 0.2 + Math.random() * 0.3; // Low filtered
|
||||
bodyResonance = 0.3 + Math.random() * 0.6; // Strong body
|
||||
bodyFreq = 0.05 + Math.random() * 0.3;
|
||||
bodyDecay = 0.4 + Math.random() * 0.6; // Long body decay
|
||||
noise = Math.random() * 0.03; // Almost no noise
|
||||
noiseDecay = 0.1 + Math.random() * 0.4; // Varied noise decay
|
||||
harmonics = Math.random() * 0.15;
|
||||
waveShape = Math.random() * 0.3; // Mostly sine
|
||||
phaseDistortion = Math.random() * 0.2; // Very subtle
|
||||
distortion = 0; // Clean
|
||||
} else if (styleRoll < 0.78) {
|
||||
// Snappy, clicky kick (fast attack, short)
|
||||
baseFreq = 0.3 + Math.random() * 0.5; // 64-100 Hz (high kicks)
|
||||
pitchMod = 0.5 + Math.random() * 0.5; // High pitch sweep
|
||||
pitchDecay = 0.02 + Math.random() * 0.25; // Very fast pitch
|
||||
decay = 0.08 + Math.random() * 0.35; // Very short to short
|
||||
click = 0.4 + Math.random() * 0.6; // Lots of click
|
||||
attackNoise = 0.5 + Math.random() * 0.5; // Strong attack burst
|
||||
attackNoiseFreq = 0.6 + Math.random() * 0.4; // High filtered
|
||||
bodyResonance = Math.random() * 0.2; // Minimal body
|
||||
bodyFreq = 0.4 + Math.random() * 0.6;
|
||||
bodyDecay = 0.05 + Math.random() * 0.3; // Short body decay
|
||||
noise = Math.random() * 0.1;
|
||||
noiseDecay = 0.02 + Math.random() * 0.2; // Fast noise decay
|
||||
harmonics = 0.2 + Math.random() * 0.5;
|
||||
waveShape = 0.4 + Math.random() * 0.6; // Triangle to square
|
||||
phaseDistortion = 0.2 + Math.random() * 0.5; // More phase distortion
|
||||
distortion = 0; // Clean
|
||||
} else if (styleRoll < 0.88) {
|
||||
// Weird/experimental kick
|
||||
baseFreq = Math.random() * 0.7; // Full range 35-96 Hz
|
||||
pitchMod = Math.random();
|
||||
pitchDecay = 0.05 + Math.random() * 0.8; // Any decay
|
||||
decay = 0.1 + Math.random() * 0.8; // Any decay
|
||||
click = Math.random() * 0.5;
|
||||
attackNoise = Math.random() * 0.7;
|
||||
attackNoiseFreq = Math.random();
|
||||
bodyResonance = Math.random() * 0.7;
|
||||
bodyFreq = Math.random();
|
||||
bodyDecay = 0.1 + Math.random() * 0.9; // Any body decay
|
||||
noise = Math.random() * 0.15;
|
||||
noiseDecay = 0.05 + Math.random() * 0.8; // Any noise decay
|
||||
harmonics = Math.random() * 0.6;
|
||||
waveShape = Math.random(); // Any shape
|
||||
phaseDistortion = Math.random() * 0.7; // Any amount
|
||||
distortion = 0; // Clean
|
||||
} else {
|
||||
// Dirty/distorted kick (only 15% of the time!)
|
||||
baseFreq = 0.1 + Math.random() * 0.5; // 44-84 Hz (low to mid)
|
||||
pitchMod = 0.4 + Math.random() * 0.5;
|
||||
pitchDecay = 0.1 + Math.random() * 0.6; // Varied pitch decay
|
||||
decay = 0.2 + Math.random() * 0.6; // Varied decay
|
||||
click = 0.2 + Math.random() * 0.5;
|
||||
attackNoise = 0.3 + Math.random() * 0.5;
|
||||
attackNoiseFreq = 0.4 + Math.random() * 0.5;
|
||||
bodyResonance = 0.1 + Math.random() * 0.5;
|
||||
bodyFreq = 0.2 + Math.random() * 0.6;
|
||||
bodyDecay = 0.15 + Math.random() * 0.7; // Varied body decay
|
||||
noise = 0.05 + Math.random() * 0.2;
|
||||
noiseDecay = 0.15 + Math.random() * 0.6; // Varied noise decay
|
||||
harmonics = 0.1 + Math.random() * 0.4;
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : baseFreq,
|
||||
pitchMod,
|
||||
pitchDecay,
|
||||
decay,
|
||||
click,
|
||||
attackNoise,
|
||||
attackNoiseFreq,
|
||||
bodyResonance,
|
||||
bodyFreq,
|
||||
bodyDecay,
|
||||
noise,
|
||||
noiseDecay,
|
||||
harmonics,
|
||||
waveShape,
|
||||
phaseDistortion,
|
||||
distortion,
|
||||
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: BassDrum909Params, mutationAmount: number = 0.15, pitchLock?: PitchLock): BassDrum909Params {
|
||||
const mutate = (value: number, amount: number = mutationAmount): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
return {
|
||||
baseFreq: pitchLock ? params.baseFreq : mutate(params.baseFreq, 0.2),
|
||||
pitchMod: mutate(params.pitchMod, 0.2),
|
||||
pitchDecay: mutate(params.pitchDecay, 0.2),
|
||||
decay: mutate(params.decay, 0.25),
|
||||
click: mutate(params.click, 0.2),
|
||||
attackNoise: mutate(params.attackNoise, 0.25),
|
||||
attackNoiseFreq: mutate(params.attackNoiseFreq, 0.25),
|
||||
bodyResonance: mutate(params.bodyResonance, 0.2),
|
||||
bodyFreq: mutate(params.bodyFreq, 0.25),
|
||||
bodyDecay: mutate(params.bodyDecay, 0.2),
|
||||
noise: mutate(params.noise, 0.15),
|
||||
noiseDecay: mutate(params.noiseDecay, 0.2),
|
||||
harmonics: mutate(params.harmonics, 0.2),
|
||||
waveShape: mutate(params.waveShape, 0.25),
|
||||
phaseDistortion: mutate(params.phaseDistortion, 0.25),
|
||||
distortion: mutate(params.distortion, 0.2),
|
||||
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
|
||||
};
|
||||
}
|
||||
|
||||
generate(
|
||||
params: BassDrum909Params,
|
||||
sampleRate: number,
|
||||
duration: number,
|
||||
pitchLock?: PitchLock
|
||||
): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const left = new Float32Array(numSamples);
|
||||
const right = new Float32Array(numSamples);
|
||||
|
||||
// Base frequency: 35Hz to 115Hz (full kick range from sub to high)
|
||||
const baseFreq = pitchLock ? pitchLock.frequency : 35 + params.baseFreq * 80;
|
||||
|
||||
// Tuning offset: -12% to +12%
|
||||
const tuningFactor = 0.88 + params.tuning * 0.24;
|
||||
const tunedFreq = baseFreq * tuningFactor;
|
||||
|
||||
// Pitch modulation: when locked, starts at locked freq and decays down
|
||||
// when not locked, starts higher and decays to base
|
||||
const pitchMultiplier = 1 + params.pitchMod * 1.5; // 1x to 2.5x
|
||||
const pitchDecayRatio = pitchLock ? 1 / pitchMultiplier : pitchMultiplier; // Invert for pitch lock
|
||||
|
||||
// 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)
|
||||
const ampDecayTime = (0.2 + params.decay * 1.8) * duration;
|
||||
|
||||
// Noise decay time
|
||||
const noiseDecayTime = (0.02 + params.noiseDecay * 0.15) * duration; // 20ms to 170ms
|
||||
|
||||
// Body resonance frequency (40Hz to 200Hz for low body resonances)
|
||||
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;
|
||||
|
||||
// Click decay (very fast for transient)
|
||||
const clickDecayTime = 0.003 + params.click * 0.007; // 3ms to 10ms
|
||||
|
||||
// Attack noise burst decay (super fast: 1-5ms)
|
||||
const attackNoiseDecayTime = 0.001 + params.attackNoise * 0.004; // 1ms to 5ms
|
||||
|
||||
// Attack noise filter frequency (500Hz to 8000Hz)
|
||||
const attackNoiseFilterFreq = 500 + params.attackNoiseFreq * 7500;
|
||||
|
||||
// Low-pass filter at 3000Hz
|
||||
const filterFreq = 3000;
|
||||
|
||||
for (let channel = 0; channel < 2; channel++) {
|
||||
const output = channel === 0 ? left : right;
|
||||
|
||||
// Triangle oscillator phase
|
||||
let phase = 0;
|
||||
|
||||
// Low-pass filter state
|
||||
let filterState = 0;
|
||||
|
||||
// Body resonance filter state
|
||||
let bodyState1 = 0;
|
||||
let bodyState2 = 0;
|
||||
|
||||
// Attack noise filter state (highpass for snap)
|
||||
let attackNoiseState1 = 0;
|
||||
let attackNoiseState2 = 0;
|
||||
|
||||
// Stereo variation
|
||||
const stereoDetune = channel === 0 ? 0.999 : 1.001;
|
||||
const stereoPhaseOffset = channel === 0 ? 0 : 0.05;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
// Pitch envelope: exponential decay
|
||||
const pitchEnv = Math.exp(-t / pitchDecayTime);
|
||||
|
||||
// When pitch locked: start at locked freq, decay down
|
||||
// When not locked: start higher, decay to base
|
||||
const currentFreq = pitchLock
|
||||
? tunedFreq * stereoDetune * (pitchDecayRatio + (1 - pitchDecayRatio) * pitchEnv)
|
||||
: tunedFreq * stereoDetune * (1 + (pitchMultiplier - 1) * pitchEnv);
|
||||
|
||||
// Generate oscillator with wave shape morphing and phase distortion
|
||||
phase += (2 * Math.PI * currentFreq) / sampleRate;
|
||||
if (phase > 2 * Math.PI) phase -= 2 * Math.PI;
|
||||
|
||||
// Apply phase distortion (modulate the phase itself)
|
||||
let distortedPhase = phase;
|
||||
if (params.phaseDistortion > 0.01) {
|
||||
// Phase distortion creates harmonic richness
|
||||
const phaseMod = Math.sin(phase * 2) * params.phaseDistortion * 0.5;
|
||||
distortedPhase = phase + phaseMod;
|
||||
if (distortedPhase < 0) distortedPhase += 2 * Math.PI;
|
||||
if (distortedPhase > 2 * Math.PI) distortedPhase -= 2 * Math.PI;
|
||||
}
|
||||
|
||||
// Wave shape morphing: 0=sine, 0.5=triangle, 1=square
|
||||
let waveform: number;
|
||||
|
||||
// Generate sine wave
|
||||
const sine = Math.sin(distortedPhase);
|
||||
|
||||
// Generate triangle wave
|
||||
const triangle = distortedPhase < Math.PI
|
||||
? -1 + (2 * distortedPhase) / Math.PI
|
||||
: 3 - (2 * distortedPhase) / Math.PI;
|
||||
|
||||
// Generate square wave
|
||||
const square = distortedPhase < Math.PI ? 1 : -1;
|
||||
|
||||
// Morph between shapes
|
||||
if (params.waveShape < 0.5) {
|
||||
// Morph between sine and triangle
|
||||
const mix = params.waveShape * 2; // 0 to 1
|
||||
waveform = sine * (1 - mix) + triangle * mix;
|
||||
} else {
|
||||
// Morph between triangle and square
|
||||
const mix = (params.waveShape - 0.5) * 2; // 0 to 1
|
||||
waveform = triangle * (1 - mix) + square * mix;
|
||||
}
|
||||
|
||||
// Attack envelope (5ms linear ramp)
|
||||
const attackEnv = t < attackTime ? t / attackTime : 1.0;
|
||||
|
||||
// Amplitude envelope: exponential decay with attack
|
||||
const ampEnv = Math.exp(-t / ampDecayTime) * attackEnv;
|
||||
|
||||
// 1. Morphed oscillator (main tone) - reduced level
|
||||
let signal = waveform * ampEnv * 0.6;
|
||||
|
||||
// 1b. Add harmonics (2nd and 3rd) for more character
|
||||
if (params.harmonics > 0.01) {
|
||||
const harmonic2 = Math.sin(2 * distortedPhase) * params.harmonics * 0.3;
|
||||
const harmonic3 = Math.sin(3 * distortedPhase) * params.harmonics * 0.15;
|
||||
signal += (harmonic2 + harmonic3) * ampEnv;
|
||||
}
|
||||
|
||||
// 2. Attack noise burst (super sharp transient, 1-5ms)
|
||||
if (params.attackNoise > 0.01) {
|
||||
const attackEnv = Math.exp(-t / attackNoiseDecayTime);
|
||||
const attackNoise = Math.random() * 2 - 1;
|
||||
|
||||
// Highpass filter the attack noise for snap
|
||||
const attackNoiseFiltered = this.highpassFilter(
|
||||
attackNoise,
|
||||
attackNoiseFilterFreq,
|
||||
2.0,
|
||||
sampleRate,
|
||||
attackNoiseState1,
|
||||
attackNoiseState2
|
||||
);
|
||||
attackNoiseState1 = attackNoiseFiltered.state1;
|
||||
attackNoiseState2 = attackNoiseFiltered.state2;
|
||||
|
||||
signal += attackNoiseFiltered.output * params.attackNoise * attackEnv * 0.4;
|
||||
}
|
||||
|
||||
// 3. Click/snap transient (fast decaying high-pitched sine)
|
||||
if (params.click > 0.01) {
|
||||
const clickEnv = Math.exp(-t / clickDecayTime);
|
||||
const clickFreq = tunedFreq * 4; // High frequency for click
|
||||
const clickOsc = Math.sin(2 * Math.PI * clickFreq * t);
|
||||
signal += clickOsc * params.click * clickEnv * 0.15;
|
||||
}
|
||||
|
||||
// 4. Body resonance (bandpass filtered feedback with decay)
|
||||
if (params.bodyResonance > 0.05) {
|
||||
const bodyEnv = Math.exp(-t / bodyDecayTime);
|
||||
const bodyFiltered = this.bandpassFilter(
|
||||
signal,
|
||||
bodyFreq,
|
||||
5 + params.bodyResonance * 10,
|
||||
sampleRate,
|
||||
bodyState1,
|
||||
bodyState2
|
||||
);
|
||||
bodyState1 = bodyFiltered.state1;
|
||||
bodyState2 = bodyFiltered.state2;
|
||||
signal += bodyFiltered.output * params.bodyResonance * bodyEnv * 0.2;
|
||||
}
|
||||
|
||||
// 5. Add sustaining noise
|
||||
if (params.noise > 0.01) {
|
||||
const noise = Math.random() * 2 - 1;
|
||||
const noiseEnv = Math.exp(-t / noiseDecayTime);
|
||||
signal += noise * params.noise * noiseEnv * 0.1;
|
||||
}
|
||||
|
||||
// 6. Optional distortion/saturation (only if distortion > 0.2)
|
||||
if (params.distortion > 0.2) {
|
||||
const distAmount = 1 + params.distortion * 3; // Reduced range
|
||||
signal = this.waveshaper(signal * distAmount, distAmount) / distAmount;
|
||||
}
|
||||
|
||||
// 7. Low-pass filter at 3000Hz
|
||||
const normalizedFreq = Math.min(filterFreq / sampleRate, 0.48);
|
||||
const a = 2 * Math.PI * normalizedFreq;
|
||||
filterState += a * (signal - filterState);
|
||||
signal = filterState;
|
||||
|
||||
output[i] = signal;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize output to consistent level
|
||||
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; // Normalize to 0.5 peak for more headroom
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
left[i] *= normGain;
|
||||
right[i] *= normGain;
|
||||
}
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
private bandpassFilter(
|
||||
input: number,
|
||||
freq: number,
|
||||
q: number,
|
||||
sampleRate: number,
|
||||
state1: number,
|
||||
state2: number
|
||||
): { output: number; state1: number; state2: number } {
|
||||
// State variable filter in bandpass mode
|
||||
const normalizedFreq = Math.min(freq / sampleRate, 0.48);
|
||||
const f = 2 * Math.sin(Math.PI * normalizedFreq);
|
||||
const qRecip = 1 / Math.max(q, 0.5);
|
||||
|
||||
const lowpass = state2 + f * state1;
|
||||
const highpass = input - lowpass - qRecip * state1;
|
||||
const bandpass = f * highpass + state1;
|
||||
|
||||
// Update states with clamping
|
||||
const newState1 = Math.max(-2, Math.min(2, bandpass));
|
||||
const newState2 = Math.max(-2, Math.min(2, lowpass));
|
||||
|
||||
return {
|
||||
output: bandpass,
|
||||
state1: newState1,
|
||||
state2: newState2,
|
||||
};
|
||||
}
|
||||
|
||||
private highpassFilter(
|
||||
input: number,
|
||||
freq: number,
|
||||
q: number,
|
||||
sampleRate: number,
|
||||
state1: number,
|
||||
state2: number
|
||||
): { output: number; state1: number; state2: number } {
|
||||
// State variable filter in highpass mode
|
||||
const normalizedFreq = Math.min(freq / sampleRate, 0.48);
|
||||
const f = 2 * Math.sin(Math.PI * normalizedFreq);
|
||||
const qRecip = 1 / Math.max(q, 0.5);
|
||||
|
||||
const lowpass = state2 + f * state1;
|
||||
const highpass = input - lowpass - qRecip * state1;
|
||||
const bandpass = f * highpass + state1;
|
||||
|
||||
// Update states with clamping
|
||||
const newState1 = Math.max(-2, Math.min(2, bandpass));
|
||||
const newState2 = Math.max(-2, Math.min(2, lowpass));
|
||||
|
||||
return {
|
||||
output: highpass,
|
||||
state1: newState1,
|
||||
state2: newState2,
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
return ((PI + amount) * x) / (PI + amount * Math.abs(x));
|
||||
}
|
||||
|
||||
private softClip(x: number): number {
|
||||
if (x > 1) return 1;
|
||||
if (x < -1) return -1;
|
||||
if (x > 0.66) return (3 - (2 - 3 * x) ** 2) / 3;
|
||||
if (x < -0.66) return -(3 - (2 - 3 * -x) ** 2) / 3;
|
||||
return x;
|
||||
}
|
||||
|
||||
private freqToParam(freq: number): number {
|
||||
// Map frequency to 0-1 range (35-115 Hz)
|
||||
return Math.max(0, Math.min(1, (freq - 35) / 80));
|
||||
}
|
||||
}
|
||||
@ -248,6 +248,22 @@ export class Ring implements SynthEngine<RingParams> {
|
||||
if (secondModPhaseR > TAU * 1000) secondModPhaseR -= TAU * 1000;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
|
||||
236
src/lib/audio/engines/Snare909.ts
Normal file
236
src/lib/audio/engines/Snare909.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
|
||||
interface Snare909Params {
|
||||
// Core frequency (base pitch of the snare)
|
||||
baseFreq: number;
|
||||
|
||||
// Tone control (0 = more tonal, 1 = more noise)
|
||||
tone: number;
|
||||
|
||||
// Snap amount (pitch modulation intensity)
|
||||
snap: number;
|
||||
|
||||
// Decay times
|
||||
tonalDecay: number;
|
||||
noiseDecay: number;
|
||||
|
||||
// Accent (volume boost)
|
||||
accent: number;
|
||||
|
||||
// Tuning offset
|
||||
tuning: number;
|
||||
}
|
||||
|
||||
export class Snare909 implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Classic 909-style snare drum with triangle wave and noise';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Snare909Params {
|
||||
return {
|
||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
||||
tone: 0.4 + Math.random() * 0.4,
|
||||
snap: 0.5 + Math.random() * 0.5,
|
||||
tonalDecay: 0.15 + Math.random() * 0.35,
|
||||
noiseDecay: 0.4 + Math.random() * 0.4,
|
||||
accent: 0.5 + Math.random() * 0.5,
|
||||
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Snare909Params, mutationAmount: number = 0.15, pitchLock?: PitchLock): Snare909Params {
|
||||
const mutate = (value: number, amount: number = mutationAmount): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
return {
|
||||
baseFreq: pitchLock ? params.baseFreq : mutate(params.baseFreq, 0.2),
|
||||
tone: mutate(params.tone, 0.25),
|
||||
snap: mutate(params.snap, 0.2),
|
||||
tonalDecay: mutate(params.tonalDecay, 0.2),
|
||||
noiseDecay: mutate(params.noiseDecay, 0.2),
|
||||
accent: mutate(params.accent, 0.2),
|
||||
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
|
||||
};
|
||||
}
|
||||
|
||||
generate(
|
||||
params: Snare909Params,
|
||||
sampleRate: number,
|
||||
duration: number,
|
||||
pitchLock?: PitchLock
|
||||
): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const left = new Float32Array(numSamples);
|
||||
const right = new Float32Array(numSamples);
|
||||
|
||||
// Base frequency: 100Hz to 400Hz
|
||||
const baseFreq = pitchLock ? pitchLock.frequency : 100 + params.baseFreq * 300;
|
||||
|
||||
// Tuning offset: -12% to +12%
|
||||
const tuningFactor = 0.88 + params.tuning * 0.24;
|
||||
const tunedFreq = baseFreq * tuningFactor;
|
||||
|
||||
// Pitch modulation parameters (classic 909: 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
|
||||
|
||||
// Decay times scaled by duration
|
||||
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)
|
||||
|
||||
// Attack time (5ms ramp like original)
|
||||
const attackTime = 0.005;
|
||||
|
||||
// Notch filter parameters for noise (fixed at 1000Hz like the original)
|
||||
const notchFreq = 1000;
|
||||
const notchQ = 5;
|
||||
|
||||
for (let channel = 0; channel < 2; channel++) {
|
||||
const output = channel === 0 ? left : right;
|
||||
|
||||
// Triangle oscillator phase
|
||||
let phase = Math.random() * Math.PI * 2;
|
||||
|
||||
// Notch filter state variables
|
||||
let notchState1 = 0;
|
||||
let notchState2 = 0;
|
||||
|
||||
// Stereo variation (slight detune and phase offset)
|
||||
const stereoDetune = channel === 0 ? 0.998 : 1.002;
|
||||
const stereoPhaseOffset = channel === 0 ? 0 : 0.1;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
const normPhase = i / numSamples;
|
||||
|
||||
// Pitch envelope: exponential ramp from pitchMultiplier*freq down to base freq
|
||||
const pitchEnv = Math.exp(-t / modDuration);
|
||||
const currentFreq = tunedFreq * stereoDetune * (1 + (pitchMultiplier - 1) * pitchEnv);
|
||||
|
||||
// Generate triangle wave
|
||||
phase += (2 * Math.PI * currentFreq) / sampleRate;
|
||||
if (phase > 2 * Math.PI) phase -= 2 * Math.PI;
|
||||
|
||||
// Triangle wave from phase
|
||||
const triangle = phase < Math.PI
|
||||
? -1 + (2 * phase) / Math.PI
|
||||
: 3 - (2 * phase) / Math.PI;
|
||||
|
||||
// Attack envelope (5ms linear ramp like original)
|
||||
const attackEnv = t < attackTime ? t / attackTime : 1.0;
|
||||
|
||||
// Tonal envelope: exponential decay
|
||||
const tonalEnv = Math.exp(-t / tonalDecayTime) * attackEnv;
|
||||
const tonalSignal = triangle * tonalEnv;
|
||||
|
||||
// Generate white noise
|
||||
const noise = Math.random() * 2 - 1;
|
||||
|
||||
// Apply notch filter to noise (removes 1000Hz component)
|
||||
const notchFiltered = this.notchFilter(
|
||||
noise,
|
||||
notchFreq,
|
||||
notchQ,
|
||||
sampleRate,
|
||||
notchState1,
|
||||
notchState2
|
||||
);
|
||||
notchState1 = notchFiltered.state1;
|
||||
notchState2 = notchFiltered.state2;
|
||||
|
||||
// Noise envelope: exponential decay (longer than tonal)
|
||||
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
|
||||
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
|
||||
|
||||
// Soft clipping
|
||||
sample = this.softClip(sample);
|
||||
|
||||
output[i] = sample;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize output to consistent level
|
||||
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; // Normalize to 0.5 peak for more headroom
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
left[i] *= normGain;
|
||||
right[i] *= normGain;
|
||||
}
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
private notchFilter(
|
||||
input: number,
|
||||
frequency: number,
|
||||
q: number,
|
||||
sampleRate: number,
|
||||
state1: number,
|
||||
state2: number
|
||||
): { output: number; state1: number; state2: number } {
|
||||
// State variable filter configured as notch
|
||||
const normalizedFreq = Math.min(frequency / sampleRate, 0.48);
|
||||
const f = 2 * Math.sin(Math.PI * normalizedFreq);
|
||||
const qRecip = 1 / Math.max(q, 0.5);
|
||||
|
||||
const lowpass = state2 + f * state1;
|
||||
const highpass = input - lowpass - qRecip * state1;
|
||||
const bandpass = f * highpass + state1;
|
||||
|
||||
// Notch = input - bandpass
|
||||
const notch = input - bandpass;
|
||||
|
||||
// Update states with clamping
|
||||
const newState1 = Math.max(-2, Math.min(2, bandpass));
|
||||
const newState2 = Math.max(-2, Math.min(2, lowpass));
|
||||
|
||||
return {
|
||||
output: notch,
|
||||
state1: newState1,
|
||||
state2: newState2,
|
||||
};
|
||||
}
|
||||
|
||||
private softClip(x: number): number {
|
||||
if (x > 1) return 1;
|
||||
if (x < -1) return -1;
|
||||
if (x > 0.66) return (3 - (2 - 3 * x) ** 2) / 3;
|
||||
if (x < -0.66) return -(3 - (2 - 3 * -x) ** 2) / 3;
|
||||
return x;
|
||||
}
|
||||
|
||||
private freqToParam(freq: number): number {
|
||||
// Map frequency to 0-1 range (100-400 Hz)
|
||||
return Math.max(0, Math.min(1, (freq - 100) / 300));
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,8 @@ import { Sample } from './Sample';
|
||||
import { Input } from './Input';
|
||||
import { KarplusStrong } from './KarplusStrong';
|
||||
import { AdditiveEngine } from './AdditiveEngine';
|
||||
import { Snare909 } from './Snare909';
|
||||
import { BassDrum909 } from './BassDrum909';
|
||||
|
||||
export const engines: SynthEngine[] = [
|
||||
new Sample(),
|
||||
@ -20,6 +22,8 @@ export const engines: SynthEngine[] = [
|
||||
new Benjolin(),
|
||||
new ZzfxEngine(),
|
||||
new NoiseDrum(),
|
||||
new Snare909(),
|
||||
new BassDrum909(),
|
||||
new Ring(),
|
||||
new KarplusStrong(),
|
||||
new AdditiveEngine(),
|
||||
|
||||
Reference in New Issue
Block a user