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.
147 lines
6 KiB
JavaScript
147 lines
6 KiB
JavaScript
// 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();
|
||
}
|
||
}
|