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)); } }