memlnaut-nisps/playground/js/synth/engine-interface.js

146 lines
4.7 KiB
JavaScript
Raw Normal View History

// 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 0127
* @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 0127
*/
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;
}
}