memlnaut-nisps/playground/js/shapeseq/session.js
w1n5t0n 9ebfec8814 feat(shapeseq): session serialization + preset integration
Add serializeShapeSeqState/restoreShapeSeqState for complete ShapeSeq
state capture (chain config, primitive states, projection knobs, voice
mode, MLP weights, freeze state, clock config). SessionPresetManager
lazily imports the session module when a ShapeSeq engine is registered,
including ShapeSeq state in save/load without a hard dependency.
2026-04-07 00:01:12 +01:00

116 lines
3.3 KiB
JavaScript

/**
* ShapeSeq session state serialization.
*
* Captures and restores the complete ShapeSeq state:
* - Chain configuration (which primitives, in what order)
* - Primitive states (e.g., PitchWalker position)
* - Generator combine mode
* - Projection config (3 knobs)
* - Voice mode (mono/poly)
* - Sequence MLP weights (if in dual mode)
* - Freeze state (mode, frozen params, live flags, seeds)
* - Clock config (BPM, step count)
*
* @module shapeseq/session
*/
import { PRIMITIVE_REGISTRY } from './primitives.js';
/**
* Serialize ShapeSeq engine state to a plain object (JSON-safe).
* @param {import('./sequencer.js').ShapeSeqEngine} engine
* @returns {Object}
*/
export function serializeShapeSeqState(engine) {
const chain = engine.getChain();
const prims = chain.getPrimitives();
return {
version: 1,
chain: {
primitives: prims.map(p => ({
name: p.name,
})),
states: chain.getState(),
generatorCombineMode: chain.generatorCombineMode,
},
projection: engine.getProjection(),
voiceMode: engine.voiceMode,
clock: {
bpm: engine.getClock().bpm,
},
// Sequence MLP weights (for dual mode restoration)
sequenceWeights: engine.getSequenceIML()?.getWeights?.() ?? null,
// Freeze state
freeze: serializeFreezeState(engine.freezeManager),
};
}
/**
* Serialize freeze manager state to a JSON-safe object.
* Returns null when not frozen.
*
* @param {import('./freeze.js').FreezeManager} fm
* @returns {Object|null}
*/
function serializeFreezeState(fm) {
if (!fm.isFrozen) return null;
return {
mode: fm.freezeMode,
frozenParams: fm.getFrozenParams() ? Array.from(fm.getFrozenParams()) : null,
liveFlags: fm.getLiveFlags() ? Array.from(fm.getLiveFlags()) : null,
seeds: fm.getFrozenSeeds(),
states: fm.getFrozenStates(),
masterSeed: fm.getMasterSeed(),
// For pattern mode:
frozenPattern: fm.getFrozenPattern() ?? null,
};
}
/**
* Restore ShapeSeq engine state from a serialized object.
* @param {import('./sequencer.js').ShapeSeqEngine} engine
* @param {Object} state - from serializeShapeSeqState
*/
export function restoreShapeSeqState(engine, state) {
if (!state || state.version !== 1) return;
const chain = engine.getChain();
// Rebuild chain from primitive names
if (state.chain?.primitives) {
// Clear existing
const existing = chain.getPrimitives();
for (let i = existing.length - 1; i >= 0; i--) {
chain.removePrimitive(i);
}
// Add from state
for (const p of state.chain.primitives) {
const Ctor = PRIMITIVE_REGISTRY[p.name];
if (Ctor) chain.addPrimitive(new Ctor());
}
// Restore states
if (state.chain.states) chain.setState(state.chain.states);
if (state.chain.generatorCombineMode) {
chain.generatorCombineMode = state.chain.generatorCombineMode;
}
}
// Projection
if (state.projection) engine.setProjection(state.projection);
// Voice mode
if (state.voiceMode) engine.setVoiceMode(state.voiceMode);
// Clock
if (state.clock?.bpm) engine.setTempo(state.clock.bpm);
// Sequence MLP weights
if (state.sequenceWeights) {
const iml = engine.getSequenceIML();
if (iml?.setWeights) iml.setWeights(state.sequenceWeights);
}
// Freeze state restoration is complex — skip for now, just ensure unfrozen on load
// TODO: restore freeze state properly
}