memlnaut-nisps/playground/js/eoc/modules/reverb-module.js

148 lines
6 KiB
JavaScript
Raw Normal View History

// 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();
}
}