130 lines
5.1 KiB
JavaScript
130 lines
5.1 KiB
JavaScript
|
|
// 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.1–200 ms, init 10]
|
|||
|
|
// 1: knee [0–24 dB, init 6]
|
|||
|
|
// 2: makeup [0–24 dB, init 0]
|
|||
|
|
// 3: mix [0–1, init 1] (dry/wet for parallel compression)
|
|||
|
|
// 4: ratio [1–20, init 4]
|
|||
|
|
// 5: release [10–2000 ms, init 100]
|
|||
|
|
// 6: threshold [-60–0 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();
|
|||
|
|
}
|
|||
|
|
}
|