Adds the Faust DSP toolchain infrastructure: placeholder additive and FM DSP files, build.sh (faust -lang wasm per .dsp), faustJsonToParamMeta() to convert Faust JSON UI trees into the standard paramMeta format, FaustEngineBase (SynthEngine subclass wiring init/setParam/noteOn/noteOff through AudioWorklet messages), and FaustWorkletProcessor base class for concrete engine processors.
138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
/**
|
||
* faust-worklet-processor.js — Base AudioWorklet processor for Faust WASM engines
|
||
*
|
||
* This file must be loaded via audioContext.audioWorklet.addModule() before
|
||
* creating a FaustWorkletNode. It runs in AudioWorkletGlobalScope.
|
||
*
|
||
* Each Faust engine subclasses FaustWorkletProcessor and overrides:
|
||
* - static get processorName() — returns the unique processor name string
|
||
* - _initWasm(wasmBytes, sampleRate) — initialises the Faust WASM instance
|
||
* - _renderBlock(outputL, outputR, blockSize) — fills output buffers per block
|
||
*
|
||
* Message protocol (port.postMessage from main thread):
|
||
* { type: 'init', wasmBytes: ArrayBuffer, sampleRate: number }
|
||
* { type: 'setParam', index: number, value: number }
|
||
* { type: 'noteOn', freq: number, vel: number }
|
||
* { type: 'noteOff', freq: number }
|
||
*
|
||
* Replies from worklet to main thread:
|
||
* { type: 'ready' }
|
||
* { type: 'error', message: string }
|
||
*/
|
||
|
||
class FaustWorkletProcessor extends AudioWorkletProcessor {
|
||
constructor(options) {
|
||
super(options);
|
||
this._ready = false;
|
||
this._paramValues = {}; // index → current value (raw Faust units)
|
||
|
||
this.port.onmessage = (e) => this._handleMessage(e.data);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Message handler (runs in worklet thread)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
_handleMessage(msg) {
|
||
if (!msg || !msg.type) return;
|
||
|
||
switch (msg.type) {
|
||
case 'init':
|
||
this._initWasm(msg.wasmBytes, msg.sampleRate || sampleRate)
|
||
.then(() => {
|
||
this._ready = true;
|
||
this.port.postMessage({ type: 'ready' });
|
||
})
|
||
.catch((err) => {
|
||
this.port.postMessage({ type: 'error', message: String(err) });
|
||
});
|
||
break;
|
||
|
||
case 'setParam':
|
||
this._paramValues[msg.index] = msg.value;
|
||
this._onSetParam(msg.index, msg.value);
|
||
break;
|
||
|
||
case 'noteOn':
|
||
this._onNoteOn(msg.freq, msg.vel);
|
||
break;
|
||
|
||
case 'noteOff':
|
||
this._onNoteOff(msg.freq);
|
||
break;
|
||
|
||
default:
|
||
console.warn('[FaustWorkletProcessor] Unknown message type:', msg.type);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// AudioWorkletProcessor interface
|
||
// ---------------------------------------------------------------------------
|
||
|
||
process(_inputs, outputs, _params) {
|
||
if (!this._ready) return true;
|
||
|
||
const out = outputs[0];
|
||
const blockSize = out[0]?.length ?? 128;
|
||
const outL = out[0] ?? new Float32Array(blockSize);
|
||
const outR = out[1] ?? new Float32Array(blockSize);
|
||
|
||
this._renderBlock(outL, outR, blockSize);
|
||
|
||
return true; // keep processor alive
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Subclass API — override these in concrete engine processors
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Initialise the WASM module. Called once with the raw bytes and sample rate.
|
||
* Must return a Promise that resolves when the engine is ready to render.
|
||
*
|
||
* @param {ArrayBuffer} wasmBytes
|
||
* @param {number} sampleRate
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async _initWasm(_wasmBytes, _sampleRate) {
|
||
// Default no-op: subclasses that don't use WASM can override _renderBlock only.
|
||
}
|
||
|
||
/**
|
||
* Called when a parameter value changes. Override to forward to DSP.
|
||
* @param {number} index Param index (matches paramMeta order)
|
||
* @param {number} value Raw value in Faust units
|
||
*/
|
||
_onSetParam(_index, _value) {}
|
||
|
||
/**
|
||
* Called on note-on.
|
||
* @param {number} freq Hz
|
||
* @param {number} vel 0–1
|
||
*/
|
||
_onNoteOn(_freq, _vel) {}
|
||
|
||
/**
|
||
* Called on note-off.
|
||
* @param {number} freq Hz
|
||
*/
|
||
_onNoteOff(_freq) {}
|
||
|
||
/**
|
||
* Fill a single audio block. Called from process() every 128 samples.
|
||
* Both arrays are pre-allocated Float32Arrays of length blockSize.
|
||
*
|
||
* @param {Float32Array} outL Left channel output buffer (write to this)
|
||
* @param {Float32Array} outR Right channel output buffer (write to this)
|
||
* @param {number} blockSize
|
||
*/
|
||
_renderBlock(_outL, _outR, _blockSize) {
|
||
// Default: silence — override in subclass
|
||
}
|
||
}
|
||
|
||
// Note: registerProcessor() is called by each concrete engine file, not here,
|
||
// because each engine has its own processor name.
|
||
// Subclass files should end with:
|
||
// registerProcessor('my-engine-processor', MyEngineProcessor);
|