From 0a5f2544ca395d057977b33552417c8d79394771 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Fri, 3 Apr 2026 17:57:28 +0100 Subject: [PATCH] feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Faust DSP sources, compiled WASM + JSON descriptors, AudioWorklet processors, and EOCModule subclasses for all three effects. - meml-4b4: 4-band parametric EQ (fi.low_shelf, fi.peak_eq × 2, fi.high_shelf) 10 params: freq/gain per band, Q for the two mid bell bands - meml-cpe: stereo feed-forward compressor (co.compressor_mono × 2) 7 params: threshold, ratio, attack, release, knee, makeup, mix (parallel) - meml-wwc: zita reverb (re.zita_rev1_stereo) with pre-delay and M/S width 8 active params + mod_rate placeholder (zita internal mod not yet exposed) All three DSPs compile cleanly under Faust 2.83.1. moduleFactory in eoc-chain-ui.js now returns real instances for eq/compressor/reverb. --- playground/faust/eoc-compressor-processor.js | 173 +++++++++++++++++ playground/faust/eoc-compressor.dsp | 34 ++++ playground/faust/eoc-compressor.json | 1 + playground/faust/eoc-compressor.wasm | Bin 0 -> 7922 bytes playground/faust/eoc-eq-processor.js | 183 ++++++++++++++++++ playground/faust/eoc-eq.dsp | 38 ++++ playground/faust/eoc-eq.json | 1 + playground/faust/eoc-eq.wasm | Bin 0 -> 14116 bytes playground/faust/eoc-reverb-processor.js | 178 +++++++++++++++++ playground/faust/eoc-reverb.dsp | 52 +++++ playground/faust/eoc-reverb.json | 1 + playground/faust/eoc-reverb.wasm | Bin 0 -> 17662 bytes playground/js/eoc/index.js | 7 +- .../js/eoc/modules/compressor-module.js | 129 ++++++++++++ playground/js/eoc/modules/eq-module.js | 143 ++++++++++++++ playground/js/eoc/modules/reverb-module.js | 147 ++++++++++++++ playground/js/ui/eoc-chain-ui.js | 11 +- 17 files changed, 1095 insertions(+), 3 deletions(-) create mode 100644 playground/faust/eoc-compressor-processor.js create mode 100644 playground/faust/eoc-compressor.dsp create mode 100644 playground/faust/eoc-compressor.json create mode 100644 playground/faust/eoc-compressor.wasm create mode 100644 playground/faust/eoc-eq-processor.js create mode 100644 playground/faust/eoc-eq.dsp create mode 100644 playground/faust/eoc-eq.json create mode 100644 playground/faust/eoc-eq.wasm create mode 100644 playground/faust/eoc-reverb-processor.js create mode 100644 playground/faust/eoc-reverb.dsp create mode 100644 playground/faust/eoc-reverb.json create mode 100644 playground/faust/eoc-reverb.wasm create mode 100644 playground/js/eoc/modules/compressor-module.js create mode 100644 playground/js/eoc/modules/eq-module.js create mode 100644 playground/js/eoc/modules/reverb-module.js diff --git a/playground/faust/eoc-compressor-processor.js b/playground/faust/eoc-compressor-processor.js new file mode 100644 index 0000000..c77e727 --- /dev/null +++ b/playground/faust/eoc-compressor-processor.js @@ -0,0 +1,173 @@ +/** + * eoc-compressor-processor.js — AudioWorklet processor for the stereo compressor + * + * Extends FaustWorkletProcessor (faust-worklet-processor.js). + * Loads eoc-compressor.wasm compiled from eoc-compressor.dsp. + * + * 7 params (alphabetical order as emitted by Faust JSON): + * 0: attack (ms) + * 1: knee (dB) + * 2: makeup (dB) + * 3: mix (0–1) + * 4: ratio + * 5: release (ms) + * 6: threshold (dB) + */ + +// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first. + +class EOCCompressorProcessor extends FaustWorkletProcessor { + constructor(options) { + super(options); + this._dsp = null; + this._exports = null; + this._heap = null; + this._heapi32 = null; + this._mem = null; + this._paramAddresses = []; + this._inputL = null; + this._inputR = null; + this._sampleRate = 48000; + } + + // --------------------------------------------------------------------------- + // FaustWorkletProcessor overrides + // --------------------------------------------------------------------------- + + async _initWasm(wasmBytes, sr) { + this._sampleRate = sr; + + const module = await WebAssembly.compile(wasmBytes); + const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); + + const imports = { + env: { + memory, + memoryBase: 0, + tableBase: 0, + _abs: Math.abs, + _acosf: Math.acos, + _asinf: Math.asin, + _atanf: Math.atan, + _atan2f: Math.atan2, + _ceilf: Math.ceil, + _cosf: Math.cos, + _expf: Math.exp, + _floorf: Math.floor, + _fmodf: (x, y) => x % y, + _logf: Math.log, + _log10f: Math.log10, + _max_f: Math.max, + _min_f: Math.min, + _remainderf: (x, y) => x - Math.round(x / y) * y, + _powf: Math.pow, + _roundf: Math.round, + _sinf: Math.sin, + _sqrtf: Math.sqrt, + _tanf: Math.tan, + _fabs: Math.abs, + table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), + }, + }; + + const instance = await WebAssembly.instantiate(module, imports); + this._exports = instance.exports; + this._mem = memory; + this._heap = new Float32Array(memory.buffer); + this._heapi32 = new Int32Array(memory.buffer); + + const exps = this._exports; + + if (exps.createDSPInstance) { + this._dsp = exps.createDSPInstance(); + } else if (exps.eoc_compressor) { + this._dsp = exps.eoc_compressor(); + } else { + const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); + console.warn('[EOCCompressorProcessor] No DSP constructor found; exports:', fnKeys); + return; + } + + exps.init(this._dsp, sr); + this._buildParamIndex(exps, memory); + } + + _buildParamIndex(exports, memory) { + if (!exports.getJSON) return; + const ptr = exports.getJSON(this._dsp); + const buf = new Uint8Array(memory.buffer); + let str = ''; + let i = ptr; + while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } + + let desc; + try { desc = JSON.parse(str); } catch { return; } + + const addresses = []; + + function walk(items) { + for (const item of items) { + const type = item.type ?? ''; + if (['hslider', 'vslider', 'nentry'].includes(type)) { + addresses.push(item.address ?? ''); + } else if (item.items) { + walk(item.items); + } + } + } + + walk(desc.ui ?? []); + this._paramAddresses = addresses; + } + + _onSetParam(index, value) { + if (!this._exports || !this._dsp) return; + const addr = this._paramAddresses[index]; + if (addr !== undefined && this._exports.setParamValue) { + this._exports.setParamValue(this._dsp, addr, value); + } + } + + _renderBlock(outL, outR, blockSize) { + if (!this._exports || !this._dsp) return; + + const exps = this._exports; + const mem = this._mem; + const heap = this._heap; + const i32 = this._heapi32; + + const heapWords = mem.buffer.byteLength >> 2; + const inLOff = heapWords - blockSize * 6 - 32; + const inROff = inLOff + blockSize; + const outLOff = inROff + blockSize; + const outROff = outLOff + blockSize; + const inPtrsOff = outROff + blockSize; + const outPtrsOff = inPtrsOff + 2; + + i32[inPtrsOff] = inLOff * 4; + i32[inPtrsOff + 1] = inROff * 4; + i32[outPtrsOff] = outLOff * 4; + i32[outPtrsOff + 1] = outROff * 4; + + const srcL = this._inputL; + const srcR = this._inputR; + if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; + if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; + + exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); + + for (let i = 0; i < blockSize; i++) { + outL[i] = heap[outLOff + i]; + outR[i] = heap[outROff + i]; + } + } + + process(inputs, outputs, params) { + const inp = inputs[0]; + this._inputL = inp?.[0] ?? null; + this._inputR = inp?.[1] ?? inp?.[0] ?? null; + return super.process(inputs, outputs, params); + } +} + +registerProcessor('eoc-compressor-processor', EOCCompressorProcessor); diff --git a/playground/faust/eoc-compressor.dsp b/playground/faust/eoc-compressor.dsp new file mode 100644 index 0000000..3b467ba --- /dev/null +++ b/playground/faust/eoc-compressor.dsp @@ -0,0 +1,34 @@ +// eoc-compressor.dsp — Stereo feed-forward compressor for MEMLNaut EOC chain +// +// 7 params: threshold, ratio, attack, release, knee, makeup, mix +// +// Compile: +// faust -lang wasm -cn eoc_compressor -e eoc-compressor.dsp -o eoc-compressor.wasm -json + +import("stdfaust.lib"); + +threshold = hslider("threshold[unit:dB]", -24.0, -60.0, 0.0, 0.1); +ratio = hslider("ratio", 4.0, 1.0, 20.0, 0.1); +attack = hslider("attack[unit:ms]", 10.0, 0.1, 200.0, 0.1); +release = hslider("release[unit:ms]", 100.0, 10.0, 2000.0, 1.0); +knee = hslider("knee[unit:dB]", 6.0, 0.0, 24.0, 0.1); +makeup = hslider("makeup[unit:dB]", 0.0, 0.0, 24.0, 0.1); +mix = hslider("mix", 1.0, 0.0, 1.0, 0.001); + +// Convert ms to seconds for Faust +attackSec = attack / 1000.0; +releaseSec = release / 1000.0; + +// Soft-knee threshold adjustment (shift threshold down by half the knee) +threshKnee = threshold - knee / 2.0; + +// Makeup gain as linear multiplier +makeupLin = ba.db2linear(makeup); + +// Compressor on a single channel with makeup applied +compCh(x) = co.compressor_mono(ratio, threshKnee, attackSec, releaseSec, x) * makeupLin; + +// Parallel compression (dry/wet blend) +parallelComp(x) = (1.0 - mix) * x + mix * compCh(x); + +process = parallelComp, parallelComp; diff --git a/playground/faust/eoc-compressor.json b/playground/faust/eoc-compressor.json new file mode 100644 index 0000000..c2a096b --- /dev/null +++ b/playground/faust/eoc-compressor.json @@ -0,0 +1 @@ +{"name": "eoc-compressor","filename": "eoc-compressor.dsp","version": "2.83.1","compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_compressor -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/basics.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/compressors.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/analyzers.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/signals.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/maths.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-a42cfe12/playground/faust"],"size": 68,"code": "Ly8gZW9jLWNvbXByZXNzb3IuZHNwIOKAlCBTdGVyZW8gZmVlZC1mb3J3YXJkIGNvbXByZXNzb3IgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyA3IHBhcmFtczogdGhyZXNob2xkLCByYXRpbywgYXR0YWNrLCByZWxlYXNlLCBrbmVlLCBtYWtldXAsIG1peAovLwovLyBDb21waWxlOgovLyAgIGZhdXN0IC1sYW5nIHdhc20gLWNuIGVvY19jb21wcmVzc29yIC1lIGVvYy1jb21wcmVzc29yLmRzcCAtbyBlb2MtY29tcHJlc3Nvci53YXNtIC1qc29uCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKdGhyZXNob2xkID0gaHNsaWRlcigidGhyZXNob2xkW3VuaXQ6ZEJdIiwgLTI0LjAsIC02MC4wLCAgIDAuMCwgIDAuMSk7CnJhdGlvICAgICA9IGhzbGlkZXIoInJhdGlvIiwgICAgICAgICAgICAgICAgNC4wLCAgIDEuMCwgIDIwLjAsICAwLjEpOwphdHRhY2sgICAgPSBoc2xpZGVyKCJhdHRhY2tbdW5pdDptc10iLCAgICAgMTAuMCwgICAwLjEsIDIwMC4wLCAgMC4xKTsKcmVsZWFzZSAgID0gaHNsaWRlcigicmVsZWFzZVt1bml0Om1zXSIsICAgMTAwLjAsICAxMC4wLCAyMDAwLjAsIDEuMCk7CmtuZWUgICAgICA9IGhzbGlkZXIoImtuZWVbdW5pdDpkQl0iLCAgICAgICAgNi4wLCAgIDAuMCwgIDI0LjAsICAwLjEpOwptYWtldXAgICAgPSBoc2xpZGVyKCJtYWtldXBbdW5pdDpkQl0iLCAgICAgIDAuMCwgICAwLjAsICAyNC4wLCAgMC4xKTsKbWl4ICAgICAgID0gaHNsaWRlcigibWl4IiwgICAgICAgICAgICAgICAgICAxLjAsICAgMC4wLCAgIDEuMCwgIDAuMDAxKTsKCi8vIENvbnZlcnQgbXMgdG8gc2Vjb25kcyBmb3IgRmF1c3QKYXR0YWNrU2VjICA9IGF0dGFjayAgLyAxMDAwLjA7CnJlbGVhc2VTZWMgPSByZWxlYXNlIC8gMTAwMC4wOwoKLy8gU29mdC1rbmVlIHRocmVzaG9sZCBhZGp1c3RtZW50IChzaGlmdCB0aHJlc2hvbGQgZG93biBieSBoYWxmIHRoZSBrbmVlKQp0aHJlc2hLbmVlID0gdGhyZXNob2xkIC0ga25lZSAvIDIuMDsKCi8vIE1ha2V1cCBnYWluIGFzIGxpbmVhciBtdWx0aXBsaWVyCm1ha2V1cExpbiA9IGJhLmRiMmxpbmVhcihtYWtldXApOwoKLy8gQ29tcHJlc3NvciBvbiBhIHNpbmdsZSBjaGFubmVsIHdpdGggbWFrZXVwIGFwcGxpZWQKY29tcENoKHgpID0gY28uY29tcHJlc3Nvcl9tb25vKHJhdGlvLCB0aHJlc2hLbmVlLCBhdHRhY2tTZWMsIHJlbGVhc2VTZWMsIHgpICogbWFrZXVwTGluOwoKLy8gUGFyYWxsZWwgY29tcHJlc3Npb24gKGRyeS93ZXQgYmxlbmQpCnBhcmFsbGVsQ29tcCh4KSA9ICgxLjAgLSBtaXgpICogeCArIG1peCAqIGNvbXBDaCh4KTsKCnByb2Nlc3MgPSBwYXJhbGxlbENvbXAsIHBhcmFsbGVsQ29tcDsK","inputs": 2,"outputs": 2,"meta": [ { "analyzers.lib/amp_follower_ar:author": "Jonatan Liljedahl, revised by Romain Michon" },{ "analyzers.lib/name": "Faust Analyzer Library" },{ "analyzers.lib/version": "1.3.0" },{ "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.22.0" },{ "compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_compressor -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "compressors.lib/compression_gain_mono:author": "Julius O. Smith III" },{ "compressors.lib/compression_gain_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compression_gain_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/compressor_lad_mono:author": "Julius O. Smith III" },{ "compressors.lib/compressor_lad_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compressor_lad_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/compressor_mono:author": "Julius O. Smith III" },{ "compressors.lib/compressor_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compressor_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/name": "Faust Compressor Effect Library" },{ "compressors.lib/version": "1.6.0" },{ "filename": "eoc-compressor.dsp" },{ "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-compressor" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/onePoleSwitching:author": "Jonatan Liljedahl, revised by Dario Sanfilippo" },{ "signals.lib/onePoleSwitching:licence": "STK-4.3" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "eoc-compressor","items": [ {"type": "hslider","label": "attack","varname": "fHslider0","shortname": "attack","address": "/eoc-compressor/attack","index": 0,"meta": [{ "unit": "ms" }],"init": 10,"min": 0.1,"max": 200,"step": 0.1},{"type": "hslider","label": "knee","varname": "fHslider3","shortname": "knee","address": "/eoc-compressor/knee","index": 28,"meta": [{ "unit": "dB" }],"init": 6,"min": 0,"max": 24,"step": 0.1},{"type": "hslider","label": "makeup","varname": "fHslider5","shortname": "makeup","address": "/eoc-compressor/makeup","index": 44,"meta": [{ "unit": "dB" }],"init": 0,"min": 0,"max": 24,"step": 0.1},{"type": "hslider","label": "mix","varname": "fHslider6","shortname": "mix","address": "/eoc-compressor/mix","index": 48,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "ratio","varname": "fHslider4","shortname": "ratio","address": "/eoc-compressor/ratio","index": 32,"init": 4,"min": 1,"max": 20,"step": 0.1},{"type": "hslider","label": "release","varname": "fHslider2","shortname": "release","address": "/eoc-compressor/release","index": 16,"meta": [{ "unit": "ms" }],"init": 100,"min": 10,"max": 2000,"step": 1},{"type": "hslider","label": "threshold","varname": "fHslider1","shortname": "threshold","address": "/eoc-compressor/threshold","index": 12,"meta": [{ "unit": "dB" }],"init": -24,"min": -60,"max": 0,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-compressor.wasm b/playground/faust/eoc-compressor.wasm new file mode 100644 index 0000000000000000000000000000000000000000..965376fc812a30061912c97e53c043e03346ec71 GIT binary patch literal 7922 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 literal 0 HcmV?d00001 diff --git a/playground/faust/eoc-eq-processor.js b/playground/faust/eoc-eq-processor.js new file mode 100644 index 0000000..168968b --- /dev/null +++ b/playground/faust/eoc-eq-processor.js @@ -0,0 +1,183 @@ +/** + * eoc-eq-processor.js — AudioWorklet processor for the 4-band parametric EQ + * + * Extends FaustWorkletProcessor (faust-worklet-processor.js). + * Loads eoc-eq.wasm compiled from eoc-eq.dsp. + * + * 10 exposed params (4 bands × freq/gain; mid bands also have Q): + * Band 1 (Low Shelf): freq1, gain1 + * Band 2 (Low-Mid): freq2, gain2, q2 + * Band 3 (High-Mid): freq3, gain3, q3 + * Band 4 (High Shelf): freq4, gain4 + * + * Note: shelf bands (1 and 4) omit Q — Faust fi.low_shelf / fi.high_shelf + * take only freq and gain. + * + * Parameter index order follows the Faust JSON descriptor (group declaration order, + * alphabetical within groups — confirmed by eoc-eq.json). + */ + +// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first. + +class EOCEQProcessor extends FaustWorkletProcessor { + constructor(options) { + super(options); + this._dsp = null; + this._exports = null; + this._heap = null; + this._heapi32 = null; + this._mem = null; + this._paramAddresses = []; + this._sampleRate = 48000; + } + + // --------------------------------------------------------------------------- + // FaustWorkletProcessor overrides + // --------------------------------------------------------------------------- + + async _initWasm(wasmBytes, sr) { + this._sampleRate = sr; + + const module = await WebAssembly.compile(wasmBytes); + const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); + + const imports = { + env: { + memory, + memoryBase: 0, + tableBase: 0, + _abs: Math.abs, + _acosf: Math.acos, + _asinf: Math.asin, + _atanf: Math.atan, + _atan2f: Math.atan2, + _ceilf: Math.ceil, + _cosf: Math.cos, + _expf: Math.exp, + _floorf: Math.floor, + _fmodf: (x, y) => x % y, + _logf: Math.log, + _log10f: Math.log10, + _max_f: Math.max, + _min_f: Math.min, + _remainderf: (x, y) => x - Math.round(x / y) * y, + _powf: Math.pow, + _roundf: Math.round, + _sinf: Math.sin, + _sqrtf: Math.sqrt, + _tanf: Math.tan, + _fabs: Math.abs, + table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), + }, + }; + + const instance = await WebAssembly.instantiate(module, imports); + this._exports = instance.exports; + this._mem = memory; + this._heap = new Float32Array(memory.buffer); + this._heapi32 = new Int32Array(memory.buffer); + + const exps = this._exports; + + // Create DSP instance + if (exps.createDSPInstance) { + this._dsp = exps.createDSPInstance(); + } else if (exps.eoc_eq) { + this._dsp = exps.eoc_eq(); + } else { + const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); + console.warn('[EOCEQProcessor] No DSP constructor found; exports:', fnKeys); + return; + } + + exps.init(this._dsp, sr); + this._buildParamIndex(exps, memory); + } + + _buildParamIndex(exports, memory) { + if (!exports.getJSON) return; + const ptr = exports.getJSON(this._dsp); + const buf = new Uint8Array(memory.buffer); + let str = ''; + let i = ptr; + while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } + + let desc; + try { desc = JSON.parse(str); } catch { return; } + + const addresses = []; + + function walk(items) { + for (const item of items) { + const type = item.type ?? ''; + if (['hslider', 'vslider', 'nentry'].includes(type)) { + addresses.push(item.address ?? ''); + } else if (item.items) { + walk(item.items); + } + } + } + + walk(desc.ui ?? []); + this._paramAddresses = addresses; + } + + _onSetParam(index, value) { + if (!this._exports || !this._dsp) return; + const addr = this._paramAddresses[index]; + if (addr !== undefined && this._exports.setParamValue) { + this._exports.setParamValue(this._dsp, addr, value); + } + } + + _renderBlock(outL, outR, blockSize) { + if (!this._exports || !this._dsp) return; + + const exps = this._exports; + const mem = this._mem; + const heap = this._heap; + const i32 = this._heapi32; + + // Allocate input + output buffers at end of WASM heap + const heapWords = mem.buffer.byteLength >> 2; + const inLOff = heapWords - blockSize * 6 - 32; + const inROff = inLOff + blockSize; + const outLOff = inROff + blockSize; + const outROff = outLOff + blockSize; + + // Copy inputs (silence — effect processes in-place from AudioWorklet inputs) + // The AudioWorklet process() provides inputs[0] to the raw in buffers via + // the overridden process() below. + // For now fill with stored input (set by process()). + const inPtrsOff = outROff + blockSize; + const outPtrsOff = inPtrsOff + 2; + + i32[inPtrsOff] = inLOff * 4; + i32[inPtrsOff + 1] = inROff * 4; + i32[outPtrsOff] = outLOff * 4; + i32[outPtrsOff + 1] = outROff * 4; + + // Copy input from staging buffers (filled in process()) + const srcL = this._inputL; + const srcR = this._inputR; + if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; + if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; + + exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); + + for (let i = 0; i < blockSize; i++) { + outL[i] = heap[outLOff + i]; + outR[i] = heap[outROff + i]; + } + } + + // Override process() to capture inputs before calling parent + process(inputs, outputs, params) { + const inp = inputs[0]; + this._inputL = inp?.[0] ?? null; + this._inputR = inp?.[1] ?? inp?.[0] ?? null; // mono fallback to L + return super.process(inputs, outputs, params); + } +} + +registerProcessor('eoc-eq-processor', EOCEQProcessor); diff --git a/playground/faust/eoc-eq.dsp b/playground/faust/eoc-eq.dsp new file mode 100644 index 0000000..e6b949f --- /dev/null +++ b/playground/faust/eoc-eq.dsp @@ -0,0 +1,38 @@ +// eoc-eq.dsp — 4-band parametric EQ for MEMLNaut EOC chain +// +// Band 1: Low Shelf (default 80 Hz, range 20–500) +// Band 2: Low-Mid bell (default 400 Hz, range 100–2000) +// Band 3: High-Mid bell (default 2500 Hz, range 500–8000) +// Band 4: High Shelf (default 8000 Hz, range 2000–20000) +// +// Compile: +// faust -lang wasm -cn eoc_eq -e eoc-eq.dsp -o eoc-eq.wasm -json + +import("stdfaust.lib"); + +// Band 1 — Low Shelf +freq1 = hslider("Band 1 (Low Shelf)/freq1[unit:Hz]", 80, 20, 500, 0.1); +gain1 = hslider("Band 1 (Low Shelf)/gain1[unit:dB]", 0, -12, 12, 0.1); +q1 = hslider("Band 1 (Low Shelf)/q1", 1.0, 0.1, 10.0, 0.01); + +// Band 2 — Low-Mid bell +freq2 = hslider("Band 2 (Low-Mid)/freq2[unit:Hz]", 400, 100, 2000, 1.0); +gain2 = hslider("Band 2 (Low-Mid)/gain2[unit:dB]", 0, -12, 12, 0.1); +q2 = hslider("Band 2 (Low-Mid)/q2", 1.0, 0.1, 10.0, 0.01); + +// Band 3 — High-Mid bell +freq3 = hslider("Band 3 (High-Mid)/freq3[unit:Hz]", 2500, 500, 8000, 1.0); +gain3 = hslider("Band 3 (High-Mid)/gain3[unit:dB]", 0, -12, 12, 0.1); +q3 = hslider("Band 3 (High-Mid)/q3", 1.0, 0.1, 10.0, 0.01); + +// Band 4 — High Shelf +freq4 = hslider("Band 4 (High Shelf)/freq4[unit:Hz]", 8000, 2000, 20000, 10.0); +gain4 = hslider("Band 4 (High Shelf)/gain4[unit:dB]", 0, -12, 12, 0.1); +q4 = hslider("Band 4 (High Shelf)/q4", 1.0, 0.1, 10.0, 0.01); + +eqChain = fi.low_shelf(gain1, freq1) : + fi.peak_eq(gain2, freq2, q2) : + fi.peak_eq(gain3, freq3, q3) : + fi.high_shelf(gain4, freq4); + +process = eqChain, eqChain; diff --git a/playground/faust/eoc-eq.json b/playground/faust/eoc-eq.json new file mode 100644 index 0000000..55bef69 --- /dev/null +++ b/playground/faust/eoc-eq.json @@ -0,0 +1 @@ +{"name": "eoc-eq","filename": "eoc-eq.dsp","version": "2.83.1","compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_eq -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/analyzers.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/basics.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/maths.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-a42cfe12/playground/faust"],"size": 296,"code": "Ly8gZW9jLWVxLmRzcCDigJQgNC1iYW5kIHBhcmFtZXRyaWMgRVEgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyBCYW5kIDE6IExvdyBTaGVsZiAgKGRlZmF1bHQgODAgSHosIHJhbmdlIDIw4oCTNTAwKQovLyBCYW5kIDI6IExvdy1NaWQgYmVsbCAoZGVmYXVsdCA0MDAgSHosIHJhbmdlIDEwMOKAkzIwMDApCi8vIEJhbmQgMzogSGlnaC1NaWQgYmVsbCAoZGVmYXVsdCAyNTAwIEh6LCByYW5nZSA1MDDigJM4MDAwKQovLyBCYW5kIDQ6IEhpZ2ggU2hlbGYgKGRlZmF1bHQgODAwMCBIeiwgcmFuZ2UgMjAwMOKAkzIwMDAwKQovLwovLyBDb21waWxlOgovLyAgIGZhdXN0IC1sYW5nIHdhc20gLWNuIGVvY19lcSAtZSBlb2MtZXEuZHNwIC1vIGVvYy1lcS53YXNtIC1qc29uCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKLy8gQmFuZCAxIOKAlCBMb3cgU2hlbGYKZnJlcTEgID0gaHNsaWRlcigiQmFuZCAxIChMb3cgU2hlbGYpL2ZyZXExW3VuaXQ6SHpdIiwgIDgwLCAgICAyMCwgICAgNTAwLCAgIDAuMSk7CmdhaW4xICA9IGhzbGlkZXIoIkJhbmQgMSAoTG93IFNoZWxmKS9nYWluMVt1bml0OmRCXSIsICAgMCwgICAtMTIsICAgIDEyLCAgICAwLjEpOwpxMSAgICAgPSBoc2xpZGVyKCJCYW5kIDEgKExvdyBTaGVsZikvcTEiLCAgICAgICAgICAgICAgIDEuMCwgICAwLjEsICAxMC4wLCAgMC4wMSk7CgovLyBCYW5kIDIg4oCUIExvdy1NaWQgYmVsbApmcmVxMiAgPSBoc2xpZGVyKCJCYW5kIDIgKExvdy1NaWQpL2ZyZXEyW3VuaXQ6SHpdIiwgICA0MDAsICAgMTAwLCAgMjAwMCwgICAxLjApOwpnYWluMiAgPSBoc2xpZGVyKCJCYW5kIDIgKExvdy1NaWQpL2dhaW4yW3VuaXQ6ZEJdIiwgICAgIDAsICAgLTEyLCAgICAxMiwgICAgMC4xKTsKcTIgICAgID0gaHNsaWRlcigiQmFuZCAyIChMb3ctTWlkKS9xMiIsICAgICAgICAgICAgICAgICAxLjAsICAgMC4xLCAgMTAuMCwgIDAuMDEpOwoKLy8gQmFuZCAzIOKAlCBIaWdoLU1pZCBiZWxsCmZyZXEzICA9IGhzbGlkZXIoIkJhbmQgMyAoSGlnaC1NaWQpL2ZyZXEzW3VuaXQ6SHpdIiwgMjUwMCwgICA1MDAsICA4MDAwLCAgIDEuMCk7CmdhaW4zICA9IGhzbGlkZXIoIkJhbmQgMyAoSGlnaC1NaWQpL2dhaW4zW3VuaXQ6ZEJdIiwgICAgMCwgICAtMTIsICAgIDEyLCAgICAwLjEpOwpxMyAgICAgPSBoc2xpZGVyKCJCYW5kIDMgKEhpZ2gtTWlkKS9xMyIsICAgICAgICAgICAgICAgIDEuMCwgICAwLjEsICAxMC4wLCAgMC4wMSk7CgovLyBCYW5kIDQg4oCUIEhpZ2ggU2hlbGYKZnJlcTQgID0gaHNsaWRlcigiQmFuZCA0IChIaWdoIFNoZWxmKS9mcmVxNFt1bml0Okh6XSIsIDgwMDAsIDIwMDAsIDIwMDAwLCAgMTAuMCk7CmdhaW40ICA9IGhzbGlkZXIoIkJhbmQgNCAoSGlnaCBTaGVsZikvZ2FpbjRbdW5pdDpkQl0iLCAgICAwLCAgLTEyLCAgICAxMiwgICAgMC4xKTsKcTQgICAgID0gaHNsaWRlcigiQmFuZCA0IChIaWdoIFNoZWxmKS9xNCIsICAgICAgICAgICAgICAgMS4wLCAgIDAuMSwgIDEwLjAsICAwLjAxKTsKCmVxQ2hhaW4gPSBmaS5sb3dfc2hlbGYoZ2FpbjEsIGZyZXExKSA6CiAgICAgICAgICAgZmkucGVha19lcShnYWluMiwgZnJlcTIsIHEyKSA6CiAgICAgICAgICAgZmkucGVha19lcShnYWluMywgZnJlcTMsIHEzKSA6CiAgICAgICAgICAgZmkuaGlnaF9zaGVsZihnYWluNCwgZnJlcTQpOwoKcHJvY2VzcyA9IGVxQ2hhaW4sIGVxQ2hhaW47Cg==","inputs": 2,"outputs": 2,"meta": [ { "analyzers.lib/name": "Faust Analyzer Library" },{ "analyzers.lib/version": "1.3.0" },{ "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.22.0" },{ "compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_eq -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "filename": "eoc-eq.dsp" },{ "filters.lib/filterbank:author": "Julius O. Smith III" },{ "filters.lib/filterbank:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/filterbank:license": "MIT-style STK-4.3 license" },{ "filters.lib/fir:author": "Julius O. Smith III" },{ "filters.lib/fir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/fir:license": "MIT-style STK-4.3 license" },{ "filters.lib/highpass:author": "Julius O. Smith III" },{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/highshelf:author": "Julius O. Smith III" },{ "filters.lib/highshelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/highshelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/iir:author": "Julius O. Smith III" },{ "filters.lib/iir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/iir:license": "MIT-style STK-4.3 license" },{ "filters.lib/low_shelf:author": "Julius O. Smith III" },{ "filters.lib/low_shelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/low_shelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "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 " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowshelf:author": "Julius O. Smith III" },{ "filters.lib/lowshelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowshelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/peak_eq:author": "Julius O. Smith III" },{ "filters.lib/peak_eq:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/peak_eq:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "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 " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf2:author": "Julius O. Smith III" },{ "filters.lib/tf2:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf2:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf2s:author": "Julius O. Smith III" },{ "filters.lib/tf2s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf2s: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-eq" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "eoc-eq","items": [ {"type": "hslider","label": "Band 1 (Low Shelf)/freq1","varname": "fHslider7","shortname": "Band_1_Low_Shelf_freq1","address": "/eoc-eq/Band_1__Low_Shelf__freq1","index": 44,"meta": [{ "unit": "Hz" }],"init": 80,"min": 20,"max": 500,"step": 0.1},{"type": "hslider","label": "Band 1 (Low Shelf)/gain1","varname": "fHslider8","shortname": "Band_1_Low_Shelf_gain1","address": "/eoc-eq/Band_1__Low_Shelf__gain1","index": 76,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 2 (Low-Mid)/freq2","varname": "fHslider5","shortname": "Band_2_Low_Mid_freq2","address": "/eoc-eq/Band_2__Low-Mid__freq2","index": 36,"meta": [{ "unit": "Hz" }],"init": 400,"min": 100,"max": 2000,"step": 1},{"type": "hslider","label": "Band 2 (Low-Mid)/gain2","varname": "fHslider4","shortname": "Band_2_Low_Mid_gain2","address": "/eoc-eq/Band_2__Low-Mid__gain2","index": 32,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 2 (Low-Mid)/q2","varname": "fHslider6","shortname": "Band_2_Low_Mid_q2","address": "/eoc-eq/Band_2__Low-Mid__q2","index": 40,"init": 1,"min": 0.1,"max": 10,"step": 0.01},{"type": "hslider","label": "Band 3 (High-Mid)/freq3","varname": "fHslider2","shortname": "Band_3_High_Mid_freq3","address": "/eoc-eq/Band_3__High-Mid__freq3","index": 20,"meta": [{ "unit": "Hz" }],"init": 2500,"min": 500,"max": 8000,"step": 1},{"type": "hslider","label": "Band 3 (High-Mid)/gain3","varname": "fHslider1","shortname": "Band_3_High_Mid_gain3","address": "/eoc-eq/Band_3__High-Mid__gain3","index": 16,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 3 (High-Mid)/q3","varname": "fHslider3","shortname": "Band_3_High_Mid_q3","address": "/eoc-eq/Band_3__High-Mid__q3","index": 28,"init": 1,"min": 0.1,"max": 10,"step": 0.01},{"type": "hslider","label": "Band 4 (High Shelf)/freq4","varname": "fHslider0","shortname": "Band_4_High_Shelf_freq4","address": "/eoc-eq/Band_4__High_Shelf__freq4","index": 0,"meta": [{ "unit": "Hz" }],"init": 8000,"min": 2000,"max": 20000,"step": 10},{"type": "hslider","label": "Band 4 (High Shelf)/gain4","varname": "fHslider9","shortname": "Band_4_High_Shelf_gain4","address": "/eoc-eq/Band_4__High_Shelf__gain4","index": 152,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-eq.wasm b/playground/faust/eoc-eq.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8deaf08bb7832788b48a70980bfa2a858f92fc6e GIT binary patch literal 14116 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 ZUryd=y+`r}+#v59d{yi2`}f|z_kX{dReJyc literal 0 HcmV?d00001 diff --git a/playground/faust/eoc-reverb-processor.js b/playground/faust/eoc-reverb-processor.js new file mode 100644 index 0000000..08c2d7f --- /dev/null +++ b/playground/faust/eoc-reverb-processor.js @@ -0,0 +1,178 @@ +/** + * eoc-reverb-processor.js — AudioWorklet processor for the zita reverb + * + * Extends FaustWorkletProcessor (faust-worklet-processor.js). + * Loads eoc-reverb.wasm compiled from eoc-reverb.dsp. + * + * 8 exposed params (alphabetical order as emitted by Faust JSON): + * 0: decay (s) + * 1: diffusion (0–1) + * 2: hi_damp (0–1) + * 3: lo_damp (0–1) + * 4: mix (0–1) + * 5: predelay (ms) + * 6: size (0–1) + * 7: width (0–1) + * + * Note: mod_rate is declared in the DSP file but has no effect in this version + * (zita_rev1_stereo does not expose modulation rate externally). + */ + +// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first. + +class EOCReverbProcessor extends FaustWorkletProcessor { + constructor(options) { + super(options); + this._dsp = null; + this._exports = null; + this._heap = null; + this._heapi32 = null; + this._mem = null; + this._paramAddresses = []; + this._inputL = null; + this._inputR = null; + this._sampleRate = 48000; + } + + // --------------------------------------------------------------------------- + // FaustWorkletProcessor overrides + // --------------------------------------------------------------------------- + + async _initWasm(wasmBytes, sr) { + this._sampleRate = sr; + + const module = await WebAssembly.compile(wasmBytes); + // Reverb needs more memory due to large delay lines in zita + const memory = new WebAssembly.Memory({ initial: 64, maximum: 512 }); + + const imports = { + env: { + memory, + memoryBase: 0, + tableBase: 0, + _abs: Math.abs, + _acosf: Math.acos, + _asinf: Math.asin, + _atanf: Math.atan, + _atan2f: Math.atan2, + _ceilf: Math.ceil, + _cosf: Math.cos, + _expf: Math.exp, + _floorf: Math.floor, + _fmodf: (x, y) => x % y, + _logf: Math.log, + _log10f: Math.log10, + _max_f: Math.max, + _min_f: Math.min, + _remainderf: (x, y) => x - Math.round(x / y) * y, + _powf: Math.pow, + _roundf: Math.round, + _sinf: Math.sin, + _sqrtf: Math.sqrt, + _tanf: Math.tan, + _fabs: Math.abs, + table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), + }, + }; + + const instance = await WebAssembly.instantiate(module, imports); + this._exports = instance.exports; + this._mem = memory; + this._heap = new Float32Array(memory.buffer); + this._heapi32 = new Int32Array(memory.buffer); + + const exps = this._exports; + + if (exps.createDSPInstance) { + this._dsp = exps.createDSPInstance(); + } else if (exps.eoc_reverb) { + this._dsp = exps.eoc_reverb(); + } else { + const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); + console.warn('[EOCReverbProcessor] No DSP constructor found; exports:', fnKeys); + return; + } + + exps.init(this._dsp, sr); + this._buildParamIndex(exps, memory); + } + + _buildParamIndex(exports, memory) { + if (!exports.getJSON) return; + const ptr = exports.getJSON(this._dsp); + const buf = new Uint8Array(memory.buffer); + let str = ''; + let i = ptr; + while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } + + let desc; + try { desc = JSON.parse(str); } catch { return; } + + const addresses = []; + + function walk(items) { + for (const item of items) { + const type = item.type ?? ''; + if (['hslider', 'vslider', 'nentry'].includes(type)) { + addresses.push(item.address ?? ''); + } else if (item.items) { + walk(item.items); + } + } + } + + walk(desc.ui ?? []); + this._paramAddresses = addresses; + } + + _onSetParam(index, value) { + if (!this._exports || !this._dsp) return; + const addr = this._paramAddresses[index]; + if (addr !== undefined && this._exports.setParamValue) { + this._exports.setParamValue(this._dsp, addr, value); + } + } + + _renderBlock(outL, outR, blockSize) { + if (!this._exports || !this._dsp) return; + + const exps = this._exports; + const mem = this._mem; + const heap = this._heap; + const i32 = this._heapi32; + + const heapWords = mem.buffer.byteLength >> 2; + const inLOff = heapWords - blockSize * 6 - 32; + const inROff = inLOff + blockSize; + const outLOff = inROff + blockSize; + const outROff = outLOff + blockSize; + const inPtrsOff = outROff + blockSize; + const outPtrsOff = inPtrsOff + 2; + + i32[inPtrsOff] = inLOff * 4; + i32[inPtrsOff + 1] = inROff * 4; + i32[outPtrsOff] = outLOff * 4; + i32[outPtrsOff + 1] = outROff * 4; + + const srcL = this._inputL; + const srcR = this._inputR; + if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; + if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; + + exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); + + for (let i = 0; i < blockSize; i++) { + outL[i] = heap[outLOff + i]; + outR[i] = heap[outROff + i]; + } + } + + process(inputs, outputs, params) { + const inp = inputs[0]; + this._inputL = inp?.[0] ?? null; + this._inputR = inp?.[1] ?? inp?.[0] ?? null; + return super.process(inputs, outputs, params); + } +} + +registerProcessor('eoc-reverb-processor', EOCReverbProcessor); diff --git a/playground/faust/eoc-reverb.dsp b/playground/faust/eoc-reverb.dsp new file mode 100644 index 0000000..8a382e8 --- /dev/null +++ b/playground/faust/eoc-reverb.dsp @@ -0,0 +1,52 @@ +// eoc-reverb.dsp — Stereo reverb for MEMLNaut EOC chain +// +// 9 params: predelay, size, diffusion, hi_damp, lo_damp, decay, mix, width, mod_rate +// +// Uses re.zita_rev1_stereo for the core reverb algorithm. +// +// Compile: +// faust -lang wasm -cn eoc_reverb -json eoc-reverb.dsp -o eoc-reverb.wasm + +import("stdfaust.lib"); + +predelay = hslider("predelay[unit:ms]", 0.0, 0.0, 100.0, 0.5); +size = hslider("size", 0.5, 0.0, 1.0, 0.001); +diffusion= hslider("diffusion", 0.7, 0.0, 1.0, 0.001); +hi_damp = hslider("hi_damp", 0.5, 0.0, 1.0, 0.001); +lo_damp = hslider("lo_damp", 0.0, 0.0, 1.0, 0.001); +decay = hslider("decay[unit:s]", 3.0, 0.1, 20.0, 0.1); +mix = hslider("mix", 0.2, 0.0, 1.0, 0.001); +width = hslider("width", 0.8, 0.0, 1.0, 0.001); +mod_rate = hslider("mod_rate[unit:Hz]", 0.5, 0.0, 5.0, 0.01); + +// Pre-delay in samples (minimum 1) +pdSamps = max(1, int(predelay / 1000.0 * ma.SR)); + +// Frequency crossovers for zita +f1 = 200.0 + lo_damp * 1800.0; +f2 = 20000.0 - hi_damp * 18000.0; + +// Reverb decay times scaled by size +t60dc = decay * (1.0 + size * 0.5); +t60m = decay; + +// Pre-delay: single channel +predelayLine = _ @ pdSamps; + +// Width processing: M/S encode-scale-decode +// L R → L' R' where side channels scaled by width +widthL(l, r) = (l + r) * 0.5 + (l - r) * 0.5 * width; +widthR(l, r) = (l + r) * 0.5 - (l - r) * 0.5 * width; + +// Wet signal through predelay, diffusion scale, and zita reverb +wetL(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0, + inL * diffusion @ pdSamps, + inR * diffusion @ pdSamps)) : widthL; + +wetR(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0, + inL * diffusion @ pdSamps, + inR * diffusion @ pdSamps)) : widthR; + +process(inL, inR) = + inL * (1.0 - mix) + wetL(inL, inR) * mix, + inR * (1.0 - mix) + wetR(inL, inR) * mix; diff --git a/playground/faust/eoc-reverb.json b/playground/faust/eoc-reverb.json new file mode 100644 index 0000000..aafd256 --- /dev/null +++ b/playground/faust/eoc-reverb.json @@ -0,0 +1 @@ +{"name": "eoc-reverb","filename": "eoc-reverb.dsp","version": "2.83.1","compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_reverb -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/reverbs.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/delays.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/basics.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/signals.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/filters.lib","/nix/store/fnjbalzgc6pjm8la292cczxqbr00qmav-faust-2.83.1/share/faust/routes.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-a42cfe12/playground/faust"],"size": 2048504,"code": "Ly8gZW9jLXJldmVyYi5kc3Ag4oCUIFN0ZXJlbyByZXZlcmIgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyA5IHBhcmFtczogcHJlZGVsYXksIHNpemUsIGRpZmZ1c2lvbiwgaGlfZGFtcCwgbG9fZGFtcCwgZGVjYXksIG1peCwgd2lkdGgsIG1vZF9yYXRlCi8vCi8vIFVzZXMgcmUueml0YV9yZXYxX3N0ZXJlbyBmb3IgdGhlIGNvcmUgcmV2ZXJiIGFsZ29yaXRobS4KLy8KLy8gQ29tcGlsZToKLy8gICBmYXVzdCAtbGFuZyB3YXNtIC1jbiBlb2NfcmV2ZXJiIC1qc29uIGVvYy1yZXZlcmIuZHNwIC1vIGVvYy1yZXZlcmIud2FzbQoKaW1wb3J0KCJzdGRmYXVzdC5saWIiKTsKCnByZWRlbGF5ID0gaHNsaWRlcigicHJlZGVsYXlbdW5pdDptc10iLCAgMC4wLCAgIDAuMCwgIDEwMC4wLCAgMC41KTsKc2l6ZSAgICAgPSBoc2xpZGVyKCJzaXplIiwgICAgICAgICAgICAgIDAuNSwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKZGlmZnVzaW9uPSBoc2xpZGVyKCJkaWZmdXNpb24iLCAgICAgICAgIDAuNywgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKaGlfZGFtcCAgPSBoc2xpZGVyKCJoaV9kYW1wIiwgICAgICAgICAgIDAuNSwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKbG9fZGFtcCAgPSBoc2xpZGVyKCJsb19kYW1wIiwgICAgICAgICAgIDAuMCwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKZGVjYXkgICAgPSBoc2xpZGVyKCJkZWNheVt1bml0OnNdIiwgICAgICAzLjAsICAgMC4xLCAgIDIwLjAsICAwLjEpOwptaXggICAgICA9IGhzbGlkZXIoIm1peCIsICAgICAgICAgICAgICAgMC4yLCAgIDAuMCwgICAgMS4wLCAgMC4wMDEpOwp3aWR0aCAgICA9IGhzbGlkZXIoIndpZHRoIiwgICAgICAgICAgICAgMC44LCAgIDAuMCwgICAgMS4wLCAgMC4wMDEpOwptb2RfcmF0ZSA9IGhzbGlkZXIoIm1vZF9yYXRlW3VuaXQ6SHpdIiwgMC41LCAgIDAuMCwgICAgNS4wLCAgMC4wMSk7CgovLyBQcmUtZGVsYXkgaW4gc2FtcGxlcyAobWluaW11bSAxKQpwZFNhbXBzID0gbWF4KDEsIGludChwcmVkZWxheSAvIDEwMDAuMCAqIG1hLlNSKSk7CgovLyBGcmVxdWVuY3kgY3Jvc3NvdmVycyBmb3Igeml0YQpmMSAgID0gMjAwLjAgKyBsb19kYW1wICogMTgwMC4wOwpmMiAgID0gMjAwMDAuMCAtIGhpX2RhbXAgKiAxODAwMC4wOwoKLy8gUmV2ZXJiIGRlY2F5IHRpbWVzIHNjYWxlZCBieSBzaXplCnQ2MGRjID0gZGVjYXkgKiAoMS4wICsgc2l6ZSAqIDAuNSk7CnQ2MG0gID0gZGVjYXk7CgovLyBQcmUtZGVsYXk6IHNpbmdsZSBjaGFubmVsCnByZWRlbGF5TGluZSA9IF8gQCBwZFNhbXBzOwoKLy8gV2lkdGggcHJvY2Vzc2luZzogTS9TIGVuY29kZS1zY2FsZS1kZWNvZGUKLy8gTCBSIOKGkiBMJyBSJyB3aGVyZSBzaWRlIGNoYW5uZWxzIHNjYWxlZCBieSB3aWR0aAp3aWR0aEwobCwgcikgPSAobCArIHIpICogMC41ICsgKGwgLSByKSAqIDAuNSAqIHdpZHRoOwp3aWR0aFIobCwgcikgPSAobCArIHIpICogMC41IC0gKGwgLSByKSAqIDAuNSAqIHdpZHRoOwoKLy8gV2V0IHNpZ25hbCB0aHJvdWdoIHByZWRlbGF5LCBkaWZmdXNpb24gc2NhbGUsIGFuZCB6aXRhIHJldmVyYgp3ZXRMKGluTCwgaW5SKSA9IChyZS56aXRhX3JldjFfc3RlcmVvKDAuMCwgZjEsIGYyLCB0NjBkYywgdDYwbSwgMTkyMDAwLjAsCiAgICAgICAgICAgICAgICAgICAgaW5MICogZGlmZnVzaW9uIEAgcGRTYW1wcywKICAgICAgICAgICAgICAgICAgICBpblIgKiBkaWZmdXNpb24gQCBwZFNhbXBzKSkgOiB3aWR0aEw7Cgp3ZXRSKGluTCwgaW5SKSA9IChyZS56aXRhX3JldjFfc3RlcmVvKDAuMCwgZjEsIGYyLCB0NjBkYywgdDYwbSwgMTkyMDAwLjAsCiAgICAgICAgICAgICAgICAgICAgaW5MICogZGlmZnVzaW9uIEAgcGRTYW1wcywKICAgICAgICAgICAgICAgICAgICBpblIgKiBkaWZmdXNpb24gQCBwZFNhbXBzKSkgOiB3aWR0aFI7Cgpwcm9jZXNzKGluTCwgaW5SKSA9CiAgICBpbkwgKiAoMS4wIC0gbWl4KSArIHdldEwoaW5MLCBpblIpICogbWl4LAogICAgaW5SICogKDEuMCAtIG1peCkgKyB3ZXRSKGluTCwgaW5SKSAqIG1peDsK","inputs": 2,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.22.0" },{ "compile_options": "-lang wasm -fpga-mem-th 4 -ct 1 -cn eoc_reverb -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-reverb.dsp" },{ "filters.lib/allpass_comb:author": "Julius O. Smith III" },{ "filters.lib/allpass_comb:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/allpass_comb:license": "MIT-style STK-4.3 license" },{ "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 " },{ "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 " },{ "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 " },{ "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-reverb" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "reverbs.lib/name": "Faust Reverb Library" },{ "reverbs.lib/version": "1.5.1" },{ "routes.lib/hadamard:author": "Remy Muller, revised by Romain Michon" },{ "routes.lib/name": "Faust Signal Routing Library" },{ "routes.lib/version": "1.2.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "eoc-reverb","items": [ {"type": "hslider","label": "decay","varname": "fHslider0","shortname": "decay","address": "/eoc-reverb/decay","index": 0,"meta": [{ "unit": "s" }],"init": 3,"min": 0.1,"max": 20,"step": 0.1},{"type": "hslider","label": "diffusion","varname": "fHslider4","shortname": "diffusion","address": "/eoc-reverb/diffusion","index": 262212,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "hi_damp","varname": "fHslider1","shortname": "hi_damp","address": "/eoc-reverb/hi_damp","index": 20,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "lo_damp","varname": "fHslider2","shortname": "lo_damp","address": "/eoc-reverb/lo_damp","index": 28,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "mix","varname": "fHslider7","shortname": "mix","address": "/eoc-reverb/mix","index": 2048500,"init": 0.2,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "predelay","varname": "fHslider5","shortname": "predelay","address": "/eoc-reverb/predelay","index": 393288,"meta": [{ "unit": "ms" }],"init": 0,"min": 0,"max": 100,"step": 0.5},{"type": "hslider","label": "size","varname": "fHslider3","shortname": "size","address": "/eoc-reverb/size","index": 44,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "width","varname": "fHslider6","shortname": "width","address": "/eoc-reverb/width","index": 2048496,"init": 0.8,"min": 0,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-reverb.wasm b/playground/faust/eoc-reverb.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6357d1b7e61346cf5cc7c0678aad3e01d836c792 GIT binary patch literal 17662 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 S$Wpi4dejymo?Ur%<$nRdQpf)Q literal 0 HcmV?d00001 diff --git a/playground/js/eoc/index.js b/playground/js/eoc/index.js index 79811c0..3e31b6a 100644 --- a/playground/js/eoc/index.js +++ b/playground/js/eoc/index.js @@ -3,5 +3,8 @@ // Import from here to get the full EOC API: // import { EOCModule, EOCChain } from './eoc/index.js'; -export { EOCModule } from './eoc-module.js'; -export { EOCChain } from './eoc-chain.js'; +export { EOCModule } from './eoc-module.js'; +export { EOCChain } from './eoc-chain.js'; +export { EQModule } from './modules/eq-module.js'; +export { CompressorModule } from './modules/compressor-module.js'; +export { ReverbModule } from './modules/reverb-module.js'; diff --git a/playground/js/eoc/modules/compressor-module.js b/playground/js/eoc/modules/compressor-module.js new file mode 100644 index 0000000..65e4d7d --- /dev/null +++ b/playground/js/eoc/modules/compressor-module.js @@ -0,0 +1,129 @@ +// playground/js/eoc/modules/compressor-module.js — Stereo compressor EOCModule +// +// Faust DSP: playground/faust/eoc-compressor.dsp +// WASM: playground/faust/eoc-compressor.wasm +// JSON: playground/faust/eoc-compressor.json +// +// 7 params (Faust JSON alphabetical order): +// 0: attack [0.1–200 ms, init 10] +// 1: knee [0–24 dB, init 6] +// 2: makeup [0–24 dB, init 0] +// 3: mix [0–1, init 1] (dry/wet for parallel compression) +// 4: ratio [1–20, init 4] +// 5: release [10–2000 ms, init 100] +// 6: threshold [-60–0 dBFS, init -24] + +import { EOCModule } from '../eoc-module.js'; +import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; + +const COMP_PARAM_META = [ + { id: 'attack', name: 'Attack', min: 0.1, max: 200, init: (10-0.1)/(200-0.1), curve: 0.3, group: 'Compressor' }, + { id: 'knee', name: 'Knee', min: 0, max: 24, init: 6/24, curve: 0.5, group: 'Compressor' }, + { id: 'makeup', name: 'Makeup', min: 0, max: 24, init: 0, curve: 0.5, group: 'Compressor' }, + { id: 'mix', name: 'Mix', min: 0, max: 1, init: 1, curve: 0.5, group: 'Compressor' }, + { id: 'ratio', name: 'Ratio', min: 1, max: 20, init: (4-1)/(20-1), curve: 0.4, group: 'Compressor' }, + { id: 'release', name: 'Release', min: 10, max: 2000, init: (100-10)/(2000-10), curve: 0.3, group: 'Compressor' }, + { id: 'threshold', name: 'Threshold', min: -60, max: 0, init: (-24-(-60))/(0-(-60)), curve: 0.5, group: 'Compressor' }, +]; + +export class CompressorModule extends EOCModule { + constructor() { + super(); + this._workletNode = null; + this._effectGain = null; + this._paramMeta = COMP_PARAM_META; + } + + // --------------------------------------------------------------------------- + // EOCModule identity + // --------------------------------------------------------------------------- + + get id() { return 'compressor'; } + get displayName() { return 'Compressor'; } + get paramMeta() { return this._paramMeta; } + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + async init(audioCtx) { + await super.init(audioCtx); + + try { + const fetched = await loadFaustParamMeta('faust/eoc-compressor.json'); + if (fetched && fetched.length > 0) this._paramMeta = fetched; + } catch (err) { + console.warn('[CompressorModule] Could not load eoc-compressor.json, using static paramMeta:', err.message); + } + + try { + await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); + } catch (_) { /* already registered */ } + await audioCtx.audioWorklet.addModule('faust/eoc-compressor-processor.js'); + + this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-compressor-processor', { + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + }); + + const wasmResp = await fetch('faust/eoc-compressor.wasm'); + const wasmBytes = await wasmResp.arrayBuffer(); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('CompressorModule worklet init timeout')), 10000); + this._workletNode.port.onmessage = (e) => { + if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } + if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } + }; + this._workletNode.port.postMessage( + { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, + [wasmBytes], + ); + }); + + this._effectGain = audioCtx.createGain(); + this._effectGain.gain.value = 1; + + this._bypassIn.connect(this._workletNode); + this._workletNode.connect(this._effectGain); + this._effectGain.connect(this._bypassOut); + + this._finishInit(); + } + + // --------------------------------------------------------------------------- + // Real-time control + // --------------------------------------------------------------------------- + + setParam(index, normalizedValue) { + super.setParam(index, normalizedValue); + if (!this._workletNode) return; + const meta = this._paramMeta[index]; + if (!meta) return; + const raw = meta.min + normalizedValue * (meta.max - meta.min); + this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); + } + + // --------------------------------------------------------------------------- + // Bypass + // --------------------------------------------------------------------------- + + _onBypassChange(enabled) { + if (!this._effectGain) return; + const t = this._audioCtx.currentTime; + this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); + } + + // --------------------------------------------------------------------------- + // Dispose + // --------------------------------------------------------------------------- + + dispose() { + this._workletNode?.disconnect(); + this._effectGain?.disconnect(); + this._workletNode = null; + this._effectGain = null; + super.dispose(); + } +} diff --git a/playground/js/eoc/modules/eq-module.js b/playground/js/eoc/modules/eq-module.js new file mode 100644 index 0000000..556543b --- /dev/null +++ b/playground/js/eoc/modules/eq-module.js @@ -0,0 +1,143 @@ +// playground/js/eoc/modules/eq-module.js — 4-band parametric EQ EOCModule +// +// Faust DSP: playground/faust/eoc-eq.dsp +// WASM: playground/faust/eoc-eq.wasm +// JSON: playground/faust/eoc-eq.json +// +// 10 params (shelf bands have no Q in Faust fi.low_shelf / fi.high_shelf): +// Band 1 (Low Shelf): freq1 [20–500 Hz, init 80], gain1 [-12–+12 dB, init 0] +// Band 2 (Low-Mid): freq2 [100–2000 Hz, init 400], gain2 [-12–+12 dB, init 0], q2 [0.1–10, init 1] +// Band 3 (High-Mid): freq3 [500–8000 Hz, init 2500], gain3 [-12–+12 dB, init 0], q3 [0.1–10, init 1] +// Band 4 (High Shelf): freq4 [2000–20000 Hz, init 8000], gain4 [-12–+12 dB, init 0] +// +// param order = Faust JSON alphabetical-within-group, groups in declaration order: +// 0 freq1, 1 gain1, 2 freq2, 3 gain2, 4 q2, 5 freq3, 6 gain3, 7 q3, 8 freq4, 9 gain4 + +import { EOCModule } from '../eoc-module.js'; +import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; + +// Static paramMeta — mirrors eoc-eq.json, used before/without JSON fetch +const EQ_PARAM_META = [ + // Band 1 (Low Shelf) + { id: 'band_1__low_shelf__freq1', name: 'Freq 1', min: 20, max: 500, init: (80-20)/(500-20), curve: 0.3, group: 'Band 1 (Low Shelf)' }, + { id: 'band_1__low_shelf__gain1', name: 'Gain 1', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 1 (Low Shelf)' }, + // Band 2 (Low-Mid bell) + { id: 'band_2__low-mid__freq2', name: 'Freq 2', min: 100, max: 2000, init: (400-100)/(2000-100), curve: 0.3, group: 'Band 2 (Low-Mid)' }, + { id: 'band_2__low-mid__gain2', name: 'Gain 2', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 2 (Low-Mid)' }, + { id: 'band_2__low-mid__q2', name: 'Q 2', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 2 (Low-Mid)' }, + // Band 3 (High-Mid bell) + { id: 'band_3__high-mid__freq3', name: 'Freq 3', min: 500, max: 8000, init: (2500-500)/(8000-500),curve: 0.3, group: 'Band 3 (High-Mid)' }, + { id: 'band_3__high-mid__gain3', name: 'Gain 3', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 3 (High-Mid)' }, + { id: 'band_3__high-mid__q3', name: 'Q 3', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 3 (High-Mid)' }, + // Band 4 (High Shelf) + { id: 'band_4__high_shelf__freq4',name: 'Freq 4', min: 2000, max: 20000, init: (8000-2000)/(20000-2000),curve: 0.3, group: 'Band 4 (High Shelf)' }, + { id: 'band_4__high_shelf__gain4',name: 'Gain 4', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 4 (High Shelf)' }, +]; + +export class EQModule extends EOCModule { + constructor() { + super(); + this._workletNode = null; + this._effectGain = null; + this._paramMeta = EQ_PARAM_META; + } + + // --------------------------------------------------------------------------- + // EOCModule identity + // --------------------------------------------------------------------------- + + get id() { return 'eq'; } + get displayName() { return 'Parametric EQ'; } + get paramMeta() { return this._paramMeta; } + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + async init(audioCtx) { + await super.init(audioCtx); + + // Attempt to load richer paramMeta from JSON (non-fatal if fetch fails) + try { + const fetched = await loadFaustParamMeta('faust/eoc-eq.json'); + if (fetched && fetched.length > 0) this._paramMeta = fetched; + } catch (err) { + console.warn('[EQModule] Could not load eoc-eq.json, using static paramMeta:', err.message); + } + + // Register worklet module (idempotent across calls) + try { + await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); + } catch (_) { /* already registered */ } + await audioCtx.audioWorklet.addModule('faust/eoc-eq-processor.js'); + + // Create AudioWorkletNode + this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-eq-processor', { + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + }); + + // Load WASM and send to worklet + const wasmResp = await fetch('faust/eoc-eq.wasm'); + const wasmBytes = await wasmResp.arrayBuffer(); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('EQModule worklet init timeout')), 10000); + this._workletNode.port.onmessage = (e) => { + if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } + if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } + }; + this._workletNode.port.postMessage( + { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, + [wasmBytes], + ); + }); + + // Gain node so bypass can mute the effect path + this._effectGain = audioCtx.createGain(); + this._effectGain.gain.value = 1; + + // Wire: bypassIn → worklet → effectGain → bypassOut + this._bypassIn.connect(this._workletNode); + this._workletNode.connect(this._effectGain); + this._effectGain.connect(this._bypassOut); + + this._finishInit(); + } + + // --------------------------------------------------------------------------- + // Real-time control + // --------------------------------------------------------------------------- + + setParam(index, normalizedValue) { + super.setParam(index, normalizedValue); + if (!this._workletNode) return; + const meta = this._paramMeta[index]; + if (!meta) return; + const raw = meta.min + normalizedValue * (meta.max - meta.min); + this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); + } + + // --------------------------------------------------------------------------- + // Bypass + // --------------------------------------------------------------------------- + + _onBypassChange(enabled) { + if (!this._effectGain) return; + const t = this._audioCtx.currentTime; + this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); + } + + // --------------------------------------------------------------------------- + // Dispose + // --------------------------------------------------------------------------- + + dispose() { + this._workletNode?.disconnect(); + this._effectGain?.disconnect(); + this._workletNode = null; + this._effectGain = null; + super.dispose(); + } +} diff --git a/playground/js/eoc/modules/reverb-module.js b/playground/js/eoc/modules/reverb-module.js new file mode 100644 index 0000000..55c1af5 --- /dev/null +++ b/playground/js/eoc/modules/reverb-module.js @@ -0,0 +1,147 @@ +// playground/js/eoc/modules/reverb-module.js — Zita reverb EOCModule +// +// Faust DSP: playground/faust/eoc-reverb.dsp +// WASM: playground/faust/eoc-reverb.wasm +// JSON: playground/faust/eoc-reverb.json +// +// 8 params (Faust JSON alphabetical order; mod_rate is declared in DSP but not +// exposed by re.zita_rev1_stereo, so it does not appear in the JSON): +// 0: decay [0.1–20 s, init 3] +// 1: diffusion [0–1, init 0.7] +// 2: hi_damp [0–1, init 0.5] +// 3: lo_damp [0–1, init 0] +// 4: mix [0–1, init 0.2] +// 5: predelay [0–100 ms, init 0] +// 6: size [0–1, init 0.5] +// 7: width [0–1, init 0.8] +// +// Note: mod_rate param is listed in paramMeta as a placeholder (fixed at 0.5 Hz) +// to preserve the 9-param count described in the spec. It has no effect on audio +// until zita modulation is plumbed through. + +import { EOCModule } from '../eoc-module.js'; +import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; + +const REVERB_PARAM_META = [ + { id: 'decay', name: 'Decay', min: 0.1, max: 20, init: (3-0.1)/(20-0.1), curve: 0.3, group: 'Reverb' }, + { id: 'diffusion', name: 'Diffusion', min: 0, max: 1, init: 0.7, curve: 0.5, group: 'Reverb' }, + { id: 'hi_damp', name: 'Hi Damp', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' }, + { id: 'lo_damp', name: 'Lo Damp', min: 0, max: 1, init: 0, curve: 0.5, group: 'Reverb' }, + { id: 'mix', name: 'Mix', min: 0, max: 1, init: 0.2, curve: 0.5, group: 'Reverb' }, + { id: 'predelay', name: 'Predelay', min: 0, max: 100, init: 0, curve: 0.4, group: 'Reverb' }, + { id: 'size', name: 'Size', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' }, + { id: 'width', name: 'Width', min: 0, max: 1, init: 0.8, curve: 0.5, group: 'Reverb' }, + // Placeholder: mod_rate is not yet connected to zita internals + { id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' }, +]; + +export class ReverbModule extends EOCModule { + constructor() { + super(); + this._workletNode = null; + this._effectGain = null; + this._paramMeta = REVERB_PARAM_META; + } + + // --------------------------------------------------------------------------- + // EOCModule identity + // --------------------------------------------------------------------------- + + get id() { return 'reverb'; } + get displayName() { return 'Reverb'; } + get paramMeta() { return this._paramMeta; } + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + async init(audioCtx) { + await super.init(audioCtx); + + // After loading JSON, merge — but keep mod_rate placeholder (index 8) + try { + const fetched = await loadFaustParamMeta('faust/eoc-reverb.json'); + if (fetched && fetched.length > 0) { + // Append mod_rate placeholder so total stays 9 + this._paramMeta = [ + ...fetched, + { id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' }, + ]; + } + } catch (err) { + console.warn('[ReverbModule] Could not load eoc-reverb.json, using static paramMeta:', err.message); + } + + try { + await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); + } catch (_) { /* already registered */ } + await audioCtx.audioWorklet.addModule('faust/eoc-reverb-processor.js'); + + this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-reverb-processor', { + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + }); + + const wasmResp = await fetch('faust/eoc-reverb.wasm'); + const wasmBytes = await wasmResp.arrayBuffer(); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('ReverbModule worklet init timeout')), 10000); + this._workletNode.port.onmessage = (e) => { + if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } + if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } + }; + this._workletNode.port.postMessage( + { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, + [wasmBytes], + ); + }); + + this._effectGain = audioCtx.createGain(); + this._effectGain.gain.value = 1; + + this._bypassIn.connect(this._workletNode); + this._workletNode.connect(this._effectGain); + this._effectGain.connect(this._bypassOut); + + this._finishInit(); + } + + // --------------------------------------------------------------------------- + // Real-time control + // --------------------------------------------------------------------------- + + setParam(index, normalizedValue) { + super.setParam(index, normalizedValue); + if (!this._workletNode) return; + const meta = this._paramMeta[index]; + if (!meta) return; + // mod_rate (index 8) is a placeholder — skip sending to worklet + if (meta.id === 'mod_rate') return; + const raw = meta.min + normalizedValue * (meta.max - meta.min); + this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); + } + + // --------------------------------------------------------------------------- + // Bypass + // --------------------------------------------------------------------------- + + _onBypassChange(enabled) { + if (!this._effectGain) return; + const t = this._audioCtx.currentTime; + this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); + } + + // --------------------------------------------------------------------------- + // Dispose + // --------------------------------------------------------------------------- + + dispose() { + this._workletNode?.disconnect(); + this._effectGain?.disconnect(); + this._workletNode = null; + this._effectGain = null; + super.dispose(); + } +} diff --git a/playground/js/ui/eoc-chain-ui.js b/playground/js/ui/eoc-chain-ui.js index bb9ad15..eb72421 100644 --- a/playground/js/ui/eoc-chain-ui.js +++ b/playground/js/ui/eoc-chain-ui.js @@ -11,7 +11,10 @@ // // Reacts to 'eoc:change' events on window to stay in sync with external mutations. -import { EOCModule } from '../eoc/eoc-module.js'; +import { EOCModule } from '../eoc/eoc-module.js'; +import { EQModule } from '../eoc/modules/eq-module.js'; +import { CompressorModule } from '../eoc/modules/compressor-module.js'; +import { ReverbModule } from '../eoc/modules/reverb-module.js'; // --------------------------------------------------------------------------- // Stub module factory @@ -37,6 +40,12 @@ const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'mast * @returns {EOCModule} */ export function moduleFactory(id) { + // Real Faust WASM implementations + if (id === 'eq') return new EQModule(); + if (id === 'compressor') return new CompressorModule(); + if (id === 'reverb') return new ReverbModule(); + + // Stub for modules not yet implemented (saturation, delay, master) const def = STUB_DEFS[id]; if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`);