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

130 lines
5.1 KiB
JavaScript
Raw Normal View History

// 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.1200 ms, init 10]
// 1: knee [024 dB, init 6]
// 2: makeup [024 dB, init 0]
// 3: mix [01, init 1] (dry/wet for parallel compression)
// 4: ratio [120, init 4]
// 5: release [102000 ms, init 100]
// 6: threshold [-600 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();
}
}