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.
|
|
@ -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';
|
||||
|
|
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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}'`);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue