feat(playground): EOC NISPS Linked mode — dual MLP from same controller (meml-vqg)
This commit is contained in:
parent
0a5f2544ca
commit
e8cb8e4195
1 changed files with 113 additions and 11 deletions
|
|
@ -95,6 +95,10 @@ let outputMode = 'visual';
|
||||||
let eocChain = null;
|
let eocChain = null;
|
||||||
let _eocInited = false; // guard: init once per AudioContext lifetime
|
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 ----
|
// ---- MIDI CC state ----
|
||||||
let midiOutput = null;
|
let midiOutput = null;
|
||||||
// Storage key scoped per engine so CC assignments don't bleed between engines.
|
// Storage key scoped per engine so CC assignments don't bleed between engines.
|
||||||
|
|
@ -1050,9 +1054,22 @@ async function init() {
|
||||||
EOCChainUI.init(eocChain, eocDrawerBody);
|
EOCChainUI.init(eocChain, eocDrawerBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log EOC structural changes; future NISPS modes will act on this event
|
// Log EOC structural changes; Linked mode acts on this event
|
||||||
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, paramCount:', eocChain.paramCount, reason ? `(${reason})` : '');
|
||||||
|
|
||||||
|
if (reason === 'nispsMode-changed') {
|
||||||
|
if (eocChain.nispsMode === 'linked') {
|
||||||
|
await initEocIML();
|
||||||
|
} else {
|
||||||
|
destroyEocIML();
|
||||||
|
eocTrainingTarget = 'synth';
|
||||||
|
}
|
||||||
|
} else if (eocChain.nispsMode === 'linked' && imlEoc) {
|
||||||
|
// Module added/removed: recreate EOC IML with updated param count
|
||||||
|
await initEocIML();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug probe — exposed on window when ?debug=1 is in the URL.
|
// Debug probe — exposed on window when ?debug=1 is in the URL.
|
||||||
|
|
@ -1107,6 +1124,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
|
// Start animation
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
@ -1773,6 +1798,17 @@ function onJoystickMove() {
|
||||||
|
|
||||||
syncRawParamsFromOutputs(outputs);
|
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
|
// Trail
|
||||||
joyTrail.push({ x: joyX, y: joyY, t: Date.now() });
|
joyTrail.push({ x: joyX, y: joyY, t: Date.now() });
|
||||||
if (joyTrail.length > 30) joyTrail.shift();
|
if (joyTrail.length > 30) joyTrail.shift();
|
||||||
|
|
@ -2256,33 +2292,65 @@ function updateUndoButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- RL mode ----
|
// ---- 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() {
|
function onThumbsUp() {
|
||||||
if (iml.isTraining) return;
|
const target = _rlTarget();
|
||||||
|
if (!target || target.isTraining) return;
|
||||||
|
|
||||||
pushUndoSnapshot();
|
pushUndoSnapshot();
|
||||||
const inputs = getCurrentInputs();
|
const inputs = getCurrentInputs();
|
||||||
const outputs = [...rawParamValues];
|
const outputs = [...rawParamValues];
|
||||||
iml.addExample(inputs, outputs);
|
target.addExample(inputs, outputs);
|
||||||
|
|
||||||
noiseLevel *= rlExplorationDecay;
|
noiseLevel *= rlExplorationDecay;
|
||||||
noiseLevel = Math.max(noiseLevel, 0.005);
|
noiseLevel = Math.max(noiseLevel, 0.005);
|
||||||
|
|
||||||
flash('btn-thumbsup');
|
flash('btn-thumbsup');
|
||||||
updateNoiseRing();
|
updateNoiseRing();
|
||||||
trainModelAsync();
|
// 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() {
|
function onThumbsDown() {
|
||||||
|
const target = _rlTarget();
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
pushUndoSnapshot();
|
pushUndoSnapshot();
|
||||||
const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel;
|
const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel;
|
||||||
noiseLevel = Math.min(noiseLevel * 1.5, noiseCap);
|
noiseLevel = Math.min(noiseLevel * 1.5, noiseCap);
|
||||||
|
|
||||||
iml.moveWeights(noiseLevel, spreadLevel);
|
target.moveWeights(noiseLevel, spreadLevel);
|
||||||
|
|
||||||
const outputs = iml.getOutputs();
|
// Re-route outputs from whichever IML was affected
|
||||||
routeOutputs(outputs);
|
if (target === iml) {
|
||||||
updateHeatmap(outputs);
|
const outputs = iml.getOutputs();
|
||||||
syncRawParamsFromOutputs(outputs);
|
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();
|
updateStatus();
|
||||||
updateNoiseRing();
|
updateNoiseRing();
|
||||||
flash('btn-thumbsdown');
|
flash('btn-thumbsdown');
|
||||||
|
|
@ -3391,6 +3459,34 @@ function flash(id) {
|
||||||
setTimeout(() => el.classList.remove('flash'), 250);
|
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 ----
|
// ---- Persistence ----
|
||||||
function saveState() {
|
function saveState() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3417,6 +3513,7 @@ function saveState() {
|
||||||
params: m.paramMeta.map((_, i) => m.getCurrentParamValue(i)),
|
params: m.paramMeta.map((_, i) => m.getCurrentParamValue(i)),
|
||||||
})) : [],
|
})) : [],
|
||||||
eocNispsMode: eocChain ? eocChain.nispsMode : 'bypass',
|
eocNispsMode: eocChain ? eocChain.nispsMode : 'bypass',
|
||||||
|
eocTrainingTarget,
|
||||||
};
|
};
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -3549,6 +3646,11 @@ async function loadState() {
|
||||||
try { eocChain.nispsMode = state.eocNispsMode; } catch (_) { /* invalid mode in old save */ }
|
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`);
|
console.log(`[NISPS] Restored ${state.features?.length || 0} joy examples, ${state.handFeatures?.length || 0} hand examples from storage`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[NISPS] Failed to load state:', e);
|
console.warn('[NISPS] Failed to load state:', e);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue