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

124 lines
4.3 KiB
JavaScript
Raw Normal View History

// master-module.js — EOC Master Bus module (meml-2zm part 2).
//
// Faust source: playground/faust/eoc-master.dsp
// Worklet processor: playground/faust/eoc-master-processor.js
// Processor name: 'eoc-master-processor'
//
// 4 parameters (in JSON / Faust alphabetical order):
// 0 dc_block [0, 1] init=1 (nentry: off/on)
// 1 gain [0, 2] init=1.0
// 2 limiter_thresh [-12, 0] init=-1.0
// 3 width [0, 2] init=1.0
import { EOCModule } from '../eoc-module.js';
export class MasterModule extends EOCModule {
constructor() {
super();
this._workletNode = null;
this._effectGain = null;
}
// ---------------------------------------------------------------------------
// EOCModule identity
// ---------------------------------------------------------------------------
get id() { return 'master'; }
get displayName() { return 'Master Bus'; }
get paramMeta() {
return [
{ id: 'dc_block', name: 'DC Block', min: 0, max: 1, init: 1.0, curve: 0.5, group: 'Master' },
{ id: 'gain', name: 'Gain', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' },
{ id: 'limiter_thresh', name: 'Limiter (dB)', min: -12, max: 0, init: (-1.0 - (-12)) / 12, curve: 0.5, group: 'Master' },
{ id: 'width', name: 'Width', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' },
];
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
async init(audioCtx) {
await super.init(audioCtx);
const wasmResp = await fetch('faust/eoc-master.wasm');
if (!wasmResp.ok) throw new Error(`[MasterModule] Failed to fetch eoc-master.wasm: ${wasmResp.status}`);
const wasmBytes = await wasmResp.arrayBuffer();
await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js');
await audioCtx.audioWorklet.addModule('faust/eoc-master-processor.js');
this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-master-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [2],
channelCount: 2,
channelCountMode: 'explicit',
});
this._workletNode.port.onmessage = (e) => {
if (e.data?.type === 'error') {
console.error('[MasterModule] Worklet error:', e.data.message);
}
};
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._workletNode.port.postMessage(
{ type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate },
[wasmBytes]
);
this.paramMeta.forEach((_, i) => {
this.setParam(i, this.getCurrentParamValue(i));
});
this._finishInit();
}
// ---------------------------------------------------------------------------
// Real-time control
// ---------------------------------------------------------------------------
setParam(index, normalizedValue) {
super.setParam(index, normalizedValue);
if (!this._workletNode) return;
const raw = this._denormalize(index, normalizedValue);
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);
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
dispose() {
if (this._workletNode) {
this._workletNode.disconnect();
this._workletNode.port.onmessage = null;
this._workletNode = null;
}
if (this._effectGain) {
this._effectGain.disconnect();
this._effectGain = null;
}
super.dispose();
}
}