memlnaut-nisps/playground/faust/fm-matrix-processor.js

194 lines
6.3 KiB
JavaScript
Raw Normal View History

/**
* fm-matrix-processor.js AudioWorklet processor for the FM Matrix synth engine
*
* Extends FaustWorkletProcessor (faust-worklet-processor.js).
* Loads fm-matrix.wasm compiled from fm-matrix.dsp.
*
* Parameter addresses are built from the Faust JSON descriptor at init time.
* Indexaddress mapping follows the order returned by faustJsonToParamMeta()
* (alphabetical within groups, groups in declaration order).
*
* Hidden params (not in NISPS 55-param list):
* /fm-matrix/Master/freq set by noteOn
* /fm-matrix/Master/gate set by noteOn/noteOff
* /fm-matrix/Master/_vel set by noteOn
*/
// Must be imported in AudioWorkletGlobalScope — include faust-worklet-processor.js
// via audioCtx.audioWorklet.addModule() before this file.
class FMMatrixProcessor extends FaustWorkletProcessor {
constructor(options) {
super(options);
this._dsp = null;
this._paramAddresses = []; // ordered by NISPS index (from JSON)
this._hiddenAddresses = {}; // freq, gate, _vel
this._blockSize = 128;
this._sampleRate = 48000;
}
// ---------------------------------------------------------------------------
// FaustWorkletProcessor overrides
// ---------------------------------------------------------------------------
async _initWasm(wasmBytes, sr) {
this._sampleRate = sr;
// The Faust -lang wasm output exports a single factory function
// named by the -cn flag (fm_matrix).
const module = await WebAssembly.compile(wasmBytes);
const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 });
const imports = {
env: {
memory,
memoryBase: 0,
tableBase: 0,
_abs: Math.abs,
_acosf: Math.acos,
_asinf: Math.asin,
_atanf: Math.atan,
_atan2f: Math.atan2,
_ceilf: Math.ceil,
_cosf: Math.cos,
_expf: Math.exp,
_floorf: Math.floor,
_fmodf: (x, y) => x % y,
_logf: Math.log,
_log10f: Math.log10,
_max_f: Math.max,
_min_f: Math.min,
_remainderf: (x, y) => x - Math.round(x / y) * y,
_powf: Math.pow,
_roundf: Math.round,
_sinf: Math.sin,
_sqrtf: Math.sqrt,
_tanf: Math.tan,
_fabs: Math.abs,
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
},
};
const instance = await WebAssembly.instantiate(module, imports);
const exports = instance.exports;
// Faust -lang wasm exports: getNumInputs, getNumOutputs, init,
// instanceInit, getSampleRate, compute, setParamValue, getParamValue,
// getJSON (pointer to null-terminated JSON string in memory)
this._exports = exports;
this._heap = new Float32Array(memory.buffer);
this._heapi32 = new Int32Array(memory.buffer);
this._mem = memory;
// Allocate DSP instance (Faust C++ new equivalent)
if (exports.createDSPInstance) {
this._dsp = exports.createDSPInstance();
} else if (exports.fm_matrix) {
this._dsp = exports.fm_matrix();
} else {
// fallback: look for any exported constructor-like function
const keys = Object.keys(exports).filter(k => typeof exports[k] === 'function');
console.warn('[FMMatrixProcessor] No createDSPInstance found; exports:', keys);
return;
}
// Initialise at sample rate
exports.init(this._dsp, sr);
// Parse embedded JSON to build param address list
this._buildParamIndex(exports, memory);
}
_buildParamIndex(exports, memory) {
// getJSON() returns a pointer to a JSON string in WASM memory
if (!exports.getJSON) return;
const ptr = exports.getJSON(this._dsp);
const buf = new Uint8Array(memory.buffer);
let str = '';
let i = ptr;
while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); }
let desc;
try { desc = JSON.parse(str); } catch { return; }
const HIDDEN = new Set(['freq', 'gate', '_vel']);
const addresses = [];
function walk(items, path) {
for (const item of items) {
const label = item.label ?? '';
const type = item.type ?? '';
const addr = item.address ?? (path + '/' + label);
if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) {
if (HIDDEN.has(label)) {
// store hidden separately
addresses._hidden = addresses._hidden || {};
addresses._hidden[label] = addr;
} else {
addresses.push(addr);
}
} else if (item.items) {
walk(item.items, path + '/' + label);
}
}
}
walk(desc.ui ?? [], '');
this._paramAddresses = addresses;
this._hiddenAddresses = addresses._hidden ?? {};
}
_onSetParam(index, value) {
if (!this._exports || !this._dsp) return;
const addr = this._paramAddresses[index];
if (addr && this._exports.setParamValue) {
this._exports.setParamValue(this._dsp, addr, value);
}
}
_onNoteOn(freq, vel) {
if (!this._exports || !this._dsp) return;
const set = (label, v) => {
const addr = this._hiddenAddresses[label];
if (addr) this._exports.setParamValue?.(this._dsp, addr, v);
};
set('freq', freq);
set('_vel', vel);
set('gate', 1);
}
_onNoteOff(_freq) {
if (!this._exports || !this._dsp) return;
const addr = this._hiddenAddresses['gate'];
if (addr) this._exports.setParamValue?.(this._dsp, addr, 0);
}
_renderBlock(outL, outR, blockSize) {
if (!this._exports || !this._dsp) return;
const exports = this._exports;
const heap = this._heap;
const mem = this._mem;
// Allocate output buffers in WASM heap (crude bump allocator using high addresses)
const heapBytes = mem.buffer.byteLength;
const outLOff = (heapBytes >> 2) - blockSize * 2 - 64;
const outROff = outLOff + blockSize;
// Allocate pointer arrays for outputs
const ptrSize = 4; // 32-bit pointers in wasm32
const outPtrsOff = outROff + blockSize;
const i32 = this._heapi32;
i32[outPtrsOff] = outLOff * 4; // byte offset in memory
i32[outPtrsOff + 1] = outROff * 4;
exports.compute(this._dsp, blockSize, 0 /* no inputs */, outPtrsOff * 4);
for (let i = 0; i < blockSize; i++) {
outL[i] = heap[outLOff + i];
outR[i] = heap[outROff + i];
}
}
}
registerProcessor('fm-matrix-processor', FMMatrixProcessor);