class AdditiveProcessor extends AudioWorkletProcessor { constructor() { super(); this.MAX_PARTIALS = 500; // Sine lookup table this.SINE_TABLE_SIZE = 4096; this.sineTable = new Float32Array(this.SINE_TABLE_SIZE); for (let i = 0; i < this.SINE_TABLE_SIZE; i++) { this.sineTable[i] = Math.sin((i / this.SINE_TABLE_SIZE) * 2 * Math.PI); } // Partial state arrays this.freq = new Float32Array(this.MAX_PARTIALS); this.amp = new Float32Array(this.MAX_PARTIALS); this.phase = new Float32Array(this.MAX_PARTIALS); this.targetFreq = new Float32Array(this.MAX_PARTIALS); this.targetAmp = new Float32Array(this.MAX_PARTIALS); this.activeCount = 0; this.targetCount = 0; // LPF state this.lastSample = 0; this.port.onmessage = e => { const { freqs, amps } = e.data; let count = freqs.length; if (count > this.MAX_PARTIALS) { // Supercell aggregation const factor = count / this.MAX_PARTIALS; for (let i = 0; i < this.MAX_PARTIALS; i++) { let sumFreq = 0, sumAmp = 0; const start = Math.floor(i * factor); const end = Math.floor((i + 1) * factor); for (let j = start; j < end; j++) { sumFreq += freqs[j]; sumAmp += amps[j]; } this.targetFreq[i] = sumFreq / (end - start); this.targetAmp[i] = sumAmp / (end - start); if (this.freq[i] === 0) this.phase[i] = Math.random(); } this.targetCount = this.MAX_PARTIALS; } else { // Direct assignment this.targetFreq.set(freqs); this.targetAmp.set(amps); for (let i = 0; i < count; i++) { if (this.freq[i] === 0) this.phase[i] = Math.random(); } this.targetCount = count; } }; } process(_, outputs) { const out = outputs[0][0]; const COUNT_SMOOTH = 0.02; this.activeCount += (this.targetCount - this.activeCount) * COUNT_SMOOTH; const count = Math.floor(this.activeCount); if (count === 0) { out.fill(0); return true; } // Dynamic smoothing const AMP_SMOOTH = 0.01 * Math.min(count / 50, 1); const FREQ_SMOOTH = 0.01 * Math.min(count / 50, 1); const srInv = 1 / sampleRate; const tableSize = this.SINE_TABLE_SIZE; for (let i = 0; i < out.length; i++) { let sample = 0; for (let c = 0; c < count; c++) { this.freq[c] += (this.targetFreq[c] - this.freq[c]) * FREQ_SMOOTH; this.amp[c] += (this.targetAmp[c] - this.amp[c]) * AMP_SMOOTH; // Continuous phase this.phase[c] += this.freq[c] * srInv; if (this.phase[c] >= 1) this.phase[c] -= 1; const idx = Math.floor(this.phase[c] * tableSize) % tableSize; sample += this.sineTable[idx] * this.amp[c]; } // Only apply soft LPF if we have more than a few partials const useLPF = count >= 4; if (useLPF) { out[i] = this.lastSample * 0.95 + sample * 0.05; this.lastSample = out[i]; } else { out[i] = sample; } } return true; } } registerProcessor('additive-processor', AdditiveProcessor);