- Full 4-operator FM with continuous 4×4 routing matrix (12 cross-mod pairs) - Compiles to 46KB WASM via faust -lang wasm - 55 visible NISPS params (pitch_glide missing from DSP, tracked) - Includes fm-matrix-processor.js AudioWorklet + JSON param descriptor
193 lines
6.3 KiB
JavaScript
193 lines
6.3 KiB
JavaScript
/**
|
|
* 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.
|
|
* Index→address 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);
|