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:
parent
6007ab3853
commit
0a5f2544ca
17 changed files with 1095 additions and 3 deletions
173
playground/faust/eoc-compressor-processor.js
Normal file
173
playground/faust/eoc-compressor-processor.js
Normal 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 (0–1)
|
||||||
|
* 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);
|
||||||
34
playground/faust/eoc-compressor.dsp
Normal file
34
playground/faust/eoc-compressor.dsp
Normal 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;
|
||||||
1
playground/faust/eoc-compressor.json
Normal file
1
playground/faust/eoc-compressor.json
Normal file
File diff suppressed because one or more lines are too long
BIN
playground/faust/eoc-compressor.wasm
Normal file
BIN
playground/faust/eoc-compressor.wasm
Normal file
Binary file not shown.
183
playground/faust/eoc-eq-processor.js
Normal file
183
playground/faust/eoc-eq-processor.js
Normal 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);
|
||||||
38
playground/faust/eoc-eq.dsp
Normal file
38
playground/faust/eoc-eq.dsp
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// eoc-eq.dsp — 4-band parametric EQ for MEMLNaut EOC chain
|
||||||
|
//
|
||||||
|
// Band 1: Low Shelf (default 80 Hz, range 20–500)
|
||||||
|
// Band 2: Low-Mid bell (default 400 Hz, range 100–2000)
|
||||||
|
// Band 3: High-Mid bell (default 2500 Hz, range 500–8000)
|
||||||
|
// Band 4: High Shelf (default 8000 Hz, range 2000–20000)
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
1
playground/faust/eoc-eq.json
Normal file
1
playground/faust/eoc-eq.json
Normal file
File diff suppressed because one or more lines are too long
BIN
playground/faust/eoc-eq.wasm
Normal file
BIN
playground/faust/eoc-eq.wasm
Normal file
Binary file not shown.
178
playground/faust/eoc-reverb-processor.js
Normal file
178
playground/faust/eoc-reverb-processor.js
Normal 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 (0–1)
|
||||||
|
* 2: hi_damp (0–1)
|
||||||
|
* 3: lo_damp (0–1)
|
||||||
|
* 4: mix (0–1)
|
||||||
|
* 5: predelay (ms)
|
||||||
|
* 6: size (0–1)
|
||||||
|
* 7: width (0–1)
|
||||||
|
*
|
||||||
|
* 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);
|
||||||
52
playground/faust/eoc-reverb.dsp
Normal file
52
playground/faust/eoc-reverb.dsp
Normal 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;
|
||||||
1
playground/faust/eoc-reverb.json
Normal file
1
playground/faust/eoc-reverb.json
Normal file
File diff suppressed because one or more lines are too long
BIN
playground/faust/eoc-reverb.wasm
Normal file
BIN
playground/faust/eoc-reverb.wasm
Normal file
Binary file not shown.
|
|
@ -3,5 +3,8 @@
|
||||||
// Import from here to get the full EOC API:
|
// Import from here to get the full EOC API:
|
||||||
// import { EOCModule, EOCChain } from './eoc/index.js';
|
// import { EOCModule, EOCChain } from './eoc/index.js';
|
||||||
|
|
||||||
export { EOCModule } from './eoc-module.js';
|
export { EOCModule } from './eoc-module.js';
|
||||||
export { EOCChain } from './eoc-chain.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';
|
||||||
|
|
|
||||||
129
playground/js/eoc/modules/compressor-module.js
Normal file
129
playground/js/eoc/modules/compressor-module.js
Normal 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.1–200 ms, init 10]
|
||||||
|
// 1: knee [0–24 dB, init 6]
|
||||||
|
// 2: makeup [0–24 dB, init 0]
|
||||||
|
// 3: mix [0–1, init 1] (dry/wet for parallel compression)
|
||||||
|
// 4: ratio [1–20, init 4]
|
||||||
|
// 5: release [10–2000 ms, init 100]
|
||||||
|
// 6: threshold [-60–0 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
143
playground/js/eoc/modules/eq-module.js
Normal file
143
playground/js/eoc/modules/eq-module.js
Normal 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 [20–500 Hz, init 80], gain1 [-12–+12 dB, init 0]
|
||||||
|
// Band 2 (Low-Mid): freq2 [100–2000 Hz, init 400], gain2 [-12–+12 dB, init 0], q2 [0.1–10, init 1]
|
||||||
|
// Band 3 (High-Mid): freq3 [500–8000 Hz, init 2500], gain3 [-12–+12 dB, init 0], q3 [0.1–10, init 1]
|
||||||
|
// Band 4 (High Shelf): freq4 [2000–20000 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
147
playground/js/eoc/modules/reverb-module.js
Normal file
147
playground/js/eoc/modules/reverb-module.js
Normal 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.1–20 s, init 3]
|
||||||
|
// 1: diffusion [0–1, init 0.7]
|
||||||
|
// 2: hi_damp [0–1, init 0.5]
|
||||||
|
// 3: lo_damp [0–1, init 0]
|
||||||
|
// 4: mix [0–1, init 0.2]
|
||||||
|
// 5: predelay [0–100 ms, init 0]
|
||||||
|
// 6: size [0–1, init 0.5]
|
||||||
|
// 7: width [0–1, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,10 @@
|
||||||
//
|
//
|
||||||
// Reacts to 'eoc:change' events on window to stay in sync with external mutations.
|
// Reacts to 'eoc:change' events on window to stay in sync with external mutations.
|
||||||
|
|
||||||
import { EOCModule } from '../eoc/eoc-module.js';
|
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
|
// Stub module factory
|
||||||
|
|
@ -37,6 +40,12 @@ const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'mast
|
||||||
* @returns {EOCModule}
|
* @returns {EOCModule}
|
||||||
*/
|
*/
|
||||||
export function moduleFactory(id) {
|
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];
|
const def = STUB_DEFS[id];
|
||||||
if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`);
|
if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue