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.
145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
// SynthEngine — base interface that all synth engines implement.
|
||
//
|
||
// Engines are duck-typed in JavaScript, but this base class documents the
|
||
// contract and provides default no-op stubs so subclasses only override what
|
||
// they need. All engines must satisfy this interface for hot-swapping to work.
|
||
//
|
||
// Usage:
|
||
// import { SynthEngine } from './engine-interface.js';
|
||
// class MyEngine extends SynthEngine { ... }
|
||
|
||
export class SynthEngine {
|
||
// ---------------------------------------------------------------------------
|
||
// Identity (override in subclass)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Stable machine ID, e.g. 'shaper-feedback' | 'additive' | 'fm'.
|
||
* Used for persistence keys and engine-switcher logic.
|
||
* @returns {string}
|
||
*/
|
||
get id() {
|
||
throw new Error(`${this.constructor.name}: id not implemented`);
|
||
}
|
||
|
||
/**
|
||
* Human-readable label shown in the engine-switcher UI.
|
||
* @returns {string}
|
||
*/
|
||
get displayName() {
|
||
throw new Error(`${this.constructor.name}: displayName not implemented`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Parameter schema
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Number of continuous parameters this engine exposes (= MLP output count).
|
||
* @returns {number}
|
||
*/
|
||
get paramCount() {
|
||
return this.paramMeta.length;
|
||
}
|
||
|
||
/**
|
||
* Array of parameter descriptors, one per MLP output index.
|
||
*
|
||
* Each entry must have:
|
||
* id {string} — stable machine ID (used for presets)
|
||
* name {string} — short display name
|
||
* min {number} — normalised lower bound [0,1]
|
||
* max {number} — normalised upper bound [0,1]
|
||
* init {number} — default normalised value [0,1]
|
||
* curve {number} — power-curve bias: 0.5 = linear, <0.5 = log, >0.5 = exp
|
||
* group {string} — section label (for group drawer / colour coding)
|
||
*
|
||
* @returns {Array<{id:string, name:string, min:number, max:number, init:number, curve:number, group:string}>}
|
||
*/
|
||
get paramMeta() {
|
||
throw new Error(`${this.constructor.name}: paramMeta not implemented`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Lifecycle
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Load WASM / worklet, connect the output node to audioCtx.destination.
|
||
* Must be idempotent — calling init() twice should be safe.
|
||
*
|
||
* @param {AudioContext} audioCtx
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async init(audioCtx) { // eslint-disable-line no-unused-vars
|
||
throw new Error(`${this.constructor.name}: init() not implemented`);
|
||
}
|
||
|
||
/**
|
||
* Release all AudioNodes, Workers, and WASM memory.
|
||
* The engine should be unusable after dispose().
|
||
*/
|
||
dispose() {
|
||
// default no-op — override when cleanup is needed
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Real-time control
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Set a single parameter by MLP output index.
|
||
*
|
||
* @param {number} index — 0-based index into paramMeta
|
||
* @param {number} normalizedValue — [0, 1]
|
||
*/
|
||
setParam(index, normalizedValue) { // eslint-disable-line no-unused-vars
|
||
// default no-op
|
||
}
|
||
|
||
/**
|
||
* Trigger a note.
|
||
*
|
||
* @param {number} note — MIDI note number 0–127
|
||
* @param {number} velocity — [0, 1]
|
||
*/
|
||
noteOn(note, velocity = 0.7) { // eslint-disable-line no-unused-vars
|
||
// default no-op
|
||
}
|
||
|
||
/**
|
||
* Release a note.
|
||
*
|
||
* @param {number} note — MIDI note number 0–127
|
||
*/
|
||
noteOff(note) { // eslint-disable-line no-unused-vars
|
||
// default no-op
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Audio graph
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Return the AudioNode that should be connected downstream (e.g. to a
|
||
* compressor or the AudioContext destination).
|
||
*
|
||
* @returns {AudioNode}
|
||
*/
|
||
getOutputNode() {
|
||
throw new Error(`${this.constructor.name}: getOutputNode() not implemented`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Status (mirrors C15Bridge.running for backward compat)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* True once init() has completed successfully and the engine is producing
|
||
* audio. Engines should set this themselves after init().
|
||
* @type {boolean}
|
||
*/
|
||
get running() {
|
||
return this._running ?? false;
|
||
}
|
||
}
|