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.
This commit is contained in:
w1n5t0n 2026-04-03 17:57:28 +01:00
parent 6007ab3853
commit 0a5f2544ca
17 changed files with 1095 additions and 3 deletions

View file

@ -0,0 +1,173 @@
/**
* 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);

View file

@ -0,0 +1,34 @@
// eoc-compressor.dsp — Stereo feed-forward compressor for MEMLNaut EOC chain
//
// 7 params: threshold, ratio, attack, release, knee, makeup, mix
//
// Compile:
// faust -lang wasm -cn eoc_compressor -e eoc-compressor.dsp -o eoc-compressor.wasm -json
import("stdfaust.lib");
threshold = hslider("threshold[unit:dB]", -24.0, -60.0, 0.0, 0.1);
ratio = hslider("ratio", 4.0, 1.0, 20.0, 0.1);
attack = hslider("attack[unit:ms]", 10.0, 0.1, 200.0, 0.1);
release = hslider("release[unit:ms]", 100.0, 10.0, 2000.0, 1.0);
knee = hslider("knee[unit:dB]", 6.0, 0.0, 24.0, 0.1);
makeup = hslider("makeup[unit:dB]", 0.0, 0.0, 24.0, 0.1);
mix = hslider("mix", 1.0, 0.0, 1.0, 0.001);
// Convert ms to seconds for Faust
attackSec = attack / 1000.0;
releaseSec = release / 1000.0;
// Soft-knee threshold adjustment (shift threshold down by half the knee)
threshKnee = threshold - knee / 2.0;
// Makeup gain as linear multiplier
makeupLin = ba.db2linear(makeup);
// Compressor on a single channel with makeup applied
compCh(x) = co.compressor_mono(ratio, threshKnee, attackSec, releaseSec, x) * makeupLin;
// Parallel compression (dry/wet blend)
parallelComp(x) = (1.0 - mix) * x + mix * compCh(x);
process = parallelComp, parallelComp;

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,183 @@
/**
* 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);

View file

@ -0,0 +1,38 @@
// eoc-eq.dsp — 4-band parametric EQ for MEMLNaut EOC chain
//
// Band 1: Low Shelf (default 80 Hz, range 20500)
// Band 2: Low-Mid bell (default 400 Hz, range 1002000)
// Band 3: High-Mid bell (default 2500 Hz, range 5008000)
// Band 4: High Shelf (default 8000 Hz, range 200020000)
//
// Compile:
// faust -lang wasm -cn eoc_eq -e eoc-eq.dsp -o eoc-eq.wasm -json
import("stdfaust.lib");
// Band 1 — Low Shelf
freq1 = hslider("Band 1 (Low Shelf)/freq1[unit:Hz]", 80, 20, 500, 0.1);
gain1 = hslider("Band 1 (Low Shelf)/gain1[unit:dB]", 0, -12, 12, 0.1);
q1 = hslider("Band 1 (Low Shelf)/q1", 1.0, 0.1, 10.0, 0.01);
// Band 2 — Low-Mid bell
freq2 = hslider("Band 2 (Low-Mid)/freq2[unit:Hz]", 400, 100, 2000, 1.0);
gain2 = hslider("Band 2 (Low-Mid)/gain2[unit:dB]", 0, -12, 12, 0.1);
q2 = hslider("Band 2 (Low-Mid)/q2", 1.0, 0.1, 10.0, 0.01);
// Band 3 — High-Mid bell
freq3 = hslider("Band 3 (High-Mid)/freq3[unit:Hz]", 2500, 500, 8000, 1.0);
gain3 = hslider("Band 3 (High-Mid)/gain3[unit:dB]", 0, -12, 12, 0.1);
q3 = hslider("Band 3 (High-Mid)/q3", 1.0, 0.1, 10.0, 0.01);
// Band 4 — High Shelf
freq4 = hslider("Band 4 (High Shelf)/freq4[unit:Hz]", 8000, 2000, 20000, 10.0);
gain4 = hslider("Band 4 (High Shelf)/gain4[unit:dB]", 0, -12, 12, 0.1);
q4 = hslider("Band 4 (High Shelf)/q4", 1.0, 0.1, 10.0, 0.01);
eqChain = fi.low_shelf(gain1, freq1) :
fi.peak_eq(gain2, freq2, q2) :
fi.peak_eq(gain3, freq3, q3) :
fi.high_shelf(gain4, freq4);
process = eqChain, eqChain;

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,178 @@
/**
* eoc-reverb-processor.js AudioWorklet processor for the zita reverb
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads eoc-reverb.wasm compiled from eoc-reverb.dsp.
*
* 8 exposed params (alphabetical order as emitted by Faust JSON):
* 0: decay (s)
* 1: diffusion (01)
* 2: hi_damp (01)
* 3: lo_damp (01)
* 4: mix (01)
* 5: predelay (ms)
* 6: size (01)
* 7: width (01)
*
* Note: mod_rate is declared in the DSP file but has no effect in this version
* (zita_rev1_stereo does not expose modulation rate externally).
*/
// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first.
class EOCReverbProcessor 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);
// Reverb needs more memory due to large delay lines in zita
const memory = new WebAssembly.Memory({ initial: 64, maximum: 512 });
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_reverb) {
this._dsp = exps.eoc_reverb();
} else {
const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function');
console.warn('[EOCReverbProcessor] 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-reverb-processor', EOCReverbProcessor);

View file

@ -0,0 +1,52 @@
// eoc-reverb.dsp — Stereo reverb for MEMLNaut EOC chain
//
// 9 params: predelay, size, diffusion, hi_damp, lo_damp, decay, mix, width, mod_rate
//
// Uses re.zita_rev1_stereo for the core reverb algorithm.
//
// Compile:
// faust -lang wasm -cn eoc_reverb -json eoc-reverb.dsp -o eoc-reverb.wasm
import("stdfaust.lib");
predelay = hslider("predelay[unit:ms]", 0.0, 0.0, 100.0, 0.5);
size = hslider("size", 0.5, 0.0, 1.0, 0.001);
diffusion= hslider("diffusion", 0.7, 0.0, 1.0, 0.001);
hi_damp = hslider("hi_damp", 0.5, 0.0, 1.0, 0.001);
lo_damp = hslider("lo_damp", 0.0, 0.0, 1.0, 0.001);
decay = hslider("decay[unit:s]", 3.0, 0.1, 20.0, 0.1);
mix = hslider("mix", 0.2, 0.0, 1.0, 0.001);
width = hslider("width", 0.8, 0.0, 1.0, 0.001);
mod_rate = hslider("mod_rate[unit:Hz]", 0.5, 0.0, 5.0, 0.01);
// Pre-delay in samples (minimum 1)
pdSamps = max(1, int(predelay / 1000.0 * ma.SR));
// Frequency crossovers for zita
f1 = 200.0 + lo_damp * 1800.0;
f2 = 20000.0 - hi_damp * 18000.0;
// Reverb decay times scaled by size
t60dc = decay * (1.0 + size * 0.5);
t60m = decay;
// Pre-delay: single channel
predelayLine = _ @ pdSamps;
// Width processing: M/S encode-scale-decode
// L R → L' R' where side channels scaled by width
widthL(l, r) = (l + r) * 0.5 + (l - r) * 0.5 * width;
widthR(l, r) = (l + r) * 0.5 - (l - r) * 0.5 * width;
// Wet signal through predelay, diffusion scale, and zita reverb
wetL(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0,
inL * diffusion @ pdSamps,
inR * diffusion @ pdSamps)) : widthL;
wetR(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0,
inL * diffusion @ pdSamps,
inR * diffusion @ pdSamps)) : widthR;
process(inL, inR) =
inL * (1.0 - mix) + wetL(inL, inR) * mix,
inR * (1.0 - mix) + wetR(inL, inR) * mix;

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -5,3 +5,6 @@
export { EOCModule } from './eoc-module.js';
export { EOCChain } from './eoc-chain.js';
export { EQModule } from './modules/eq-module.js';
export { CompressorModule } from './modules/compressor-module.js';
export { ReverbModule } from './modules/reverb-module.js';

View file

@ -0,0 +1,129 @@
// playground/js/eoc/modules/compressor-module.js — Stereo compressor EOCModule
//
// Faust DSP: playground/faust/eoc-compressor.dsp
// WASM: playground/faust/eoc-compressor.wasm
// JSON: playground/faust/eoc-compressor.json
//
// 7 params (Faust JSON alphabetical order):
// 0: attack [0.1200 ms, init 10]
// 1: knee [024 dB, init 6]
// 2: makeup [024 dB, init 0]
// 3: mix [01, init 1] (dry/wet for parallel compression)
// 4: ratio [120, init 4]
// 5: release [102000 ms, init 100]
// 6: threshold [-600 dBFS, init -24]
import { EOCModule } from '../eoc-module.js';
import { loadFaustParamMeta } from '../../synth/faust-param-meta.js';
const COMP_PARAM_META = [
{ id: 'attack', name: 'Attack', min: 0.1, max: 200, init: (10-0.1)/(200-0.1), curve: 0.3, group: 'Compressor' },
{ id: 'knee', name: 'Knee', min: 0, max: 24, init: 6/24, curve: 0.5, group: 'Compressor' },
{ id: 'makeup', name: 'Makeup', min: 0, max: 24, init: 0, curve: 0.5, group: 'Compressor' },
{ id: 'mix', name: 'Mix', min: 0, max: 1, init: 1, curve: 0.5, group: 'Compressor' },
{ id: 'ratio', name: 'Ratio', min: 1, max: 20, init: (4-1)/(20-1), curve: 0.4, group: 'Compressor' },
{ id: 'release', name: 'Release', min: 10, max: 2000, init: (100-10)/(2000-10), curve: 0.3, group: 'Compressor' },
{ id: 'threshold', name: 'Threshold', min: -60, max: 0, init: (-24-(-60))/(0-(-60)), curve: 0.5, group: 'Compressor' },
];
export class CompressorModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
this._paramMeta = COMP_PARAM_META;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'compressor'; }
get displayName() { return 'Compressor'; }
get paramMeta() { return this._paramMeta; }
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
try {
const fetched = await loadFaustParamMeta('faust/eoc-compressor.json');
if (fetched && fetched.length > 0) this._paramMeta = fetched;
} catch (err) {
console.warn('[CompressorModule] Could not load eoc-compressor.json, using static paramMeta:', err.message);
}
try {
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
} catch (_) { /* already registered */ }
await audioCtx.audioWorklet.addModule('faust/eoc-compressor-processor.js');
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-compressor-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
});
const wasmResp = await fetch('faust/eoc-compressor.wasm');
const wasmBytes = await wasmResp.arrayBuffer();
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('CompressorModule worklet init timeout')), 10000);
this._workletNode.port.onmessage = (e) => {
if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); }
if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); }
};
this._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes],
);
});
this._effectGain = audioCtx.createGain();
this._effectGain.gain.value = 1;
this._bypassIn.connect(this._workletNode);
this._workletNode.connect(this._effectGain);
this._effectGain.connect(this._bypassOut);
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const meta = this._paramMeta[index];
if (!meta) return;
const raw = meta.min + normalizedValue * (meta.max - meta.min);
this._workletNode.port.postMessage({ type: 'setParam', index, value: raw });
}
// ---------------------------------------------------------------------------
// Bypass
// ---------------------------------------------------------------------------
_onBypassChange(enabled) {
if (!this._effectGain) return;
const t = this._audioCtx.currentTime;
this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005);
}
// ---------------------------------------------------------------------------
// Dispose
// ---------------------------------------------------------------------------
dispose() {
this._workletNode?.disconnect();
this._effectGain?.disconnect();
this._workletNode = null;
this._effectGain = null;
super.dispose();
}
}

View file

@ -0,0 +1,143 @@
// playground/js/eoc/modules/eq-module.js — 4-band parametric EQ EOCModule
//
// Faust DSP: playground/faust/eoc-eq.dsp
// WASM: playground/faust/eoc-eq.wasm
// JSON: playground/faust/eoc-eq.json
//
// 10 params (shelf bands have no Q in Faust fi.low_shelf / fi.high_shelf):
// Band 1 (Low Shelf): freq1 [20500 Hz, init 80], gain1 [-12+12 dB, init 0]
// Band 2 (Low-Mid): freq2 [1002000 Hz, init 400], gain2 [-12+12 dB, init 0], q2 [0.110, init 1]
// Band 3 (High-Mid): freq3 [5008000 Hz, init 2500], gain3 [-12+12 dB, init 0], q3 [0.110, init 1]
// Band 4 (High Shelf): freq4 [200020000 Hz, init 8000], gain4 [-12+12 dB, init 0]
//
// param order = Faust JSON alphabetical-within-group, groups in declaration order:
// 0 freq1, 1 gain1, 2 freq2, 3 gain2, 4 q2, 5 freq3, 6 gain3, 7 q3, 8 freq4, 9 gain4
import { EOCModule } from '../eoc-module.js';
import { loadFaustParamMeta } from '../../synth/faust-param-meta.js';
// Static paramMeta — mirrors eoc-eq.json, used before/without JSON fetch
const EQ_PARAM_META = [
// Band 1 (Low Shelf)
{ id: 'band_1__low_shelf__freq1', name: 'Freq 1', min: 20, max: 500, init: (80-20)/(500-20), curve: 0.3, group: 'Band 1 (Low Shelf)' },
{ id: 'band_1__low_shelf__gain1', name: 'Gain 1', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 1 (Low Shelf)' },
// Band 2 (Low-Mid bell)
{ id: 'band_2__low-mid__freq2', name: 'Freq 2', min: 100, max: 2000, init: (400-100)/(2000-100), curve: 0.3, group: 'Band 2 (Low-Mid)' },
{ id: 'band_2__low-mid__gain2', name: 'Gain 2', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 2 (Low-Mid)' },
{ id: 'band_2__low-mid__q2', name: 'Q 2', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 2 (Low-Mid)' },
// Band 3 (High-Mid bell)
{ id: 'band_3__high-mid__freq3', name: 'Freq 3', min: 500, max: 8000, init: (2500-500)/(8000-500),curve: 0.3, group: 'Band 3 (High-Mid)' },
{ id: 'band_3__high-mid__gain3', name: 'Gain 3', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 3 (High-Mid)' },
{ id: 'band_3__high-mid__q3', name: 'Q 3', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 3 (High-Mid)' },
// Band 4 (High Shelf)
{ id: 'band_4__high_shelf__freq4',name: 'Freq 4', min: 2000, max: 20000, init: (8000-2000)/(20000-2000),curve: 0.3, group: 'Band 4 (High Shelf)' },
{ id: 'band_4__high_shelf__gain4',name: 'Gain 4', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 4 (High Shelf)' },
];
export class EQModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
this._paramMeta = EQ_PARAM_META;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'eq'; }
get displayName() { return 'Parametric EQ'; }
get paramMeta() { return this._paramMeta; }
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
// Attempt to load richer paramMeta from JSON (non-fatal if fetch fails)
try {
const fetched = await loadFaustParamMeta('faust/eoc-eq.json');
if (fetched && fetched.length > 0) this._paramMeta = fetched;
} catch (err) {
console.warn('[EQModule] Could not load eoc-eq.json, using static paramMeta:', err.message);
}
// Register worklet module (idempotent across calls)
try {
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
} catch (_) { /* already registered */ }
await audioCtx.audioWorklet.addModule('faust/eoc-eq-processor.js');
// Create AudioWorkletNode
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-eq-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
});
// Load WASM and send to worklet
const wasmResp = await fetch('faust/eoc-eq.wasm');
const wasmBytes = await wasmResp.arrayBuffer();
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('EQModule worklet init timeout')), 10000);
this._workletNode.port.onmessage = (e) => {
if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); }
if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); }
};
this._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes],
);
});
// Gain node so bypass can mute the effect path
this._effectGain = audioCtx.createGain();
this._effectGain.gain.value = 1;
// Wire: bypassIn → worklet → effectGain → bypassOut
this._bypassIn.connect(this._workletNode);
this._workletNode.connect(this._effectGain);
this._effectGain.connect(this._bypassOut);
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const meta = this._paramMeta[index];
if (!meta) return;
const raw = meta.min + normalizedValue * (meta.max - meta.min);
this._workletNode.port.postMessage({ type: 'setParam', index, value: raw });
}
// ---------------------------------------------------------------------------
// Bypass
// ---------------------------------------------------------------------------
_onBypassChange(enabled) {
if (!this._effectGain) return;
const t = this._audioCtx.currentTime;
this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005);
}
// ---------------------------------------------------------------------------
// Dispose
// ---------------------------------------------------------------------------
dispose() {
this._workletNode?.disconnect();
this._effectGain?.disconnect();
this._workletNode = null;
this._effectGain = null;
super.dispose();
}
}

View file

@ -0,0 +1,147 @@
// playground/js/eoc/modules/reverb-module.js — Zita reverb EOCModule
//
// Faust DSP: playground/faust/eoc-reverb.dsp
// WASM: playground/faust/eoc-reverb.wasm
// JSON: playground/faust/eoc-reverb.json
//
// 8 params (Faust JSON alphabetical order; mod_rate is declared in DSP but not
// exposed by re.zita_rev1_stereo, so it does not appear in the JSON):
// 0: decay [0.120 s, init 3]
// 1: diffusion [01, init 0.7]
// 2: hi_damp [01, init 0.5]
// 3: lo_damp [01, init 0]
// 4: mix [01, init 0.2]
// 5: predelay [0100 ms, init 0]
// 6: size [01, init 0.5]
// 7: width [01, init 0.8]
//
// Note: mod_rate param is listed in paramMeta as a placeholder (fixed at 0.5 Hz)
// to preserve the 9-param count described in the spec. It has no effect on audio
// until zita modulation is plumbed through.
import { EOCModule } from '../eoc-module.js';
import { loadFaustParamMeta } from '../../synth/faust-param-meta.js';
const REVERB_PARAM_META = [
{ id: 'decay', name: 'Decay', min: 0.1, max: 20, init: (3-0.1)/(20-0.1), curve: 0.3, group: 'Reverb' },
{ id: 'diffusion', name: 'Diffusion', min: 0, max: 1, init: 0.7, curve: 0.5, group: 'Reverb' },
{ id: 'hi_damp', name: 'Hi Damp', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' },
{ id: 'lo_damp', name: 'Lo Damp', min: 0, max: 1, init: 0, curve: 0.5, group: 'Reverb' },
{ id: 'mix', name: 'Mix', min: 0, max: 1, init: 0.2, curve: 0.5, group: 'Reverb' },
{ id: 'predelay', name: 'Predelay', min: 0, max: 100, init: 0, curve: 0.4, group: 'Reverb' },
{ id: 'size', name: 'Size', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' },
{ id: 'width', name: 'Width', min: 0, max: 1, init: 0.8, curve: 0.5, group: 'Reverb' },
// Placeholder: mod_rate is not yet connected to zita internals
{ id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' },
];
export class ReverbModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
this._paramMeta = REVERB_PARAM_META;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'reverb'; }
get displayName() { return 'Reverb'; }
get paramMeta() { return this._paramMeta; }
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
// After loading JSON, merge — but keep mod_rate placeholder (index 8)
try {
const fetched = await loadFaustParamMeta('faust/eoc-reverb.json');
if (fetched && fetched.length > 0) {
// Append mod_rate placeholder so total stays 9
this._paramMeta = [
...fetched,
{ id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' },
];
}
} catch (err) {
console.warn('[ReverbModule] Could not load eoc-reverb.json, using static paramMeta:', err.message);
}
try {
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
} catch (_) { /* already registered */ }
await audioCtx.audioWorklet.addModule('faust/eoc-reverb-processor.js');
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-reverb-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
});
const wasmResp = await fetch('faust/eoc-reverb.wasm');
const wasmBytes = await wasmResp.arrayBuffer();
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('ReverbModule worklet init timeout')), 10000);
this._workletNode.port.onmessage = (e) => {
if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); }
if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); }
};
this._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes],
);
});
this._effectGain = audioCtx.createGain();
this._effectGain.gain.value = 1;
this._bypassIn.connect(this._workletNode);
this._workletNode.connect(this._effectGain);
this._effectGain.connect(this._bypassOut);
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const meta = this._paramMeta[index];
if (!meta) return;
// mod_rate (index 8) is a placeholder — skip sending to worklet
if (meta.id === 'mod_rate') return;
const raw = meta.min + normalizedValue * (meta.max - meta.min);
this._workletNode.port.postMessage({ type: 'setParam', index, value: raw });
}
// ---------------------------------------------------------------------------
// Bypass
// ---------------------------------------------------------------------------
_onBypassChange(enabled) {
if (!this._effectGain) return;
const t = this._audioCtx.currentTime;
this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005);
}
// ---------------------------------------------------------------------------
// Dispose
// ---------------------------------------------------------------------------
dispose() {
this._workletNode?.disconnect();
this._effectGain?.disconnect();
this._workletNode = null;
this._effectGain = null;
super.dispose();
}
}

View file

@ -12,6 +12,9 @@
// Reacts to 'eoc:change' events on window to stay in sync with external mutations.
import { EOCModule } from '../eoc/eoc-module.js';
import { EQModule } from '../eoc/modules/eq-module.js';
import { CompressorModule } from '../eoc/modules/compressor-module.js';
import { ReverbModule } from '../eoc/modules/reverb-module.js';
// ---------------------------------------------------------------------------
// Stub module factory
@ -37,6 +40,12 @@ const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'mast
* @returns {EOCModule}
*/
export function moduleFactory(id) {
// Real Faust WASM implementations
if (id === 'eq') return new EQModule();
if (id === 'compressor') return new CompressorModule();
if (id === 'reverb') return new ReverbModule();
// Stub for modules not yet implemented (saturation, delay, master)
const def = STUB_DEFS[id];
if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`);