class GranularProcessor 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 this.freq = new Float32Array(this.MAX_PARTIALS); this.phase = new Float32Array(this.MAX_PARTIALS); this.amp = new Float32Array(this.MAX_PARTIALS); this.envPhase = new Float32Array(this.MAX_PARTIALS); this.envDuration = new Float32Array(this.MAX_PARTIALS); this.targetCount = 0; this.port.onmessage = e => { const { freqs, amps, durations } = e.data; const count = freqs.length; let factor = 1; if (count > this.MAX_PARTIALS) factor = count / this.MAX_PARTIALS; for (let i = 0; i < this.MAX_PARTIALS && i * factor < count; i++) { const start = Math.floor(i * factor); const end = Math.floor((i + 1) * factor); let sumFreq = 0; let sumAmp = 0; let sumDur = 0; for (let j = start; j < end; j++) { sumFreq += freqs[j]; sumAmp += amps[j]; sumDur += durations[j]; } const n = end - start; this.freq[i] = sumFreq / n; this.amp[i] = (sumAmp / n) * 1.5; // louder this.envDuration[i] = sumDur / n; this.envPhase[i] = 0; } this.targetCount = Math.min(count, this.MAX_PARTIALS); }; } process(_, outputs) { const out = outputs[0][0]; const srInv = 1 / sampleRate; const attackTime = 0.005; // 5ms attack const decayTimeFactor = 1.2; // longer decay const expFactor = 5; // controls exponential decay speed for (let i = 0; i < out.length; i++) { let sample = 0; for (let c = 0; c < this.targetCount; c++) { let env = 0; if (this.envPhase[c] < 1) { const t = this.envPhase[c] * this.envDuration[c]; if (t < attackTime) { // linear attack env = t / attackTime; } else { // exponential decay const decayTime = this.envDuration[c] * decayTimeFactor; const decayProgress = (t - attackTime) / Math.max(decayTime, 0.001); env = Math.exp(-expFactor * decayProgress); } this.envPhase[c] += srInv / Math.max(this.envDuration[c], 0.001); } this.phase[c] += this.freq[c] * srInv; if (this.phase[c] >= 1) this.phase[c] -= 1; const idx = Math.floor(this.phase[c] * this.SINE_TABLE_SIZE) % this.SINE_TABLE_SIZE; sample += this.sineTable[idx] * this.amp[c] * env; } // soft limiting if (sample > 1) sample = 1; else if (sample < -1) sample = -1; out[i] = sample; } return true; } } registerProcessor('granular-processor', GranularProcessor);