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.
183 lines
5.7 KiB
JavaScript
183 lines
5.7 KiB
JavaScript
/**
|
||
* eoc-eq-processor.js — AudioWorklet processor for the 4-band parametric EQ
|
||
*
|
||
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
|
||
* Loads eoc-eq.wasm compiled from eoc-eq.dsp.
|
||
*
|
||
* 10 exposed params (4 bands × freq/gain; mid bands also have Q):
|
||
* Band 1 (Low Shelf): freq1, gain1
|
||
* Band 2 (Low-Mid): freq2, gain2, q2
|
||
* Band 3 (High-Mid): freq3, gain3, q3
|
||
* Band 4 (High Shelf): freq4, gain4
|
||
*
|
||
* Note: shelf bands (1 and 4) omit Q — Faust fi.low_shelf / fi.high_shelf
|
||
* take only freq and gain.
|
||
*
|
||
* Parameter index order follows the Faust JSON descriptor (group declaration order,
|
||
* alphabetical within groups — confirmed by eoc-eq.json).
|
||
*/
|
||
|
||
// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first.
|
||
|
||
class EOCEQProcessor extends FaustWorkletProcessor {
|
||
constructor(options) {
|
||
super(options);
|
||
this._dsp = null;
|
||
this._exports = null;
|
||
this._heap = null;
|
||
this._heapi32 = null;
|
||
this._mem = null;
|
||
this._paramAddresses = [];
|
||
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;
|
||
|
||
// Create DSP instance
|
||
if (exps.createDSPInstance) {
|
||
this._dsp = exps.createDSPInstance();
|
||
} else if (exps.eoc_eq) {
|
||
this._dsp = exps.eoc_eq();
|
||
} else {
|
||
const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function');
|
||
console.warn('[EOCEQProcessor] 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;
|
||
|
||
// Allocate input + output buffers at end of WASM heap
|
||
const heapWords = mem.buffer.byteLength >> 2;
|
||
const inLOff = heapWords - blockSize * 6 - 32;
|
||
const inROff = inLOff + blockSize;
|
||
const outLOff = inROff + blockSize;
|
||
const outROff = outLOff + blockSize;
|
||
|
||
// Copy inputs (silence — effect processes in-place from AudioWorklet inputs)
|
||
// The AudioWorklet process() provides inputs[0] to the raw in buffers via
|
||
// the overridden process() below.
|
||
// For now fill with stored input (set by process()).
|
||
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;
|
||
|
||
// Copy input from staging buffers (filled in process())
|
||
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];
|
||
}
|
||
}
|
||
|
||
// Override process() to capture inputs before calling parent
|
||
process(inputs, outputs, params) {
|
||
const inp = inputs[0];
|
||
this._inputL = inp?.[0] ?? null;
|
||
this._inputR = inp?.[1] ?? inp?.[0] ?? null; // mono fallback to L
|
||
return super.process(inputs, outputs, params);
|
||
}
|
||
}
|
||
|
||
registerProcessor('eoc-eq-processor', EOCEQProcessor);
|