feat(playground): EOC NISPS Shared mode — combined MLP for synth + effects (meml-dqt)

- 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
This commit is contained in:
w1n5t0n 2026-04-03 18:01:10 +01:00
parent 0a5f2544ca
commit 4f44cd016d
2 changed files with 82 additions and 7 deletions

View file

@ -1731,6 +1731,24 @@ html, body {
pointer-events: none; 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 modal ---- */
.help-overlay { .help-overlay {
position: fixed; position: fixed;

View file

@ -748,6 +748,19 @@ function outputCountForMode(mode) {
return N_SYNTH_OUTPUTS; 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. * Recreate IML instances with a new output count.
* Joystick IML uses warm-start weight transfer to preserve learned mappings. * 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}`; if (engineDockBtn) engineDockBtn.title = `Engine: ${engine.displayName}`;
EngineSwitcher.setActive(engine.id); EngineSwitcher.setActive(engine.id);
EngineSwitcher.setLoading(engine.id, false); EngineSwitcher.setLoading(engine.id, false);
await resizeMLP(engine.paramCount); await resizeMLP(totalOutputCount());
// Reload MIDI CC map for the new engine (scoped storage key) // Reload MIDI CC map for the new engine (scoped storage key)
reloadMidiCCMap(); reloadMidiCCMap();
// Rebuild heatmap cells from the new engine's paramMeta // Rebuild heatmap cells — include EOC params when in shared mode
rebuildHeatmap(engine.paramMeta); 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 // Rewire EOC chain to the new engine's output node
if (eocChain && _eocInited) { if (eocChain && _eocInited) {
@ -1050,9 +1070,30 @@ async function init() {
EOCChainUI.init(eocChain, eocDrawerBody); EOCChainUI.init(eocChain, eocDrawerBody);
} }
// Log EOC structural changes; future NISPS modes will act on this event // Handle EOC structural changes — resize MLP and rebuild heatmap when in Shared mode
window.addEventListener('eoc:change', () => { window.addEventListener('eoc:change', async (e) => {
console.log('[EOC] chain changed, paramCount:', eocChain.paramCount); 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. // Debug probe — exposed on window when ?debug=1 is in the URL.
@ -1800,15 +1841,31 @@ function routeOutputs(outputs) {
const now = performance.now(); const now = performance.now();
if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) { if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) {
_lastParamSendTime = now; _lastParamSendTime = now;
const engineParamCount = activeEngine.paramCount;
for (let i = 0; i < overridden.length && i < N_OUTPUTS; i++) { for (let i = 0; i < overridden.length && i < N_OUTPUTS; i++) {
const v = overridden[i]; const v = overridden[i];
if (Math.abs(v - _lastSentParams[i]) > PARAM_DEAD_ZONE) { 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; _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') { } else if (outputMode === 'midi-cc') {
// MIDI CC output — route through overrides then send CC messages // MIDI CC output — route through overrides then send CC messages
if (midiOutput && midiOutput.enabled && midiOutput.activeOutput) { if (midiOutput && midiOutput.enabled && midiOutput.activeOutput) {