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 0000000..965376f Binary files /dev/null and b/playground/faust/eoc-compressor.wasm differ 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 0000000..8deaf08 Binary files /dev/null and b/playground/faust/eoc-eq.wasm differ 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 0000000..6357d1b Binary files /dev/null and b/playground/faust/eoc-reverb.wasm differ 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}'`);