import type { SynthEngine, PitchLock } from './base/SynthEngine'; enum OscillatorWaveform { Sine, Triangle, Square, Saw, Pulse, } enum SweepCurve { Linear, Exponential, Logarithmic, Bounce, Elastic, } enum FilterType { None, LowPass, HighPass, BandPass, } interface DubSirenParams { startFreq: number; endFreq: number; sweepCurve: SweepCurve; waveform: OscillatorWaveform; pulseWidth: number; harmonics: number; harmonicSpread: number; lfoRate: number; lfoDepth: number; filterType: FilterType; filterFreq: number; filterResonance: number; filterSweepAmount: number; attack: number; decay: number; sustain: number; release: number; feedback: number; stereoWidth: number; distortion: number; } export class DubSiren implements SynthEngine { private filterHistoryL1 = 0; private filterHistoryL2 = 0; private filterHistoryR1 = 0; private filterHistoryR2 = 0; private dcBlockerL = 0; private dcBlockerR = 0; private readonly DENORMAL_OFFSET = 1e-24; getName(): string { return 'Siren'; } getDescription(): string { return 'Siren generator with pitch sweeps, anti-aliased oscillators and stable filtering'; } getType() { return 'generative' as const; } generate(params: DubSirenParams, 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 invSampleRate = 1 / sampleRate; // Initialize phases for oscillators const numOscillators = 1 + params.harmonics; const phasesL: number[] = new Array(numOscillators).fill(0); const phasesR: number[] = new Array(numOscillators).fill(0); // Stereo phase offset const stereoPhaseOffset = Math.PI * params.stereoWidth * 0.1; for (let i = 0; i < numOscillators; i++) { phasesR[i] = stereoPhaseOffset * (i + 1); } // LFO setup let lfoPhaseL = 0; let lfoPhaseR = Math.PI * params.stereoWidth * 0.25; const lfoIncrement = TAU * params.lfoRate * invSampleRate; // Feedback buffers with slight delay for richness const feedbackDelaySize = 64; const feedbackBufferL = new Float32Array(feedbackDelaySize); const feedbackBufferR = new Float32Array(feedbackDelaySize); let feedbackIndex = 0; // Reset filter state this.filterHistoryL1 = 0; this.filterHistoryL2 = 0; this.filterHistoryR1 = 0; this.filterHistoryR2 = 0; this.dcBlockerL = 0; this.dcBlockerR = 0; // Envelope smoothing let lastEnv = 0; const envSmoothCoeff = 0.001; for (let i = 0; i < numSamples; i++) { const t = i / numSamples; // Calculate and smooth envelope const targetEnv = this.calculateEnvelope( t * duration, duration, params.attack, params.decay, params.sustain, params.release ); const env = lastEnv + (targetEnv - lastEnv) * envSmoothCoeff; lastEnv = env; // Calculate pitch sweep const sweepProgress = this.calculateSweepCurve(t, params.sweepCurve); const currentFreq = params.startFreq + (params.endFreq - params.startFreq) * sweepProgress; // LFO modulation (using fast approximation) const lfoL = this.fastSin(lfoPhaseL); const lfoR = this.fastSin(lfoPhaseR); const pitchModL = 1 + lfoL * params.lfoDepth * 0.1; const pitchModR = 1 + lfoR * params.lfoDepth * 0.1; // Generate oscillators with harmonics let sampleL = 0; let sampleR = 0; for (let osc = 0; osc < numOscillators; osc++) { const harmonicMultiplier = 1 + osc * params.harmonicSpread; const harmonicLevel = 1 / (osc + 1); const freqL = currentFreq * harmonicMultiplier * pitchModL; const freqR = currentFreq * harmonicMultiplier * pitchModR; // Add delayed feedback to first oscillator const fbL = osc === 0 ? feedbackBufferL[feedbackIndex] * params.feedback * 0.3 : 0; const fbR = osc === 0 ? feedbackBufferR[feedbackIndex] * params.feedback * 0.3 : 0; // Use bandlimited oscillators const oscL = this.generateBandlimitedWaveform( phasesL[osc], params.waveform, params.pulseWidth, freqL, sampleRate ); const oscR = this.generateBandlimitedWaveform( phasesR[osc], params.waveform, params.pulseWidth, freqR, sampleRate ); sampleL += (oscL + fbL) * harmonicLevel; sampleR += (oscR + fbR) * harmonicLevel; // Update phases with proper wrapping const phaseIncrementL = TAU * freqL * invSampleRate; const phaseIncrementR = TAU * freqR * invSampleRate; phasesL[osc] = (phasesL[osc] + phaseIncrementL) % TAU; phasesR[osc] = (phasesR[osc] + phaseIncrementR) % TAU; } // Normalize with headroom const normFactor = 0.5 / Math.sqrt(numOscillators); sampleL *= normFactor; sampleR *= normFactor; // Apply soft saturation if needed if (params.distortion > 0) { sampleL = this.fastTanh(sampleL * (1 + params.distortion * 3)) * 0.8; sampleR = this.fastTanh(sampleR * (1 + params.distortion * 3)) * 0.8; } // Apply filter with stability checks if (params.filterType !== FilterType.None) { const filterFreqMod = params.filterFreq * (1 + params.filterSweepAmount * sweepProgress * 2); const filterFreqWithLFO = Math.min(filterFreqMod * (1 + lfoL * params.lfoDepth * 0.2), sampleRate * 0.45); const safeResonance = Math.min(params.filterResonance, 0.98); [sampleL, this.filterHistoryL1, this.filterHistoryL2] = this.applyStableFilter( sampleL, params.filterType, filterFreqWithLFO, safeResonance, sampleRate, this.filterHistoryL1, this.filterHistoryL2 ); [sampleR, this.filterHistoryR1, this.filterHistoryR2] = this.applyStableFilter( sampleR, params.filterType, filterFreqWithLFO, safeResonance, sampleRate, this.filterHistoryR1, this.filterHistoryR2 ); } // Update feedback delay buffer feedbackBufferL[feedbackIndex] = sampleL; feedbackBufferR[feedbackIndex] = sampleR; feedbackIndex = (feedbackIndex + 1) % feedbackDelaySize; // DC blocking const dcCutoff = 0.995; const blockedL = sampleL - this.dcBlockerL; const blockedR = sampleR - this.dcBlockerR; this.dcBlockerL = sampleL - blockedL * dcCutoff; this.dcBlockerR = sampleR - blockedR * dcCutoff; // Apply envelope and final limiting leftBuffer[i] = Math.max(-1, Math.min(1, blockedL * env)); rightBuffer[i] = Math.max(-1, Math.min(1, blockedR * env)); // Update LFO phases lfoPhaseL = (lfoPhaseL + lfoIncrement) % TAU; lfoPhaseR = (lfoPhaseR + lfoIncrement) % TAU; } // Peak normalization with headroom 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 generateBandlimitedWaveform( phase: number, waveform: OscillatorWaveform, pulseWidth: number, frequency: number, sampleRate: number ): number { const nyquist = sampleRate / 2; const maxHarmonic = Math.floor(nyquist / frequency); switch (waveform) { case OscillatorWaveform.Sine: return Math.sin(phase); case OscillatorWaveform.Triangle: // Bandlimited triangle using additive synthesis let tri = 0; const harmonics = Math.min(maxHarmonic, 32); for (let h = 1; h <= harmonics; h += 2) { const sign = ((h - 1) / 2) % 2 === 0 ? 1 : -1; tri += sign * Math.sin(phase * h) / (h * h); } return tri * (8 / (Math.PI * Math.PI)); case OscillatorWaveform.Square: // Bandlimited square using additive synthesis let square = 0; const squareHarmonics = Math.min(maxHarmonic, 32); for (let h = 1; h <= squareHarmonics; h += 2) { square += Math.sin(phase * h) / h; } return square * (4 / Math.PI); case OscillatorWaveform.Saw: // Bandlimited saw using additive synthesis let saw = 0; const sawHarmonics = Math.min(maxHarmonic, 32); for (let h = 1; h <= sawHarmonics; h++) { saw += Math.sin(phase * h) / h; } return -saw * (2 / Math.PI); case OscillatorWaveform.Pulse: // Bandlimited pulse as difference of two saws let pulse1 = 0; let pulse2 = 0; const pulseHarmonics = Math.min(maxHarmonic, 32); const phaseShift = phase + Math.PI * 2 * pulseWidth; for (let h = 1; h <= pulseHarmonics; h++) { pulse1 += Math.sin(phase * h) / h; pulse2 += Math.sin(phaseShift * h) / h; } return (pulse1 - pulse2) * (2 / Math.PI); default: return 0; } } private calculateSweepCurve(t: number, curve: SweepCurve): number { switch (curve) { case SweepCurve.Linear: return t; case SweepCurve.Exponential: return t * t; case SweepCurve.Logarithmic: return Math.sqrt(t); case SweepCurve.Bounce: return t < 0.5 ? t * 2 : 2 - t * 2; case SweepCurve.Elastic: const p = 0.3; const s = p / 4; if (t <= 0.001) return 0; if (t >= 0.999) return 1; return Math.pow(2, -10 * t) * Math.sin((t - s) * (2 * Math.PI) / p) + 1; default: return t; } } private calculateEnvelope( t: number, duration: number, attack: number, decay: number, sustain: number, release: number ): number { const attackTime = attack * duration; const decayTime = decay * duration; const releaseTime = release * duration; const sustainStart = attackTime + decayTime; const releaseStart = duration - releaseTime; if (t < attackTime) { // Exponential attack for smoother onset const progress = t / attackTime; return progress * progress; } else if (t < sustainStart) { const decayProgress = (t - attackTime) / decayTime; return 1 - decayProgress * (1 - sustain); } else if (t < releaseStart) { return sustain; } else { const releaseProgress = (t - releaseStart) / releaseTime; // Exponential release for smoother tail return sustain * Math.pow(1 - releaseProgress, 2); } } private fastSin(phase: number): number { // Fast sine approximation using parabolic approximation const x = (phase % (Math.PI * 2)) / Math.PI - 1; const x2 = x * x; return x * (1 - x2 * (0.16666 - x2 * 0.00833)); } private fastTanh(x: number): number { // Fast tanh approximation for soft clipping const x2 = x * x; return x * (27 + x2) / (27 + 9 * x2); } private applyStableFilter( input: number, filterType: FilterType, freq: number, resonance: number, sampleRate: number, history1: number, history2: number ): [number, number, number] { // Add denormal prevention input += this.DENORMAL_OFFSET; // Improved state-variable filter with pre-warping const w = Math.tan((Math.PI * freq) / sampleRate); const g = w / (1 + w); const k = 2 - 2 * resonance; // Stability-safe resonance scaling // State variable filter equations const v0 = input; const v1 = history1; const v2 = history2; const v3 = v0 - v2; const v1Next = v1 + g * (v3 - k * v1); const v2Next = v2 + g * v1Next; let output: number; switch (filterType) { case FilterType.LowPass: output = v2Next; break; case FilterType.HighPass: output = v0 - k * v1Next - v2Next; break; case FilterType.BandPass: output = v1Next; break; default: output = input; } // Remove denormal offset output -= this.DENORMAL_OFFSET; return [output, v1Next, v2Next]; } randomParams(pitchLock?: PitchLock): DubSirenParams { let startFreq: number; let endFreq: number; if (pitchLock?.enabled) { // When pitch locked, sweep around the locked frequency startFreq = pitchLock.frequency; endFreq = pitchLock.frequency * (Math.random() < 0.5 ? 0.5 : 2); } else { const freqPairs = [ [100, 1200], [200, 800], [300, 2000], [50, 400], [500, 3000], [150, 600], ]; const [freq1, freq2] = this.randomChoice(freqPairs); const shouldReverse = Math.random() < 0.3; startFreq = shouldReverse ? freq2 : freq1; endFreq = shouldReverse ? freq1 : freq2; } return { startFreq, endFreq, sweepCurve: this.randomInt(0, 4) as SweepCurve, waveform: this.randomInt(0, 4) as OscillatorWaveform, pulseWidth: this.randomRange(0.1, 0.9), harmonics: this.randomInt(0, 3), harmonicSpread: this.randomRange(1.5, 3.0), lfoRate: this.randomRange(0.5, 8), lfoDepth: this.randomRange(0, 0.5), filterType: this.randomInt(0, 3) as FilterType, filterFreq: this.randomRange(200, 4000), filterResonance: this.randomRange(0.1, 0.85), // Safer maximum filterSweepAmount: this.randomRange(0, 1), attack: this.randomRange(0.005, 0.1), // Minimum 220 samples decay: this.randomRange(0.01, 0.2), sustain: this.randomRange(0.3, 0.9), release: this.randomRange(0.1, 0.4), feedback: this.randomRange(0, 0.5), stereoWidth: this.randomRange(0.2, 0.8), distortion: this.randomRange(0, 0.3), }; } mutateParams(params: DubSirenParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DubSirenParams { let startFreq: number; let endFreq: number; if (pitchLock?.enabled) { // When pitch locked, keep one frequency at the locked value if (Math.random() < 0.5) { startFreq = pitchLock.frequency; endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000); } else { startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000); endFreq = pitchLock.frequency; } } else { startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000); endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000); } return { startFreq, endFreq, sweepCurve: Math.random() < 0.1 ? this.randomInt(0, 4) as SweepCurve : params.sweepCurve, waveform: Math.random() < 0.1 ? this.randomInt(0, 4) as OscillatorWaveform : params.waveform, pulseWidth: this.mutateValue(params.pulseWidth, mutationAmount, 0.05, 0.95), harmonics: Math.random() < 0.15 ? this.randomInt(0, 3) : params.harmonics, harmonicSpread: this.mutateValue(params.harmonicSpread, mutationAmount, 1, 4), lfoRate: this.mutateValue(params.lfoRate, mutationAmount, 0.1, 12), lfoDepth: this.mutateValue(params.lfoDepth, mutationAmount, 0, 0.7), filterType: Math.random() < 0.1 ? this.randomInt(0, 3) as FilterType : params.filterType, filterFreq: this.mutateValue(params.filterFreq, mutationAmount, 100, 8000), filterResonance: this.mutateValue(params.filterResonance, mutationAmount, 0, 0.85), filterSweepAmount: this.mutateValue(params.filterSweepAmount, mutationAmount, 0, 1), attack: this.mutateValue(params.attack, mutationAmount, 0.005, 0.2), decay: this.mutateValue(params.decay, mutationAmount, 0.01, 0.3), sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 1), release: this.mutateValue(params.release, mutationAmount, 0.05, 0.5), feedback: this.mutateValue(params.feedback, mutationAmount, 0, 0.7), stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1), distortion: this.mutateValue(params.distortion, mutationAmount, 0, 0.5), }; } 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(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 = (max - min) * amount * (Math.random() * 2 - 1); return Math.max(min, Math.min(max, value + variation)); } }