Merge Shared + Linked NISPS modes (meml-dqt, meml-vqg)
Resolved eoc:change listener conflict: both modes coexist in same handler. Shared mode resizes combined MLP; Linked mode manages second IML.
This commit is contained in:
commit
5a004c6a3e
1 changed files with 114 additions and 11 deletions
|
|
@ -95,6 +95,10 @@ let outputMode = 'visual';
|
|||
let eocChain = null;
|
||||
let _eocInited = false; // guard: init once per AudioContext lifetime
|
||||
|
||||
// EOC Linked mode — second IML driven by the same joystick, independent training
|
||||
let imlEoc = null; // second IML for EOC params (Linked mode)
|
||||
let eocTrainingTarget = 'synth'; // 'synth' | 'eoc' — which MLP RL feedback targets
|
||||
|
||||
// ---- MIDI CC state ----
|
||||
let midiOutput = null;
|
||||
// Storage key scoped per engine so CC assignments don't bleed between engines.
|
||||
|
|
@ -1070,18 +1074,27 @@ async function init() {
|
|||
EOCChainUI.init(eocChain, eocDrawerBody);
|
||||
}
|
||||
|
||||
// Handle EOC structural changes — resize MLP and rebuild heatmap when in Shared mode
|
||||
// Handle EOC structural changes — Shared mode resizes MLP; Linked mode manages second IML
|
||||
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') {
|
||||
// Linked mode lifecycle
|
||||
if (eocChain.nispsMode === 'linked') {
|
||||
await initEocIML();
|
||||
} else {
|
||||
destroyEocIML();
|
||||
eocTrainingTarget = 'synth';
|
||||
}
|
||||
}
|
||||
|
||||
// Shared mode: resize MLP and rebuild heatmap on mode switch or module change
|
||||
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 ?? []),
|
||||
|
|
@ -1093,6 +1106,11 @@ async function init() {
|
|||
}
|
||||
document.getElementById('heatmap-cells')?.parentElement
|
||||
?.classList.toggle('shared-mode', eocChain.nispsMode === 'shared');
|
||||
|
||||
// Linked mode: recreate EOC IML when modules change
|
||||
if (eocChain.nispsMode === 'linked' && imlEoc) {
|
||||
await initEocIML();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1148,6 +1166,14 @@ async function init() {
|
|||
};
|
||||
}
|
||||
|
||||
// EOC Linked-mode probe — exposed unconditionally so the training-target UI
|
||||
// (meml-0r0) can wire buttons without requiring ?debug=1.
|
||||
window.__nispsEoc = {
|
||||
get trainingTarget() { return eocTrainingTarget; },
|
||||
setTrainingTarget(t) { eocTrainingTarget = t; },
|
||||
get imlEoc() { return imlEoc; },
|
||||
};
|
||||
|
||||
// Start animation
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
|
@ -1814,6 +1840,17 @@ function onJoystickMove() {
|
|||
|
||||
syncRawParamsFromOutputs(outputs);
|
||||
|
||||
// EOC Linked mode: run EOC IML with same joystick inputs
|
||||
if (imlEoc && eocChain?.nispsMode === 'linked') {
|
||||
imlEoc.setInput(0, joyX);
|
||||
imlEoc.setInput(1, joyY);
|
||||
imlEoc.process();
|
||||
const eocOutputs = imlEoc.getOutputs();
|
||||
for (let i = 0; i < eocOutputs.length; i++) {
|
||||
eocChain.setParam(i, eocOutputs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Trail
|
||||
joyTrail.push({ x: joyX, y: joyY, t: Date.now() });
|
||||
if (joyTrail.length > 30) joyTrail.shift();
|
||||
|
|
@ -2313,33 +2350,65 @@ function updateUndoButton() {
|
|||
}
|
||||
|
||||
// ---- RL mode ----
|
||||
|
||||
/**
|
||||
* Return the IML instance that RL feedback should target.
|
||||
* In Linked mode the user may direct feedback to the EOC IML instead.
|
||||
*/
|
||||
function _rlTarget() {
|
||||
if (eocChain?.nispsMode === 'linked' && eocTrainingTarget === 'eoc' && imlEoc) {
|
||||
return imlEoc;
|
||||
}
|
||||
return iml;
|
||||
}
|
||||
|
||||
function onThumbsUp() {
|
||||
if (iml.isTraining) return;
|
||||
const target = _rlTarget();
|
||||
if (!target || target.isTraining) return;
|
||||
|
||||
pushUndoSnapshot();
|
||||
const inputs = getCurrentInputs();
|
||||
const outputs = [...rawParamValues];
|
||||
iml.addExample(inputs, outputs);
|
||||
target.addExample(inputs, outputs);
|
||||
|
||||
noiseLevel *= rlExplorationDecay;
|
||||
noiseLevel = Math.max(noiseLevel, 0.005);
|
||||
|
||||
flash('btn-thumbsup');
|
||||
updateNoiseRing();
|
||||
// Only run async training on the target; synth IML uses trainModelAsync, EOC IML trains directly
|
||||
if (target === iml) {
|
||||
trainModelAsync();
|
||||
} else {
|
||||
target.trainAsync(({ loss }) => {
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onThumbsDown() {
|
||||
const target = _rlTarget();
|
||||
if (!target) return;
|
||||
|
||||
pushUndoSnapshot();
|
||||
const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel;
|
||||
noiseLevel = Math.min(noiseLevel * 1.5, noiseCap);
|
||||
|
||||
iml.moveWeights(noiseLevel, spreadLevel);
|
||||
target.moveWeights(noiseLevel, spreadLevel);
|
||||
|
||||
// Re-route outputs from whichever IML was affected
|
||||
if (target === iml) {
|
||||
const outputs = iml.getOutputs();
|
||||
routeOutputs(outputs);
|
||||
updateHeatmap(outputs);
|
||||
syncRawParamsFromOutputs(outputs);
|
||||
} else if (imlEoc && eocChain?.nispsMode === 'linked') {
|
||||
imlEoc.process();
|
||||
const eocOutputs = imlEoc.getOutputs();
|
||||
for (let i = 0; i < eocOutputs.length; i++) {
|
||||
eocChain.setParam(i, eocOutputs[i]);
|
||||
}
|
||||
}
|
||||
updateStatus();
|
||||
updateNoiseRing();
|
||||
flash('btn-thumbsdown');
|
||||
|
|
@ -3448,6 +3517,34 @@ function flash(id) {
|
|||
setTimeout(() => el.classList.remove('flash'), 250);
|
||||
}
|
||||
|
||||
// ---- EOC Linked-mode IML helpers ----
|
||||
|
||||
/**
|
||||
* Create (or recreate) the EOC IML instance for Linked mode.
|
||||
* Uses a smaller network than the synth IML since EOC params are more independent.
|
||||
*/
|
||||
async function initEocIML() {
|
||||
if (imlEoc) { imlEoc.destroy(); imlEoc = null; }
|
||||
if (!eocChain || eocChain.paramCount === 0) return;
|
||||
imlEoc = await WasmIML.create(
|
||||
N_JOY_INPUTS,
|
||||
eocChain.paramCount,
|
||||
[16, 32], // smaller network — EOC params are more independent
|
||||
1000, 1.0, 0.00001,
|
||||
);
|
||||
imlEoc.randomiseWeights(spreadLevel);
|
||||
console.log(`[EOC] Linked IML created — ${eocChain.paramCount} outputs`);
|
||||
}
|
||||
|
||||
/** Destroy the EOC IML instance (e.g. when leaving Linked mode). */
|
||||
function destroyEocIML() {
|
||||
if (imlEoc) {
|
||||
imlEoc.destroy();
|
||||
imlEoc = null;
|
||||
console.log('[EOC] Linked IML destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Persistence ----
|
||||
function saveState() {
|
||||
try {
|
||||
|
|
@ -3474,6 +3571,7 @@ function saveState() {
|
|||
params: m.paramMeta.map((_, i) => m.getCurrentParamValue(i)),
|
||||
})) : [],
|
||||
eocNispsMode: eocChain ? eocChain.nispsMode : 'bypass',
|
||||
eocTrainingTarget,
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (e) {
|
||||
|
|
@ -3606,6 +3704,11 @@ async function loadState() {
|
|||
try { eocChain.nispsMode = state.eocNispsMode; } catch (_) { /* invalid mode in old save */ }
|
||||
}
|
||||
|
||||
// Restore EOC training target
|
||||
if (state.eocTrainingTarget === 'synth' || state.eocTrainingTarget === 'eoc') {
|
||||
eocTrainingTarget = state.eocTrainingTarget;
|
||||
}
|
||||
|
||||
console.log(`[NISPS] Restored ${state.features?.length || 0} joy examples, ${state.handFeatures?.length || 0} hand examples from storage`);
|
||||
} catch (e) {
|
||||
console.warn('[NISPS] Failed to load state:', e);
|
||||
|
|
|
|||
Loading…
Reference in a new issue