memlnaut-nisps/playground/js/shapeseq/freeze.js

247 lines
7.9 KiB
JavaScript
Raw Normal View History

/**
* ShapeSeq FreezeManager freeze-as-algorithm and freeze-as-pattern
*
* Two freeze modes:
*
* - **algorithm**: Captures all primitive param values + PRNG seeds.
* Frozen params hold their captured values; individual params can
* be selectively re-exposed as "live" for ML-driven exploration.
*
* - **pattern**: Captures the realized note pattern (the actual step
* events for one full loop). The sequencer bypasses the primitive
* chain entirely and just loops the frozen pattern.
*
* Delta math (proper delta+boundary logic) is a separate issue (meml-7goy).
* This module provides the simple version: frozen params use captured values,
* live params pass through current ML values.
*
* @module shapeseq/freeze
*/
import { clonePattern } from './pattern.js';
export class FreezeManager {
constructor() {
/** @private */ this._frozen = false;
/** @private @type {'algorithm'|'pattern'|null} */ this._freezeMode = null;
/** @private @type {Float32Array|null} */ this._frozenParams = null;
/** @private @type {Array<number>|null} */ this._frozenSeeds = null;
/** @private @type {Array<Object>|null} */ this._frozenStates = null;
/** @private @type {Uint8Array|null} */ this._liveFlags = null;
/** @private @type {number|null} */ this._masterSeed = null;
/** @private @type {Object|null} */ this._frozenPattern = null;
}
// ── Freeze / unfreeze ───────────────────────────────────────────────
/**
* Capture a full snapshot of the current state.
*
* In **algorithm** mode (default): captures params, seeds, and states.
* All params start frozen (liveFlags = 0); use toggleParam() or
* setParamLive() to selectively re-expose params for ML control.
*
* In **pattern** mode: captures the realized note pattern via deep clone.
* The primitive chain is bypassed entirely the sequencer just loops
* the frozen pattern.
*
* @param {import('./chain.js').Chain|null} chain
* @param {Float32Array|Array<number>|null} currentParams - flat param array
* @param {number|null} masterSeed
* @param {'algorithm'|'pattern'} [mode='algorithm']
* @param {Object|null} [currentPattern=null] - required when mode is 'pattern'
*/
freeze(chain, currentParams, masterSeed, mode = 'algorithm', currentPattern = null) {
this._freezeMode = mode;
if (mode === 'pattern') {
// Pattern mode: capture the realized pattern, bypass chain
if (!currentPattern) {
throw new Error('freeze: pattern mode requires a currentPattern');
}
this._frozenPattern = clonePattern(currentPattern);
// Clear algorithm-mode state
this._frozenParams = null;
this._frozenSeeds = null;
this._frozenStates = null;
this._liveFlags = null;
this._masterSeed = null;
} else {
// Algorithm mode: existing behavior
// Copy param values
this._frozenParams = new Float32Array(currentParams.length);
for (let i = 0; i < currentParams.length; i++) {
this._frozenParams[i] = currentParams[i];
}
// Capture per-primitive seeds
this._frozenSeeds = chain.getPrimitives().map(p => p.getSeed());
// Capture per-primitive states
this._frozenStates = chain.getState();
// Store master seed
this._masterSeed = masterSeed;
// All params frozen by default
this._liveFlags = new Uint8Array(currentParams.length);
// Clear pattern-mode state
this._frozenPattern = null;
}
this._frozen = true;
}
/**
* Clear all captured state and return to unfrozen mode.
*/
unfreeze() {
this._frozen = false;
this._freezeMode = null;
this._frozenParams = null;
this._frozenSeeds = null;
this._frozenStates = null;
this._liveFlags = null;
this._masterSeed = null;
this._frozenPattern = null;
}
// ── Per-param live/frozen control ──────────────────────────────────
/**
* Toggle a param between frozen (0) and live (1).
* Only valid when frozen; throws if not frozen.
*
* @param {number} flatIndex - index into the flat param array
*/
toggleParam(flatIndex) {
if (!this._frozen) {
throw new Error('toggleParam: cannot toggle when not frozen');
}
const idx = flatIndex | 0;
if (idx < 0 || idx >= this._liveFlags.length) {
throw new RangeError('toggleParam: index ' + flatIndex + ' out of range [0, ' + (this._liveFlags.length - 1) + ']');
}
this._liveFlags[idx] = this._liveFlags[idx] ? 0 : 1;
}
/**
* Explicitly set a param's live state.
* Only valid when frozen; throws if not frozen.
*
* @param {number} flatIndex - index into the flat param array
* @param {boolean} isLive - true = live (receives ML values), false = frozen
*/
setParamLive(flatIndex, isLive) {
if (!this._frozen) {
throw new Error('setParamLive: cannot set when not frozen');
}
const idx = flatIndex | 0;
if (idx < 0 || idx >= this._liveFlags.length) {
throw new RangeError('setParamLive: index ' + flatIndex + ' out of range [0, ' + (this._liveFlags.length - 1) + ']');
}
this._liveFlags[idx] = isLive ? 1 : 0;
}
// ── Getters ────────────────────────────────────────────────────────
/** @returns {boolean} */
get isFrozen() {
return this._frozen;
}
/**
* Returns the current freeze mode, or null if not frozen.
* @returns {'algorithm'|'pattern'|null}
*/
get freezeMode() {
return this._freezeMode;
}
/**
* Returns the captured pattern (pattern mode only), or null.
* @returns {Object|null}
*/
getFrozenPattern() {
return this._frozenPattern;
}
/**
* Returns the captured param values, or null if not frozen.
* @returns {Float32Array|null}
*/
getFrozenParams() {
return this._frozenParams;
}
/**
* Returns the live flags array, or null if not frozen.
* @returns {Uint8Array|null}
*/
getLiveFlags() {
return this._liveFlags;
}
/**
* Returns the captured per-primitive PRNG seeds, or null if not frozen.
* @returns {Array<number>|null}
*/
getFrozenSeeds() {
return this._frozenSeeds;
}
/**
* Returns the captured per-primitive states, or null if not frozen.
* @returns {Array<Object>|null}
*/
getFrozenStates() {
return this._frozenStates;
}
/**
* Returns the captured master seed, or null if not frozen.
* @returns {number|null}
*/
getMasterSeed() {
return this._masterSeed;
}
// ── Effective params ───────────────────────────────────────────────
/**
* Given current MLP-derived params, return the effective param array:
* frozen params use captured values, live params use currentMLParams.
*
* This is the simple version. The delta controller (meml-7goy) will
* replace this with proper delta+boundary logic later.
*
* Returns null if not frozen.
*
* @param {Float32Array|Array<number>} currentMLParams
* @returns {Float32Array|null}
*/
getEffectiveParams(currentMLParams) {
if (!this._frozen) return null;
if (this._freezeMode === 'pattern') return null;
const result = new Float32Array(this._frozenParams.length);
for (let i = 0; i < result.length; i++) {
result[i] = this._liveFlags[i] ? currentMLParams[i] : this._frozenParams[i];
}
return result;
}
// ── Sequencer integration ──────────────────────────────────────────
/**
* Returns true when frozen freeze-as-algorithm suppresses per-loop
* re-evaluation. The sequencer's _handleLoopStart should check this.
*
* @returns {boolean}
*/
shouldSuppressReEval() {
return this._frozen;
}
}