memlnaut-nisps/playground/js/eoc/modules/reverb-module.js
w1n5t0n 0a5f2544ca feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc)
Faust DSP sources, compiled WASM + JSON descriptors, AudioWorklet processors,
and EOCModule subclasses for all three effects.

- meml-4b4: 4-band parametric EQ (fi.low_shelf, fi.peak_eq × 2, fi.high_shelf)
  10 params: freq/gain per band, Q for the two mid bell bands
- meml-cpe: stereo feed-forward compressor (co.compressor_mono × 2)
  7 params: threshold, ratio, attack, release, knee, makeup, mix (parallel)
- meml-wwc: zita reverb (re.zita_rev1_stereo) with pre-delay and M/S width
  8 active params + mod_rate placeholder (zita internal mod not yet exposed)

All three DSPs compile cleanly under Faust 2.83.1. moduleFactory in
eoc-chain-ui.js now returns real instances for eq/compressor/reverb.
2026-04-03 17:57:36 +01:00

147 lines
6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.120 s, init 3]
// 1: diffusion [01, init 0.7]
// 2: hi_damp [01, init 0.5]
// 3: lo_damp [01, init 0]
// 4: mix [01, init 0.2]
// 5: predelay [0100 ms, init 0]
// 6: size [01, init 0.5]
// 7: width [01, 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();
}
}