From f19d5e5baab8c6f72cc8ae83cc26f01d625dcd47 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Fri, 3 Apr 2026 18:41:05 +0100 Subject: [PATCH] =?UTF-8?q?fix(playground):=20engine=20switch=20=E2=80=94?= =?UTF-8?q?=20eager=20paramMeta=20+=20engine-agnostic=20SynthVisualizer=20?= =?UTF-8?q?(meml-gdh8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- playground/js/a-app.js | 129 +++++++++++++++++++++-- playground/js/synth/faust-engine-base.js | 15 ++- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/playground/js/a-app.js b/playground/js/a-app.js index fc053ef..2d4035b 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -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 ---- /** @@ -527,6 +565,46 @@ class SynthVisualizer { 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() { this.canvas.width = window.innerWidth * (window.devicePixelRatio || 1); this.canvas.height = window.innerHeight * (window.devicePixelRatio || 1); @@ -783,15 +861,23 @@ class SynthVisualizer { const name = (activeEngine?.paramMeta?.[i]?.name) || SYNTH_PARAM_NAMES[i] || `p${i}`; const val = this.displayParams[i]; - const mapping = paramToSection[i]; let rangeStr = '0.00 – 1.00'; let curveStr = '0.50'; - if (mapping) { - const ov = groupOverrides[mapping.si]; - const p = ov.params[mapping.li]; + 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)}`; - const curve = p.curve !== 0.5 ? p.curve : ov.curve; - curveStr = curve.toFixed(2); + curveStr = p.curve.toFixed(2); + } else { + // C15 path: nested section/group overrides + const mapping = paramToSection[i]; + if (mapping) { + const ov = groupOverrides[mapping.si]; + const p = ov.params[mapping.li]; + rangeStr = `${p.min.toFixed(2)} – ${p.max.toFixed(2)}`; + const curve = p.curve !== 0.5 ? p.curve : ov.curve; + curveStr = curve.toFixed(2); + } } const lines = [name, `Val: ${val.toFixed(2)}`, `Range: ${rangeStr}`, `Curve: ${curveStr}`]; @@ -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) if (engine.id === 'shaper-feedback') { engineParamOverrides = null; @@ -1121,6 +1232,12 @@ async function init() { // Stop arpeggiator before switching 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 const audioCtxForInit = activeEngine?._audioCtx ?? activeEngine?._bridge?.audioContext diff --git a/playground/js/synth/faust-engine-base.js b/playground/js/synth/faust-engine-base.js index 76d4ed9..a683d0b 100644 --- a/playground/js/synth/faust-engine-base.js +++ b/playground/js/synth/faust-engine-base.js @@ -69,6 +69,16 @@ export class FaustEngineBase extends SynthEngine { // 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. * @@ -80,7 +90,10 @@ export class FaustEngineBase extends SynthEngine { this._audioCtx = audioCtx; // Step 1: fetch and parse the Faust JSON descriptor → paramMeta - this._paramMeta = await loadFaustParamMeta(this._jsonUrl); + // (skip if already loaded eagerly via loadParamMeta()) + if (this._paramMeta.length === 0) { + this._paramMeta = await loadFaustParamMeta(this._jsonUrl); + } // Step 2: fetch the WASM binary const wasmResp = await fetch(this._wasmUrl);