Merge EOC Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm)

Resolved conflicts: merged all 6 module imports in eoc-chain-ui.js and index.js.
All moduleFactory cases now return real Faust WASM implementations — no stubs remain.
This commit is contained in:
w1n5t0n 2026-04-03 18:22:00 +01:00
commit 5ac67df699
17 changed files with 1527 additions and 8 deletions

View file

@ -0,0 +1,159 @@
/**
* eoc-delay-processor.js AudioWorklet processor for the EOC Stereo Delay.
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads eoc-delay.wasm compiled from eoc-delay.dsp.
*
* Parameter index order (alphabetical within group, matching eoc-delay.json):
* 0 feedback [0, 0.95]
* 1 lp_cutoff [500, 20000]
* 2 mix [0, 1]
* 3 ping_pong [0, 1]
* 4 spread [0, 1]
* 5 sync [0, 3] (nentry)
* 6 time [1, 2000]
*/
// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js.
class EOCDelayProcessor extends FaustWorkletProcessor {
constructor(options) {
super(options);
this._dsp = null;
this._paramAddresses = [];
this._blockSize = 128;
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);
const exports = instance.exports;
this._exports = exports;
this._heap = new Float32Array(memory.buffer);
this._heapi32 = new Int32Array(memory.buffer);
this._mem = memory;
// Create DSP instance
if (exports.createDSPInstance) {
this._dsp = exports.createDSPInstance();
} else if (exports.eoc_delay) {
this._dsp = exports.eoc_delay();
} else {
console.warn('[EOCDelayProcessor] No DSP factory found; exports:', Object.keys(exports));
return;
}
exports.init(this._dsp, sr);
this._buildParamIndex(exports, 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, path) {
for (const item of items) {
const label = item.label ?? '';
const type = item.type ?? '';
const addr = item.address ?? (path + '/' + label);
if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) {
addresses.push(addr);
} else if (item.items) {
walk(item.items, path + '/' + label);
}
}
}
walk(desc.ui ?? [], '');
this._paramAddresses = addresses;
}
_onSetParam(index, value) {
if (!this._exports || !this._dsp) return;
const addr = this._paramAddresses[index];
if (addr && this._exports.setParamValue) {
this._exports.setParamValue(this._dsp, addr, value);
}
}
_renderBlock(outL, outR, blockSize) {
if (!this._exports || !this._dsp) return;
const exports = this._exports;
const mem = this._mem;
const heap = this._heap;
const heapBytes = mem.buffer.byteLength;
const inLOff = (heapBytes >> 2) - blockSize * 4 - 128;
const inROff = inLOff + blockSize;
const outLOff = inROff + blockSize;
const outROff = outLOff + blockSize;
// Zero input buffers (delay is an effect — pass through audio)
// Input pointers
const i32 = this._heapi32;
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;
exports.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];
}
}
}
registerProcessor('eoc-delay-processor', EOCDelayProcessor);

View file

@ -0,0 +1,103 @@
// eoc-delay.dsp — Stereo delay effect for the EOC rack.
//
// Parameters (7):
// time [250ms, 1-2000ms] delay time (ms) when sync=0
// feedback [0.3, 0-0.95] feedback amount
// lp_cutoff [8000, 500-20000] LP filter cutoff on feedback path (Hz)
// ping_pong [0.0, 0-1] 0=normal stereo, 1=full ping-pong L<->R
// spread [0.5, 0-1] stereo width of delay tails
// sync [0] nentry: 0=free, 1=half, 2=quarter, 3=eighth (120bpm)
// mix [0.3, 0-1] dry/wet mix
import("stdfaust.lib");
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
time = hslider("time[unit:ms]", 250, 1, 2000, 0.1);
feedback = hslider("feedback", 0.3, 0, 0.95, 0.001);
lp_cutoff = hslider("lp_cutoff[unit:Hz]", 8000, 500, 20000, 1);
ping_pong = hslider("ping_pong", 0.0, 0, 1, 0.001);
spread = hslider("spread", 0.5, 0, 1, 0.001);
sync = nentry("sync", 0, 0, 3, 1);
mix = hslider("mix", 0.3, 0, 1, 0.001);
// ---------------------------------------------------------------------------
// Derived delay time: free or tempo-synced at 120bpm
// ---------------------------------------------------------------------------
bpm = 120.0;
beat_ms = 60000.0 / bpm;
sync_time_ms =
ba.if(sync < 0.5, time,
ba.if(sync < 1.5, beat_ms * 2.0,
ba.if(sync < 2.5, beat_ms,
beat_ms * 0.5)));
// Fixed max delay: 96000 samples (1s at 96kHz, covers all tempos for eighth notes at 30bpm+)
max_delay_samp = 96000;
// ---------------------------------------------------------------------------
// Stereo ping-pong delay.
// Uses f ~ (_, _) pattern for stereo feedback loop.
// de.delay used for variable delay (no LP on feedback path inside the loop —
// LP is applied to the whole wet signal for efficiency).
// ---------------------------------------------------------------------------
dSamp = int(sync_time_ms * float(ma.SR) / 1000.0);
delayCore(inL, inR, fbL, fbR) = wetL, wetR
with {
pp = ping_pong;
fb = feedback;
mixL = inL + (fbL * (1.0 - pp) + fbR * pp) * fb;
mixR = inR + (fbR * (1.0 - pp) + fbL * pp) * fb;
wetL = de.delay(max_delay_samp, dSamp, mixL);
wetR = de.delay(max_delay_samp, dSamp, mixR);
};
stereoDelay = (_, _) : delayCore ~ (_, _);
// ---------------------------------------------------------------------------
// Post-delay LP filter (tone shaping on the feedback tail)
// ---------------------------------------------------------------------------
delayLP(x) = fi.lowpass(1, lp_cutoff, x);
// ---------------------------------------------------------------------------
// Mid-side stereo spread
// ---------------------------------------------------------------------------
msSpread(inL, inR) = outL, outR
with {
w = spread;
mid = (inL + inR) * 0.5;
side = (inL - inR) * 0.5;
outL = mid + side * w * 2.0;
outR = mid - side * w * 2.0;
};
// ---------------------------------------------------------------------------
// Main process: dry/wet mix
// ---------------------------------------------------------------------------
// Delay + LP + spread on two signals
delayAndProcess(inL, inR) = wideL, wideR
with {
wetL0 = stereoDelay(inL, inR) : _,!;
wetR0 = stereoDelay(inL, inR) : !,_;
wetL1 = delayLP(wetL0);
wetR1 = delayLP(wetR0);
wideL = msSpread(wetL1, wetR1) : _,!;
wideR = msSpread(wetL1, wetR1) : !,_;
};
process(inL, inR) = outL, outR
with {
wideL = delayAndProcess(inL, inR) : _,!;
wideR = delayAndProcess(inL, inR) : !,_;
outL = inL * (1.0 - mix) + wideL * mix;
outR = inR * (1.0 - mix) + wideR * mix;
};

View file

@ -0,0 +1,131 @@
{
"name": "eoc-delay",
"filename": "eoc-delay.dsp",
"version": "2.83.1",
"compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0",
"library_list": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/stdfaust.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/maths.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/platform.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/basics.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/delays.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/filters.lib"],
"include_pathnames": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/.claude/worktrees/agent-a8d5e213/playground/faust"],
"size": 1048656,
"inputs": 2,
"outputs": 2,
"meta": [
{ "basics.lib/name": "Faust Basic Element Library" },
{ "basics.lib/version": "1.22.0" },
{ "compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },
{ "delays.lib/name": "Faust Delay Library" },
{ "delays.lib/version": "1.2.0" },
{ "filename": "eoc-delay.dsp" },
{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },
{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },
{ "filters.lib/lowpass:author": "Julius O. Smith III" },
{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },
{ "filters.lib/name": "Faust Filters Library" },
{ "filters.lib/tf1:author": "Julius O. Smith III" },
{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },
{ "filters.lib/tf1s:author": "Julius O. Smith III" },
{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },
{ "filters.lib/version": "1.7.1" },
{ "maths.lib/author": "GRAME" },
{ "maths.lib/copyright": "GRAME" },
{ "maths.lib/license": "LGPL with exception" },
{ "maths.lib/name": "Faust Math Library" },
{ "maths.lib/version": "2.9.0" },
{ "name": "eoc-delay" },
{ "platform.lib/name": "Generic Platform Library" },
{ "platform.lib/version": "1.3.0" }
],
"ui": [
{
"type": "vgroup",
"label": "eoc-delay",
"items": [
{
"type": "hslider",
"label": "feedback",
"varname": "fHslider1",
"shortname": "feedback",
"address": "/eoc-delay/feedback",
"init": 0.3,
"min": 0,
"max": 0.95,
"step": 0.001
},
{
"type": "hslider",
"label": "lp_cutoff",
"varname": "fHslider3",
"shortname": "lp_cutoff",
"address": "/eoc-delay/lp_cutoff",
"meta": [
{ "unit": "Hz" }
],
"init": 8000,
"min": 500,
"max": 20000,
"step": 1
},
{
"type": "hslider",
"label": "mix",
"varname": "fHslider5",
"shortname": "mix",
"address": "/eoc-delay/mix",
"init": 0.3,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "hslider",
"label": "ping_pong",
"varname": "fHslider0",
"shortname": "ping_pong",
"address": "/eoc-delay/ping_pong",
"init": 0,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "hslider",
"label": "spread",
"varname": "fHslider4",
"shortname": "spread",
"address": "/eoc-delay/spread",
"init": 0.5,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "nentry",
"label": "sync",
"varname": "fEntry0",
"shortname": "sync",
"address": "/eoc-delay/sync",
"init": 0,
"min": 0,
"max": 3,
"step": 1
},
{
"type": "hslider",
"label": "time",
"varname": "fHslider2",
"shortname": "time",
"address": "/eoc-delay/time",
"meta": [
{ "unit": "ms" }
],
"init": 250,
"min": 1,
"max": 2000,
"step": 0.1
}
]
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,149 @@
/**
* eoc-master-processor.js AudioWorklet processor for the EOC Master Bus.
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads eoc-master.wasm compiled from eoc-master.dsp.
*
* Parameter index order (alphabetical within group, matching eoc-master.json):
* 0 dc_block [0, 1] (nentry)
* 1 gain [0, 2]
* 2 limiter_thresh [-12, 0]
* 3 width [0, 2]
*/
// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js.
class EOCMasterProcessor extends FaustWorkletProcessor {
constructor(options) {
super(options);
this._dsp = null;
this._paramAddresses = [];
this._blockSize = 128;
this._sampleRate = 48000;
}
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);
const exports = instance.exports;
this._exports = exports;
this._heap = new Float32Array(memory.buffer);
this._heapi32 = new Int32Array(memory.buffer);
this._mem = memory;
if (exports.createDSPInstance) {
this._dsp = exports.createDSPInstance();
} else if (exports.eoc_master) {
this._dsp = exports.eoc_master();
} else {
console.warn('[EOCMasterProcessor] No DSP factory found; exports:', Object.keys(exports));
return;
}
exports.init(this._dsp, sr);
this._buildParamIndex(exports, 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, path) {
for (const item of items) {
const label = item.label ?? '';
const type = item.type ?? '';
const addr = item.address ?? (path + '/' + label);
if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) {
addresses.push(addr);
} else if (item.items) {
walk(item.items, path + '/' + label);
}
}
}
walk(desc.ui ?? [], '');
this._paramAddresses = addresses;
}
_onSetParam(index, value) {
if (!this._exports || !this._dsp) return;
const addr = this._paramAddresses[index];
if (addr && this._exports.setParamValue) {
this._exports.setParamValue(this._dsp, addr, value);
}
}
_renderBlock(outL, outR, blockSize) {
if (!this._exports || !this._dsp) return;
const exports = this._exports;
const mem = this._mem;
const heap = this._heap;
const heapBytes = mem.buffer.byteLength;
const inLOff = (heapBytes >> 2) - blockSize * 4 - 128;
const inROff = inLOff + blockSize;
const outLOff = inROff + blockSize;
const outROff = outLOff + blockSize;
const i32 = this._heapi32;
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;
exports.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];
}
}
}
registerProcessor('eoc-master-processor', EOCMasterProcessor);

View file

@ -0,0 +1,85 @@
// eoc-master.dsp — Master bus effect for the EOC rack.
//
// Parameters (4):
// gain [1.0, 0-2] output gain multiplier
// width [1.0, 0-2] stereo width: 0=mono, 1=normal, 2=extra-wide
// limiter_thresh [-1.0, -12 to 0] brick-wall limiter threshold (dB)
// dc_block [1] nentry: 0=off, 1=on — DC blocking filter
import("stdfaust.lib");
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
gain = hslider("gain", 1.0, 0, 2, 0.001);
width = hslider("width", 1.0, 0, 2, 0.001);
limiter_thresh = hslider("limiter_thresh[unit:dB]", -1.0, -12, 0, 0.1);
dc_block_on = nentry("dc_block", 1, 0, 1, 1);
// ---------------------------------------------------------------------------
// DC blocking filter (~10Hz one-pole HP)
// ---------------------------------------------------------------------------
dcBlock(on, x) = on * fi.highpass(1, 10.0, x) + (1.0 - on) * x;
// ---------------------------------------------------------------------------
// Stereo width via mid-side processing
// width = 0: mono (side removed)
// width = 1: original stereo
// width = 2: enhanced stereo (doubled sides)
// ---------------------------------------------------------------------------
stereoWidth(inL, inR) = outL, outR
with {
w = width;
mid = (inL + inR) * 0.5;
side = (inL - inR) * 0.5;
outL = mid + side * w;
outR = mid - side * w;
};
// ---------------------------------------------------------------------------
// Brick-wall limiter: peak-following gain reduction.
// Uses a leaky envelope follower with fast attack (~0.5ms) and slow release (~100ms).
// ---------------------------------------------------------------------------
threshLin = ba.db2linear(limiter_thresh);
// Leaky-peak envelope follower: exponential release ~100ms
// Takes a signal, outputs its peak envelope
releaseCoeff = exp(-1.0 / (float(ma.SR) * 0.100));
peakEnv = abs : (+ ~ *(releaseCoeff));
// Gain reduction: clamp to threshold
limiterGR(env) = threshLin / max(threshLin, env);
// Stereo limiter: linked L/R gain reduction from max of both peaks
limiter(inL, inR) = inL * gr, inR * gr
with {
envL = peakEnv(inL);
envR = peakEnv(inR);
peakStereo = max(envL, envR);
gr = limiterGR(peakStereo);
};
// ---------------------------------------------------------------------------
// Main process
// ---------------------------------------------------------------------------
process(inL, inR) = outL, outR
with {
// 1. DC block
dcL = dcBlock(dc_block_on, inL);
dcR = dcBlock(dc_block_on, inR);
// 2. Gain
gL = dcL * gain;
gR = dcR * gain;
// 3. Stereo width
wL = stereoWidth(gL, gR) : _,!;
wR = stereoWidth(gL, gR) : !,_;
// 4. Limiter
outL = limiter(wL, wR) : _,!;
outR = limiter(wL, wR) : !,_;
};

View file

@ -0,0 +1,92 @@
{
"name": "eoc-master",
"filename": "eoc-master.dsp",
"version": "2.83.1",
"compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0",
"library_list": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/stdfaust.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/filters.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/maths.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/platform.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/basics.lib"],
"include_pathnames": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/.claude/worktrees/agent-a8d5e213/playground/faust"],
"size": 88,
"inputs": 2,
"outputs": 2,
"meta": [
{ "basics.lib/name": "Faust Basic Element Library" },
{ "basics.lib/version": "1.22.0" },
{ "compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },
{ "filename": "eoc-master.dsp" },
{ "filters.lib/highpass:author": "Julius O. Smith III" },
{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },
{ "filters.lib/name": "Faust Filters Library" },
{ "filters.lib/tf1:author": "Julius O. Smith III" },
{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },
{ "filters.lib/tf1s:author": "Julius O. Smith III" },
{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },
{ "filters.lib/version": "1.7.1" },
{ "maths.lib/author": "GRAME" },
{ "maths.lib/copyright": "GRAME" },
{ "maths.lib/license": "LGPL with exception" },
{ "maths.lib/name": "Faust Math Library" },
{ "maths.lib/version": "2.9.0" },
{ "name": "eoc-master" },
{ "platform.lib/name": "Generic Platform Library" },
{ "platform.lib/version": "1.3.0" }
],
"ui": [
{
"type": "vgroup",
"label": "eoc-master",
"items": [
{
"type": "nentry",
"label": "dc_block",
"varname": "fEntry0",
"shortname": "dc_block",
"address": "/eoc-master/dc_block",
"init": 1,
"min": 0,
"max": 1,
"step": 1
},
{
"type": "hslider",
"label": "gain",
"varname": "fHslider2",
"shortname": "gain",
"address": "/eoc-master/gain",
"init": 1,
"min": 0,
"max": 2,
"step": 0.001
},
{
"type": "hslider",
"label": "limiter_thresh",
"varname": "fHslider0",
"shortname": "limiter_thresh",
"address": "/eoc-master/limiter_thresh",
"meta": [
{ "unit": "dB" }
],
"init": -1,
"min": -12,
"max": 0,
"step": 0.1
},
{
"type": "hslider",
"label": "width",
"varname": "fHslider1",
"shortname": "width",
"address": "/eoc-master/width",
"init": 1,
"min": 0,
"max": 2,
"step": 0.001
}
]
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,149 @@
/**
* eoc-saturation-processor.js AudioWorklet processor for the EOC Saturation.
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads eoc-saturation.wasm compiled from eoc-saturation.dsp.
*
* Parameter index order (alphabetical within group, matching eoc-saturation.json):
* 0 character [0, 1]
* 1 drive [0, 1]
* 2 mix [0, 1]
* 3 tone [0, 1]
*/
// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js.
class EOCSaturationProcessor extends FaustWorkletProcessor {
constructor(options) {
super(options);
this._dsp = null;
this._paramAddresses = [];
this._blockSize = 128;
this._sampleRate = 48000;
}
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);
const exports = instance.exports;
this._exports = exports;
this._heap = new Float32Array(memory.buffer);
this._heapi32 = new Int32Array(memory.buffer);
this._mem = memory;
if (exports.createDSPInstance) {
this._dsp = exports.createDSPInstance();
} else if (exports.eoc_saturation) {
this._dsp = exports.eoc_saturation();
} else {
console.warn('[EOCSaturationProcessor] No DSP factory found; exports:', Object.keys(exports));
return;
}
exports.init(this._dsp, sr);
this._buildParamIndex(exports, 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, path) {
for (const item of items) {
const label = item.label ?? '';
const type = item.type ?? '';
const addr = item.address ?? (path + '/' + label);
if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) {
addresses.push(addr);
} else if (item.items) {
walk(item.items, path + '/' + label);
}
}
}
walk(desc.ui ?? [], '');
this._paramAddresses = addresses;
}
_onSetParam(index, value) {
if (!this._exports || !this._dsp) return;
const addr = this._paramAddresses[index];
if (addr && this._exports.setParamValue) {
this._exports.setParamValue(this._dsp, addr, value);
}
}
_renderBlock(outL, outR, blockSize) {
if (!this._exports || !this._dsp) return;
const exports = this._exports;
const mem = this._mem;
const heap = this._heap;
const heapBytes = mem.buffer.byteLength;
const inLOff = (heapBytes >> 2) - blockSize * 4 - 128;
const inROff = inLOff + blockSize;
const outLOff = inROff + blockSize;
const outROff = outLOff + blockSize;
const i32 = this._heapi32;
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;
exports.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];
}
}
}
registerProcessor('eoc-saturation-processor', EOCSaturationProcessor);

View file

@ -0,0 +1,71 @@
// eoc-saturation.dsp — Stereo saturation effect for the EOC rack.
//
// Parameters (4):
// drive [0.0, 0-1] 0=clean, 1=full drive
// character [0.0, 0-1] 0=soft-clip/tanh, 0.5=tape/asymmetric, 1=hard-clip
// tone [0.5, 0-1] post-saturation tone: 0=dark (LP), 1=bright (HP blend)
// mix [1.0, 0-1] dry/wet for parallel saturation
import("stdfaust.lib");
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
drive = hslider("drive", 0.0, 0, 1, 0.001);
character = hslider("character", 0.0, 0, 1, 0.001);
tone = hslider("tone", 0.5, 0, 1, 0.001);
mix = hslider("mix", 1.0, 0, 1, 0.001);
// ---------------------------------------------------------------------------
// Saturation shapes
// ---------------------------------------------------------------------------
driveGain = 1.0 + drive * 15.0; // 1x to 16x gain before clipping
// Soft clip: tanh
softClip(x) = ma.tanh(x);
// Tape: asymmetric waveshaper — slightly harder clipping on positive peaks
// Blends tanh with a gentle second-harmonic bias
tapeClip(x) = softClip(x * 1.2) * 0.55 + softClip(x) * 0.45 + x * x * 0.04 * (1.0 - softClip(abs(x)));
// Hard clip: simple saturate
hardClip(x) = max(-1.0, min(1.0, x));
// Character crossfade between the three shapes:
// c=0.0 → soft (tanh)
// c=0.5 → tape (asymmetric)
// c=1.0 → hard clip
saturate(c, x) =
softClip(x) * (max(0.0, 1.0 - c * 2.0)) +
tapeClip(x) * (1.0 - abs(c - 0.5) * 2.0) +
hardClip(x) * max(0.0, (c - 0.5) * 2.0);
// Apply drive, saturate, and normalise output level
processChannel(x) = saturate(character, x * driveGain) / max(0.001, sqrt(driveGain));
// ---------------------------------------------------------------------------
// Tone control: LP/HP blend via one-pole filters
//
// tone=0.0 → dark (400Hz LP)
// tone=0.5 → flat (passthrough)
// tone=1.0 → bright (8kHz HP blend)
// ---------------------------------------------------------------------------
toneFreqLP = 400.0 + tone * 19600.0; // 400Hz to 20kHz (fully open at tone=1)
toneFreqHP = 200.0 + tone * 7800.0; // 200Hz to 8kHz
applyTone(x) = fi.lowpass(1, toneFreqLP, x);
// ---------------------------------------------------------------------------
// Main process: stereo saturation with parallel dry/wet mix
// ---------------------------------------------------------------------------
process(inL, inR) = outL, outR
with {
wetL = applyTone(processChannel(inL));
wetR = applyTone(processChannel(inR));
outL = inL * (1.0 - mix) + wetL * mix;
outR = inR * (1.0 - mix) + wetR * mix;
};

View file

@ -0,0 +1,88 @@
{
"name": "eoc-saturation",
"filename": "eoc-saturation.dsp",
"version": "2.83.1",
"compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0",
"library_list": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/stdfaust.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/maths.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/filters.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/platform.lib"],
"include_pathnames": ["/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/.claude/worktrees/agent-a8d5e213/playground/faust"],
"size": 56,
"inputs": 2,
"outputs": 2,
"meta": [
{ "compile_options": "-lang cpp -fpga-mem-th 4 -ct 1 -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },
{ "filename": "eoc-saturation.dsp" },
{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },
{ "filters.lib/lowpass:author": "Julius O. Smith III" },
{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },
{ "filters.lib/name": "Faust Filters Library" },
{ "filters.lib/tf1:author": "Julius O. Smith III" },
{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },
{ "filters.lib/tf1s:author": "Julius O. Smith III" },
{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>" },
{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },
{ "filters.lib/version": "1.7.1" },
{ "maths.lib/author": "GRAME" },
{ "maths.lib/copyright": "GRAME" },
{ "maths.lib/license": "LGPL with exception" },
{ "maths.lib/name": "Faust Math Library" },
{ "maths.lib/version": "2.9.0" },
{ "name": "eoc-saturation" },
{ "platform.lib/name": "Generic Platform Library" },
{ "platform.lib/version": "1.3.0" }
],
"ui": [
{
"type": "vgroup",
"label": "eoc-saturation",
"items": [
{
"type": "hslider",
"label": "character",
"varname": "fHslider1",
"shortname": "character",
"address": "/eoc-saturation/character",
"init": 0,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "hslider",
"label": "drive",
"varname": "fHslider0",
"shortname": "drive",
"address": "/eoc-saturation/drive",
"init": 0,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "hslider",
"label": "mix",
"varname": "fHslider3",
"shortname": "mix",
"address": "/eoc-saturation/mix",
"init": 1,
"min": 0,
"max": 1,
"step": 0.001
},
{
"type": "hslider",
"label": "tone",
"varname": "fHslider2",
"shortname": "tone",
"address": "/eoc-saturation/tone",
"init": 0.5,
"min": 0,
"max": 1,
"step": 0.001
}
]
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,11 @@
export { EOCModule } from './eoc-module.js';
export { EOCChain } from './eoc-chain.js';
// Effect module implementations
export { EQModule } from './modules/eq-module.js';
export { CompressorModule } from './modules/compressor-module.js';
export { ReverbModule } from './modules/reverb-module.js';
export { DelayModule } from './modules/delay-module.js';
export { SaturationModule } from './modules/saturation-module.js';
export { MasterModule } from './modules/master-module.js';

View file

@ -0,0 +1,138 @@
// delay-module.js — EOC Stereo Delay module (meml-2sl).
//
// Faust source: playground/faust/eoc-delay.dsp
// Worklet processor: playground/faust/eoc-delay-processor.js
// Processor name: 'eoc-delay-processor'
//
// 7 parameters (in JSON / Faust alphabetical order):
// 0 feedback [0, 0.95] init=0.3
// 1 lp_cutoff [500, 20000] init=8000
// 2 mix [0, 1] init=0.3
// 3 ping_pong [0, 1] init=0.0
// 4 spread [0, 1] init=0.5
// 5 sync [0, 3] init=0 (nentry)
// 6 time [1, 2000] init=250
import { EOCModule } from '../eoc-module.js';
import { loadFaustParamMeta } from '../../synth/faust-param-meta.js';
export class DelayModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null; // GainNode: muted when bypassed
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'delay'; }
get displayName() { return 'Delay'; }
get paramMeta() {
// Normalized init values derived from Faust defaults
return [
{ id: 'feedback', name: 'Feedback', min: 0, max: 0.95, init: 0.3 / 0.95, curve: 0.5, group: 'Delay' },
{ id: 'lp_cutoff', name: 'LP Cutoff', min: 500, max: 20000, init: (8000 - 500) / 19500, curve: 0.35, group: 'Delay' },
{ id: 'mix', name: 'Mix', min: 0, max: 1, init: 0.3, curve: 0.5, group: 'Delay' },
{ id: 'ping_pong', name: 'Ping-Pong', min: 0, max: 1, init: 0.0, curve: 0.5, group: 'Delay' },
{ id: 'spread', name: 'Spread', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Delay' },
{ id: 'sync', name: 'Sync', min: 0, max: 3, init: 0.0, curve: 0.5, group: 'Delay' },
{ id: 'time', name: 'Time (ms)', min: 1, max: 2000, init: (250 - 1) / 1999, curve: 0.35, group: 'Delay' },
];
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
// Fetch WASM bytes
const wasmResp = await fetch('faust/eoc-delay.wasm');
if (!wasmResp.ok) throw new Error(`[DelayModule] Failed to fetch eoc-delay.wasm: ${wasmResp.status}`);
const wasmBytes = await wasmResp.arrayBuffer();
// Register worklet (browser deduplicates)
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
await audioCtx.audioWorklet.addModule('faust/eoc-delay-processor.js');
// Create worklet node: 2-in / 2-out stereo effect
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-delay-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
channelCount: 2,
channelCountMode: 'explicit',
});
// Listen for ready / error messages
this._workletNode.port.onmessage = (e) => {
if (e.data?.type === 'error') {
console.error('[DelayModule] Worklet error:', e.data.message);
}
};
// Effect gain node — muted when bypassed
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);
// Send init message to worklet
this._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes]
);
// Apply initial default param values
this.paramMeta.forEach((_, i) => {
this.setParam(i, this.getCurrentParamValue(i));
});
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const raw = this._denormalize(index, normalizedValue);
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);
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
dispose() {
if (this._workletNode) {
this._workletNode.disconnect();
this._workletNode.port.onmessage = null;
this._workletNode = null;
}
if (this._effectGain) {
this._effectGain.disconnect();
this._effectGain = null;
}
super.dispose();
}
}

View file

@ -0,0 +1,123 @@
// master-module.js — EOC Master Bus module (meml-2zm part 2).
//
// Faust source: playground/faust/eoc-master.dsp
// Worklet processor: playground/faust/eoc-master-processor.js
// Processor name: 'eoc-master-processor'
//
// 4 parameters (in JSON / Faust alphabetical order):
// 0 dc_block [0, 1] init=1 (nentry: off/on)
// 1 gain [0, 2] init=1.0
// 2 limiter_thresh [-12, 0] init=-1.0
// 3 width [0, 2] init=1.0
import { EOCModule } from '../eoc-module.js';
export class MasterModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'master'; }
get displayName() { return 'Master Bus'; }
get paramMeta() {
return [
{ id: 'dc_block', name: 'DC Block', min: 0, max: 1, init: 1.0, curve: 0.5, group: 'Master' },
{ id: 'gain', name: 'Gain', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' },
{ id: 'limiter_thresh', name: 'Limiter (dB)', min: -12, max: 0, init: (-1.0 - (-12)) / 12, curve: 0.5, group: 'Master' },
{ id: 'width', name: 'Width', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' },
];
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
const wasmResp = await fetch('faust/eoc-master.wasm');
if (!wasmResp.ok) throw new Error(`[MasterModule] Failed to fetch eoc-master.wasm: ${wasmResp.status}`);
const wasmBytes = await wasmResp.arrayBuffer();
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
await audioCtx.audioWorklet.addModule('faust/eoc-master-processor.js');
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-master-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
channelCount: 2,
channelCountMode: 'explicit',
});
this._workletNode.port.onmessage = (e) => {
if (e.data?.type === 'error') {
console.error('[MasterModule] Worklet error:', e.data.message);
}
};
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._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes]
);
this.paramMeta.forEach((_, i) => {
this.setParam(i, this.getCurrentParamValue(i));
});
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const raw = this._denormalize(index, normalizedValue);
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);
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
dispose() {
if (this._workletNode) {
this._workletNode.disconnect();
this._workletNode.port.onmessage = null;
this._workletNode = null;
}
if (this._effectGain) {
this._effectGain.disconnect();
this._effectGain = null;
}
super.dispose();
}
}

View file

@ -0,0 +1,123 @@
// saturation-module.js — EOC Saturation module (meml-2zm part 1).
//
// Faust source: playground/faust/eoc-saturation.dsp
// Worklet processor: playground/faust/eoc-saturation-processor.js
// Processor name: 'eoc-saturation-processor'
//
// 4 parameters (in JSON / Faust alphabetical order):
// 0 character [0, 1] init=0.0
// 1 drive [0, 1] init=0.0
// 2 mix [0, 1] init=1.0
// 3 tone [0, 1] init=0.5
import { EOCModule } from '../eoc-module.js';
export class SaturationModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'saturation'; }
get displayName() { return 'Saturation'; }
get paramMeta() {
return [
{ id: 'character', name: 'Character', min: 0, max: 1, init: 0.0, curve: 0.5, group: 'Saturation' },
{ id: 'drive', name: 'Drive', min: 0, max: 1, init: 0.0, curve: 0.6, group: 'Saturation' },
{ id: 'mix', name: 'Mix', min: 0, max: 1, init: 1.0, curve: 0.5, group: 'Saturation' },
{ id: 'tone', name: 'Tone', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Saturation' },
];
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
const wasmResp = await fetch('faust/eoc-saturation.wasm');
if (!wasmResp.ok) throw new Error(`[SaturationModule] Failed to fetch eoc-saturation.wasm: ${wasmResp.status}`);
const wasmBytes = await wasmResp.arrayBuffer();
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
await audioCtx.audioWorklet.addModule('faust/eoc-saturation-processor.js');
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-saturation-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
channelCount: 2,
channelCountMode: 'explicit',
});
this._workletNode.port.onmessage = (e) => {
if (e.data?.type === 'error') {
console.error('[SaturationModule] Worklet error:', e.data.message);
}
};
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._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes]
);
this.paramMeta.forEach((_, i) => {
this.setParam(i, this.getCurrentParamValue(i));
});
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const raw = this._denormalize(index, normalizedValue);
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);
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
dispose() {
if (this._workletNode) {
this._workletNode.disconnect();
this._workletNode.port.onmessage = null;
this._workletNode = null;
}
if (this._effectGain) {
this._effectGain.disconnect();
this._effectGain = null;
}
super.dispose();
}
}

View file

@ -15,6 +15,9 @@ 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';
import { DelayModule } from '../eoc/modules/delay-module.js';
import { SaturationModule } from '../eoc/modules/saturation-module.js';
import { MasterModule } from '../eoc/modules/master-module.js';
// ---------------------------------------------------------------------------
// Stub module factory
@ -41,11 +44,16 @@ const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'mast
*/
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();
switch (id) {
case 'eq': return new EQModule();
case 'compressor': return new CompressorModule();
case 'reverb': return new ReverbModule();
case 'delay': return new DelayModule();
case 'saturation': return new SaturationModule();
case 'master': return new MasterModule();
default: break;
}
// Stub for modules not yet implemented (saturation, delay, master)
const def = STUB_DEFS[id];
if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`);