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:
commit
5ac67df699
17 changed files with 1527 additions and 8 deletions
159
playground/faust/eoc-delay-processor.js
Normal file
159
playground/faust/eoc-delay-processor.js
Normal 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);
|
||||||
103
playground/faust/eoc-delay.dsp
Normal file
103
playground/faust/eoc-delay.dsp
Normal 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;
|
||||||
|
};
|
||||||
131
playground/faust/eoc-delay.json
Normal file
131
playground/faust/eoc-delay.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
playground/faust/eoc-delay.wasm
Normal file
35
playground/faust/eoc-delay.wasm
Normal file
File diff suppressed because one or more lines are too long
149
playground/faust/eoc-master-processor.js
Normal file
149
playground/faust/eoc-master-processor.js
Normal 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);
|
||||||
85
playground/faust/eoc-master.dsp
Normal file
85
playground/faust/eoc-master.dsp
Normal 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) : !,_;
|
||||||
|
};
|
||||||
92
playground/faust/eoc-master.json
Normal file
92
playground/faust/eoc-master.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
31
playground/faust/eoc-master.wasm
Normal file
31
playground/faust/eoc-master.wasm
Normal file
File diff suppressed because one or more lines are too long
149
playground/faust/eoc-saturation-processor.js
Normal file
149
playground/faust/eoc-saturation-processor.js
Normal 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);
|
||||||
71
playground/faust/eoc-saturation.dsp
Normal file
71
playground/faust/eoc-saturation.dsp
Normal 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;
|
||||||
|
};
|
||||||
88
playground/faust/eoc-saturation.json
Normal file
88
playground/faust/eoc-saturation.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
playground/faust/eoc-saturation.wasm
Normal file
29
playground/faust/eoc-saturation.wasm
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,11 @@
|
||||||
|
|
||||||
export { EOCModule } from './eoc-module.js';
|
export { EOCModule } from './eoc-module.js';
|
||||||
export { EOCChain } from './eoc-chain.js';
|
export { EOCChain } from './eoc-chain.js';
|
||||||
|
|
||||||
|
// Effect module implementations
|
||||||
export { EQModule } from './modules/eq-module.js';
|
export { EQModule } from './modules/eq-module.js';
|
||||||
export { CompressorModule } from './modules/compressor-module.js';
|
export { CompressorModule } from './modules/compressor-module.js';
|
||||||
export { ReverbModule } from './modules/reverb-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';
|
||||||
|
|
|
||||||
138
playground/js/eoc/modules/delay-module.js
Normal file
138
playground/js/eoc/modules/delay-module.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
123
playground/js/eoc/modules/master-module.js
Normal file
123
playground/js/eoc/modules/master-module.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
123
playground/js/eoc/modules/saturation-module.js
Normal file
123
playground/js/eoc/modules/saturation-module.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,10 +11,13 @@
|
||||||
//
|
//
|
||||||
// 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 { EQModule } from '../eoc/modules/eq-module.js';
|
||||||
import { CompressorModule } from '../eoc/modules/compressor-module.js';
|
import { CompressorModule } from '../eoc/modules/compressor-module.js';
|
||||||
import { ReverbModule } from '../eoc/modules/reverb-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
|
// Stub module factory
|
||||||
|
|
@ -41,11 +44,16 @@ const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'mast
|
||||||
*/
|
*/
|
||||||
export function moduleFactory(id) {
|
export function moduleFactory(id) {
|
||||||
// Real Faust WASM implementations
|
// Real Faust WASM implementations
|
||||||
if (id === 'eq') return new EQModule();
|
switch (id) {
|
||||||
if (id === 'compressor') return new CompressorModule();
|
case 'eq': return new EQModule();
|
||||||
if (id === 'reverb') return new ReverbModule();
|
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];
|
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