From 4f44cd016dab42c0cd4e34c8e308312570dfaeef Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Fri, 3 Apr 2026 18:01:10 +0100 Subject: [PATCH] =?UTF-8?q?feat(playground):=20EOC=20NISPS=20Shared=20mode?= =?UTF-8?q?=20=E2=80=94=20combined=20MLP=20for=20synth=20+=20effects=20(me?= =?UTF-8?q?ml-dqt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add totalOutputCount() helper: engine params + EOC params when nispsMode === 'shared' - setActiveEngine() calls resizeMLP(totalOutputCount()) and rebuilds heatmap with combined paramMeta - eoc:change listener handles nispsMode-changed and module-* events: resizes MLP, rebuilds heatmap, toggles .shared-mode CSS class - routeOutputs() slices outputs: indices < engineParamCount go to engine, remainder to eocChain.setParam() in Shared mode - CSS: .heatmap-strip.shared-mode adds green bottom border and '+ FX' label to signal combined output space --- playground/css/a-immersive.css | 18 +++++++++ playground/js/a-app.js | 71 ++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/playground/css/a-immersive.css b/playground/css/a-immersive.css index 433ad29..98ee391 100644 --- a/playground/css/a-immersive.css +++ b/playground/css/a-immersive.css @@ -1731,6 +1731,24 @@ html, body { pointer-events: none; } +/* ---- Heatmap shared-mode indicator (EOC NISPS Shared mode active) ---- */ +/* .heatmap-strip gets this class; ::after appends a small '+ FX' label */ +.heatmap-strip.shared-mode { + border-bottom: 1px solid rgba(120, 220, 180, 0.35); +} + +.heatmap-strip.shared-mode::after { + content: '+ FX'; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.04em; + color: rgba(120, 220, 180, 0.8); + padding: 0 4px; + white-space: nowrap; + align-self: center; + pointer-events: none; +} + /* ---- Help modal ---- */ .help-overlay { position: fixed; diff --git a/playground/js/a-app.js b/playground/js/a-app.js index 3a0147f..3410e9b 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -748,6 +748,19 @@ function outputCountForMode(mode) { return N_SYNTH_OUTPUTS; } +/** + * Total MLP output count: engine params + EOC params when in Shared mode. + * In all other NISPS modes (bypass/linked/independent) or when no EOC chain + * exists, only the engine's own param count is included. + * + * @returns {number} + */ +function totalOutputCount() { + const engineParams = activeEngine?.paramCount ?? N_SYNTH_OUTPUTS; + const eocParams = (eocChain?.nispsMode === 'shared') ? (eocChain?.paramCount ?? 0) : 0; + return engineParams + eocParams; +} + /** * Recreate IML instances with a new output count. * Joystick IML uses warm-start weight transfer to preserve learned mappings. @@ -799,13 +812,20 @@ async function setActiveEngine(engine) { if (engineDockBtn) engineDockBtn.title = `Engine: ${engine.displayName}`; EngineSwitcher.setActive(engine.id); EngineSwitcher.setLoading(engine.id, false); - await resizeMLP(engine.paramCount); + await resizeMLP(totalOutputCount()); // Reload MIDI CC map for the new engine (scoped storage key) reloadMidiCCMap(); - // Rebuild heatmap cells from the new engine's paramMeta - rebuildHeatmap(engine.paramMeta); + // Rebuild heatmap cells — include EOC params when in shared mode + if (eocChain?.nispsMode === 'shared') { + const combinedMeta = [...(engine.paramMeta ?? []), ...eocChain.paramMeta]; + rebuildHeatmap(combinedMeta); + } else { + rebuildHeatmap(engine.paramMeta); + } + document.getElementById('heatmap-cells')?.parentElement + ?.classList.toggle('shared-mode', eocChain?.nispsMode === 'shared'); // Rewire EOC chain to the new engine's output node if (eocChain && _eocInited) { @@ -1050,9 +1070,30 @@ async function init() { EOCChainUI.init(eocChain, eocDrawerBody); } - // Log EOC structural changes; future NISPS modes will act on this event - window.addEventListener('eoc:change', () => { - console.log('[EOC] chain changed, paramCount:', eocChain.paramCount); + // Handle EOC structural changes — resize MLP and rebuild heatmap when in Shared mode + window.addEventListener('eoc:change', async (e) => { + const reason = e.detail?.reason; + console.log('[EOC] chain changed, reason:', reason, 'paramCount:', eocChain.paramCount, 'nispsMode:', eocChain.nispsMode); + + // Only act on nispsMode switches or module add/remove/move — ignore other events + if (reason === 'nispsMode-changed' || reason?.startsWith('module')) { + const newTotal = totalOutputCount(); + if (newTotal !== N_OUTPUTS) { + await resizeMLP(newTotal); + } + // Rebuild heatmap to include EOC params when in shared mode, or revert when leaving + if (eocChain.nispsMode === 'shared') { + const combinedMeta = [ + ...(activeEngine?.paramMeta ?? []), + ...eocChain.paramMeta, + ]; + rebuildHeatmap(combinedMeta); + } else { + rebuildHeatmap(activeEngine?.paramMeta ?? []); + } + document.getElementById('heatmap-cells')?.parentElement + ?.classList.toggle('shared-mode', eocChain.nispsMode === 'shared'); + } }); // Debug probe — exposed on window when ?debug=1 is in the URL. @@ -1800,15 +1841,31 @@ function routeOutputs(outputs) { const now = performance.now(); if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) { _lastParamSendTime = now; + const engineParamCount = activeEngine.paramCount; for (let i = 0; i < overridden.length && i < N_OUTPUTS; i++) { const v = overridden[i]; if (Math.abs(v - _lastSentParams[i]) > PARAM_DEAD_ZONE) { - activeEngine.setParam(i, v); + // Only send engine params (indices before engineParamCount) to the engine + if (i < engineParamCount) { + activeEngine.setParam(i, v); + } _lastSentParams[i] = v; } } } } + + // In Shared mode, route outputs beyond engine params to the EOC chain + if (eocChain?.nispsMode === 'shared') { + const engineParamCount = activeEngine?.paramCount ?? 0; + const eocParamCount = eocChain.paramCount; + for (let i = 0; i < eocParamCount; i++) { + const outputIndex = engineParamCount + i; + if (outputIndex < outputs.length) { + eocChain.setParam(i, outputs[outputIndex]); + } + } + } } else if (outputMode === 'midi-cc') { // MIDI CC output — route through overrides then send CC messages if (midiOutput && midiOutput.enabled && midiOutput.activeOutput) {