fix(playground): engine switch — eager paramMeta + engine-agnostic SynthVisualizer (meml-gdh8)
When switching from C15 to Additive or FM engines, the MLP was resized to 0 outputs because paramMeta wasn't loaded until init() (which needs AudioContext). The SynthVisualizer also disappeared because its section map was hardcoded to C15's 18 param groups. - Add FaustEngineBase.loadParamMeta() for eager JSON-only loading - Call loadParamMeta() before setActiveEngine() in engine switch handler - Add SynthVisualizer.rebuild(paramMeta) to derive sections from groups - Add rebuildParamToSection() / restoreC15ParamToSection() for engine-agnostic param-to-section mapping - Fix tooltip to use engineParamOverrides for non-C15 engines
This commit is contained in:
parent
78e9e64dee
commit
f19d5e5baa
2 changed files with 137 additions and 7 deletions
|
|
@ -260,6 +260,44 @@ const paramToSection = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild paramToSection from an engine's paramMeta groups.
|
||||||
|
* For C15, restores the static SYNTH_SECTIONS-based mapping.
|
||||||
|
* For non-C15 engines, derives sections from paramMeta group fields.
|
||||||
|
*/
|
||||||
|
function rebuildParamToSection(paramMeta) {
|
||||||
|
paramToSection.length = 0;
|
||||||
|
let si = 0, li = 0;
|
||||||
|
let currentGroup = null;
|
||||||
|
for (const pm of paramMeta) {
|
||||||
|
const group = pm.group ?? 'Other';
|
||||||
|
if (group !== currentGroup) {
|
||||||
|
if (currentGroup !== null) si++;
|
||||||
|
currentGroup = group;
|
||||||
|
li = 0;
|
||||||
|
}
|
||||||
|
paramToSection.push({ si, li });
|
||||||
|
li++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore paramToSection to the static C15 layout (from SYNTH_SECTIONS).
|
||||||
|
*/
|
||||||
|
function restoreC15ParamToSection() {
|
||||||
|
paramToSection.length = 0;
|
||||||
|
let idx = 0;
|
||||||
|
for (let si = 0; si < SYNTH_SECTIONS.length; si++) {
|
||||||
|
for (let li = 0; li < SYNTH_SECTIONS[si].count; li++) {
|
||||||
|
paramToSection.push({ si, li });
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (paramToSection.length < N_SYNTH_OUTPUTS) {
|
||||||
|
paramToSection.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Engine-aware preset helpers ----
|
// ---- Engine-aware preset helpers ----
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -527,6 +565,46 @@ class SynthVisualizer {
|
||||||
this.resize();
|
this.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild the visualizer for a new engine's param layout.
|
||||||
|
* Derives sections from paramMeta's group field so it works with any engine.
|
||||||
|
* @param {Array<{group?: string}>} paramMeta
|
||||||
|
*/
|
||||||
|
rebuild(paramMeta) {
|
||||||
|
if (!paramMeta || paramMeta.length === 0) return;
|
||||||
|
|
||||||
|
// Derive sections from paramMeta groups
|
||||||
|
const sections = [];
|
||||||
|
let currentGroup = null;
|
||||||
|
let currentCount = 0;
|
||||||
|
for (const pm of paramMeta) {
|
||||||
|
const group = pm.group ?? 'Other';
|
||||||
|
if (group !== currentGroup) {
|
||||||
|
if (currentGroup !== null) {
|
||||||
|
sections.push({ name: currentGroup, count: currentCount, color: _colorFromGroup(currentGroup) });
|
||||||
|
}
|
||||||
|
currentGroup = group;
|
||||||
|
currentCount = 0;
|
||||||
|
}
|
||||||
|
currentCount++;
|
||||||
|
}
|
||||||
|
if (currentGroup !== null) {
|
||||||
|
sections.push({ name: currentGroup, count: currentCount, color: _colorFromGroup(currentGroup) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild sectionMap
|
||||||
|
this.sectionMap = [];
|
||||||
|
for (const sec of sections) {
|
||||||
|
for (let i = 0; i < sec.count; i++) {
|
||||||
|
this.sectionMap.push(sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize param arrays
|
||||||
|
this.params = new Array(paramMeta.length).fill(0.5);
|
||||||
|
this.displayParams = new Array(paramMeta.length).fill(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
this.canvas.width = window.innerWidth * (window.devicePixelRatio || 1);
|
this.canvas.width = window.innerWidth * (window.devicePixelRatio || 1);
|
||||||
this.canvas.height = window.innerHeight * (window.devicePixelRatio || 1);
|
this.canvas.height = window.innerHeight * (window.devicePixelRatio || 1);
|
||||||
|
|
@ -783,9 +861,16 @@ class SynthVisualizer {
|
||||||
|
|
||||||
const name = (activeEngine?.paramMeta?.[i]?.name) || SYNTH_PARAM_NAMES[i] || `p${i}`;
|
const name = (activeEngine?.paramMeta?.[i]?.name) || SYNTH_PARAM_NAMES[i] || `p${i}`;
|
||||||
const val = this.displayParams[i];
|
const val = this.displayParams[i];
|
||||||
const mapping = paramToSection[i];
|
|
||||||
let rangeStr = '0.00 – 1.00';
|
let rangeStr = '0.00 – 1.00';
|
||||||
let curveStr = '0.50';
|
let curveStr = '0.50';
|
||||||
|
if (engineParamOverrides && i < engineParamOverrides.length) {
|
||||||
|
// Non-C15 engine: use flat engine param overrides
|
||||||
|
const p = engineParamOverrides[i];
|
||||||
|
rangeStr = `${p.min.toFixed(2)} – ${p.max.toFixed(2)}`;
|
||||||
|
curveStr = p.curve.toFixed(2);
|
||||||
|
} else {
|
||||||
|
// C15 path: nested section/group overrides
|
||||||
|
const mapping = paramToSection[i];
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
const ov = groupOverrides[mapping.si];
|
const ov = groupOverrides[mapping.si];
|
||||||
const p = ov.params[mapping.li];
|
const p = ov.params[mapping.li];
|
||||||
|
|
@ -793,6 +878,7 @@ class SynthVisualizer {
|
||||||
const curve = p.curve !== 0.5 ? p.curve : ov.curve;
|
const curve = p.curve !== 0.5 ? p.curve : ov.curve;
|
||||||
curveStr = curve.toFixed(2);
|
curveStr = curve.toFixed(2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lines = [name, `Val: ${val.toFixed(2)}`, `Range: ${rangeStr}`, `Curve: ${curveStr}`];
|
const lines = [name, `Val: ${val.toFixed(2)}`, `Range: ${rangeStr}`, `Curve: ${curveStr}`];
|
||||||
const fontSize = 10 * dpr;
|
const fontSize = 10 * dpr;
|
||||||
|
|
@ -956,6 +1042,31 @@ async function setActiveEngine(engine) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rebuild paramToSection and SynthVisualizer for the new engine
|
||||||
|
if (engine.id === 'shaper-feedback') {
|
||||||
|
restoreC15ParamToSection();
|
||||||
|
// Restore C15 section map in SynthVisualizer
|
||||||
|
if (synthVisualizer) {
|
||||||
|
synthVisualizer.sectionMap = [];
|
||||||
|
let idx = 0;
|
||||||
|
for (const sec of SYNTH_SECTIONS) {
|
||||||
|
for (let i = 0; i < sec.count; i++, idx++) {
|
||||||
|
synthVisualizer.sectionMap.push(sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (synthVisualizer.sectionMap.length < N_SYNTH_OUTPUTS) {
|
||||||
|
synthVisualizer.sectionMap.push({ name: 'Other', count: 1, color: '#666666' });
|
||||||
|
}
|
||||||
|
synthVisualizer.params = new Array(N_SYNTH_OUTPUTS).fill(0.5);
|
||||||
|
synthVisualizer.displayParams = new Array(N_SYNTH_OUTPUTS).fill(0.5);
|
||||||
|
}
|
||||||
|
} else if (engine.paramMeta?.length > 0) {
|
||||||
|
rebuildParamToSection(engine.paramMeta);
|
||||||
|
if (synthVisualizer) {
|
||||||
|
synthVisualizer.rebuild(engine.paramMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build engine-specific param overrides (null for C15, flat array for Faust)
|
// Build engine-specific param overrides (null for C15, flat array for Faust)
|
||||||
if (engine.id === 'shaper-feedback') {
|
if (engine.id === 'shaper-feedback') {
|
||||||
engineParamOverrides = null;
|
engineParamOverrides = null;
|
||||||
|
|
@ -1121,6 +1232,12 @@ async function init() {
|
||||||
// Stop arpeggiator before switching
|
// Stop arpeggiator before switching
|
||||||
if (arpeggiator) arpeggiator.stop();
|
if (arpeggiator) arpeggiator.stop();
|
||||||
|
|
||||||
|
// Ensure paramMeta is available even if audio isn't started yet.
|
||||||
|
// This prevents MLP resizing to 0 outputs for Faust engines.
|
||||||
|
if (newEngine.loadParamMeta) {
|
||||||
|
await newEngine.loadParamMeta();
|
||||||
|
}
|
||||||
|
|
||||||
// If audio is already running, init the new engine now
|
// If audio is already running, init the new engine now
|
||||||
const audioCtxForInit = activeEngine?._audioCtx
|
const audioCtxForInit = activeEngine?._audioCtx
|
||||||
?? activeEngine?._bridge?.audioContext
|
?? activeEngine?._bridge?.audioContext
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,16 @@ export class FaustEngineBase extends SynthEngine {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eagerly load ONLY the JSON descriptor (no WASM, no AudioContext needed).
|
||||||
|
* Safe to call multiple times — no-ops if paramMeta is already populated.
|
||||||
|
* This ensures paramCount is available before init() for MLP sizing.
|
||||||
|
*/
|
||||||
|
async loadParamMeta() {
|
||||||
|
if (this._paramMeta.length > 0) return;
|
||||||
|
this._paramMeta = await loadFaustParamMeta(this._jsonUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise the engine: fetch JSON → build paramMeta → load WASM worklet.
|
* Initialise the engine: fetch JSON → build paramMeta → load WASM worklet.
|
||||||
*
|
*
|
||||||
|
|
@ -80,7 +90,10 @@ export class FaustEngineBase extends SynthEngine {
|
||||||
this._audioCtx = audioCtx;
|
this._audioCtx = audioCtx;
|
||||||
|
|
||||||
// Step 1: fetch and parse the Faust JSON descriptor → paramMeta
|
// Step 1: fetch and parse the Faust JSON descriptor → paramMeta
|
||||||
|
// (skip if already loaded eagerly via loadParamMeta())
|
||||||
|
if (this._paramMeta.length === 0) {
|
||||||
this._paramMeta = await loadFaustParamMeta(this._jsonUrl);
|
this._paramMeta = await loadFaustParamMeta(this._jsonUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: fetch the WASM binary
|
// Step 2: fetch the WASM binary
|
||||||
const wasmResp = await fetch(this._wasmUrl);
|
const wasmResp = await fetch(this._wasmUrl);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue