You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
103 lines
3.4 KiB
103 lines
3.4 KiB
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);
|