memlnaut-nisps/playground/js/eoc/modules/eq-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

143 lines
6.3 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/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 [20500 Hz, init 80], gain1 [-12+12 dB, init 0]
// Band 2 (Low-Mid): freq2 [1002000 Hz, init 400], gain2 [-12+12 dB, init 0], q2 [0.110, init 1]
// Band 3 (High-Mid): freq3 [5008000 Hz, init 2500], gain3 [-12+12 dB, init 0], q3 [0.110, init 1]
// Band 4 (High Shelf): freq4 [200020000 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();
}
}