import type { PitchLock, SynthEngine } from './base/SynthEngine'; interface SnareParams { // 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 Snare implements SynthEngine { getName(): string { return 'Snare'; } getDescription(): string { return 'Classic snare drum with triangle wave and noise'; } getType() { return 'generative' as const; } randomParams(pitchLock?: PitchLock): SnareParams { 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: 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)); }; 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: SnareParams, 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 (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 const baseVolume = 0.5; const accentGain = baseVolume * (1 + params.accent); // Attack time (5ms ramp) const attackTime = 0.005; // Notch filter parameters for noise (fixed at 1000Hz) 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) 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 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); // 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)); } }