memlnaut-nisps/playground/faust/eoc-compressor-processor.js
w1n5t0n 0a5f2544ca feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc)
Faust DSP sources, compiled WASM + JSON descriptors, AudioWorklet processors,
and EOCModule subclasses for all three effects.

- meml-4b4: 4-band parametric EQ (fi.low_shelf, fi.peak_eq × 2, fi.high_shelf)
  10 params: freq/gain per band, Q for the two mid bell bands
- meml-cpe: stereo feed-forward compressor (co.compressor_mono × 2)
  7 params: threshold, ratio, attack, release, knee, makeup, mix (parallel)
- meml-wwc: zita reverb (re.zita_rev1_stereo) with pre-delay and M/S width
  8 active params + mod_rate placeholder (zita internal mod not yet exposed)

All three DSPs compile cleanly under Faust 2.83.1. moduleFactory in
eoc-chain-ui.js now returns real instances for eq/compressor/reverb.
2026-04-03 17:57:36 +01:00

173 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* eoc-compressor-processor.js — AudioWorklet processor for the stereo compressor
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads eoc-compressor.wasm compiled from eoc-compressor.dsp.
*
* 7 params (alphabetical order as emitted by Faust JSON):
* 0: attack (ms)
* 1: knee (dB)
* 2: makeup (dB)
* 3: mix (01)
* 4: ratio
* 5: release (ms)
* 6: threshold (dB)
*/
// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first.
class EOCCompressorProcessor extends FaustWorkletProcessor {
constructor(options) {
super(options);
this._dsp = null;
this._exports = null;
this._heap = null;
this._heapi32 = null;
this._mem = null;
this._paramAddresses = [];
this._inputL = null;
this._inputR = null;
this._sampleRate = 48000;
}
// ---------------------------------------------------------------------------
// FaustWorkletProcessor overrides
// ---------------------------------------------------------------------------
async _initWasm(wasmBytes, sr) {
this._sampleRate = sr;
const module = await WebAssembly.compile(wasmBytes);
const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 });
const imports = {
env: {
memory,
memoryBase: 0,
tableBase: 0,
_abs: Math.abs,
_acosf: Math.acos,
_asinf: Math.asin,
_atanf: Math.atan,
_atan2f: Math.atan2,
_ceilf: Math.ceil,
_cosf: Math.cos,
_expf: Math.exp,
_floorf: Math.floor,
_fmodf: (x, y) => x % y,
_logf: Math.log,
_log10f: Math.log10,
_max_f: Math.max,
_min_f: Math.min,
_remainderf: (x, y) => x - Math.round(x / y) * y,
_powf: Math.pow,
_roundf: Math.round,
_sinf: Math.sin,
_sqrtf: Math.sqrt,
_tanf: Math.tan,
_fabs: Math.abs,
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
},
};
const instance = await WebAssembly.instantiate(module, imports);
this._exports = instance.exports;
this._mem = memory;
this._heap = new Float32Array(memory.buffer);
this._heapi32 = new Int32Array(memory.buffer);
const exps = this._exports;
if (exps.createDSPInstance) {
this._dsp = exps.createDSPInstance();
} else if (exps.eoc_compressor) {
this._dsp = exps.eoc_compressor();
} else {
const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function');
console.warn('[EOCCompressorProcessor] No DSP constructor found; exports:', fnKeys);
return;
}
exps.init(this._dsp, sr);
this._buildParamIndex(exps, memory);
}
_buildParamIndex(exports, memory) {
if (!exports.getJSON) return;
const ptr = exports.getJSON(this._dsp);
const buf = new Uint8Array(memory.buffer);
let str = '';
let i = ptr;
while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); }
let desc;
try { desc = JSON.parse(str); } catch { return; }
const addresses = [];
function walk(items) {
for (const item of items) {
const type = item.type ?? '';
if (['hslider', 'vslider', 'nentry'].includes(type)) {
addresses.push(item.address ?? '');
} else if (item.items) {
walk(item.items);
}
}
}
walk(desc.ui ?? []);
this._paramAddresses = addresses;
}
_onSetParam(index, value) {
if (!this._exports || !this._dsp) return;
const addr = this._paramAddresses[index];
if (addr !== undefined && this._exports.setParamValue) {
this._exports.setParamValue(this._dsp, addr, value);
}
}
_renderBlock(outL, outR, blockSize) {
if (!this._exports || !this._dsp) return;
const exps = this._exports;
const mem = this._mem;
const heap = this._heap;
const i32 = this._heapi32;
const heapWords = mem.buffer.byteLength >> 2;
const inLOff = heapWords - blockSize * 6 - 32;
const inROff = inLOff + blockSize;
const outLOff = inROff + blockSize;
const outROff = outLOff + blockSize;
const inPtrsOff = outROff + blockSize;
const outPtrsOff = inPtrsOff + 2;
i32[inPtrsOff] = inLOff * 4;
i32[inPtrsOff + 1] = inROff * 4;
i32[outPtrsOff] = outLOff * 4;
i32[outPtrsOff + 1] = outROff * 4;
const srcL = this._inputL;
const srcR = this._inputR;
if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i];
if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i];
exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4);
for (let i = 0; i < blockSize; i++) {
outL[i] = heap[outLOff + i];
outR[i] = heap[outROff + i];
}
}
process(inputs, outputs, params) {
const inp = inputs[0];
this._inputL = inp?.[0] ?? null;
this._inputR = inp?.[1] ?? inp?.[0] ?? null;
return super.process(inputs, outputs, params);
}
}
registerProcessor('eoc-compressor-processor', EOCCompressorProcessor);