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

320 lines
10 KiB
JavaScript
Raw Normal View History

/**
* ShapeSeq Chain sequential pipeline runner with generator combination modes
*
* Evaluates an ordered list of primitives as a sequential pipeline:
* 1. Generators run first, combined via additive or multiplicative merge
* 2. Processors transform the pattern in chain order
* 3. Converters run in chain order
* 4. Timing modifiers annotate last
*
* Params are distributed flat across primitives in chain order.
* Each primitive gets a deterministic PRNG stream via fork(masterPRNG, index).
*
* Port-ready: explicit state, typed arrays, no closures in hot path.
*
* @module shapeseq/chain
*/
import { createPattern, mergePatterns } from './pattern.js';
import { createPRNG, fork } from './prng.js';
// ── Category execution order ────────────────────────────────────────
const PHASE_ORDER = ['generator', 'processor', 'converter', 'timing'];
// ── Chain class ─────────────────────────────────────────────────────
export class Chain {
constructor() {
/** @private @type {Array<import('./primitive.js').Primitive>} */
this._primitives = [];
/** @type {'additive'|'multiplicative'} */
this.generatorCombineMode = 'additive';
/** @private @type {number} */
this._masterSeed = 0;
/**
* Per-primitive state objects, indexed by position in chain.
* Populated after evaluate() calls; used for freeze support.
* @private @type {Array<Object>}
*/
this._primitiveStates = [];
}
// ── Primitive management ────────────────────────────────────────
/**
* Append a primitive to the end of the chain.
* @param {import('./primitive.js').Primitive} primitive
*/
addPrimitive(primitive) {
this._primitives.push(primitive);
this._primitiveStates.push(primitive.getState());
}
/**
* Remove the primitive at the given index.
* @param {number} index
*/
removePrimitive(index) {
const idx = index | 0;
if (idx < 0 || idx >= this._primitives.length) {
throw new RangeError('removePrimitive: index ' + index + ' out of range [0, ' + (this._primitives.length - 1) + ']');
}
this._primitives.splice(idx, 1);
this._primitiveStates.splice(idx, 1);
}
/**
* Insert a primitive at the given index, shifting others right.
* @param {number} index
* @param {import('./primitive.js').Primitive} primitive
*/
insertPrimitive(index, primitive) {
const idx = index | 0;
if (idx < 0 || idx > this._primitives.length) {
throw new RangeError('insertPrimitive: index ' + index + ' out of range [0, ' + this._primitives.length + ']');
}
this._primitives.splice(idx, 0, primitive);
this._primitiveStates.splice(idx, 0, primitive.getState());
}
/**
* Move a primitive from one position to another.
* @param {number} fromIndex
* @param {number} toIndex
*/
movePrimitive(fromIndex, toIndex) {
const from = fromIndex | 0;
const to = toIndex | 0;
const len = this._primitives.length;
if (from < 0 || from >= len) {
throw new RangeError('movePrimitive: fromIndex ' + fromIndex + ' out of range [0, ' + (len - 1) + ']');
}
if (to < 0 || to >= len) {
throw new RangeError('movePrimitive: toIndex ' + toIndex + ' out of range [0, ' + (len - 1) + ']');
}
const [prim] = this._primitives.splice(from, 1);
const [state] = this._primitiveStates.splice(from, 1);
this._primitives.splice(to, 0, prim);
this._primitiveStates.splice(to, 0, state);
}
/**
* Get the current list of primitives (shallow copy).
* @returns {Array<import('./primitive.js').Primitive>}
*/
getPrimitives() {
return this._primitives.slice();
}
// ── Configuration ───────────────────────────────────────────────
/**
* Total parameter count across all primitives in the chain.
* @returns {number}
*/
get totalParamCount() {
let total = 0;
for (let i = 0; i < this._primitives.length; i++) {
total += this._primitives[i].paramCount;
}
return total;
}
/**
* Get a flat list of all param schemas across all primitives,
* annotated with their primitive and param indices.
*
* @returns {Array<{ primitiveIndex: number, paramIndex: number, schema: Object }>}
*/
getParamSchemas() {
const result = [];
for (let pi = 0; pi < this._primitives.length; pi++) {
const prim = this._primitives[pi];
const schema = prim.paramSchema;
for (let si = 0; si < schema.length; si++) {
result.push({
primitiveIndex: pi,
paramIndex: si,
schema: schema[si],
});
}
}
return result;
}
// ── Evaluation ──────────────────────────────────────────────────
/**
* Evaluate the chain, producing a pattern description.
*
* Pipeline order:
* 1. Generators combined via generatorCombineMode
* 2. Processors sequential transform
* 3. Converters sequential transform
* 4. Timing modifiers annotate last
*
* @param {Float32Array|Array<number>} params - flat param array distributed across primitives
* @param {number} stepCount - number of steps in the output pattern
* @param {number} masterSeed - seed for the master PRNG
* @returns {{ steps: Array, stepCount: number, metadata: Object }}
*/
evaluate(params, stepCount, masterSeed) {
const primitives = this._primitives;
const primCount = primitives.length;
// Create master PRNG from seed
const masterPRNG = createPRNG(masterSeed >>> 0);
// ── Bucket primitives by category, preserving chain order ──
/** @type {Array<{ index: number, prim: Object }>} */
const generators = [];
const processors = [];
const converters = [];
const timingMods = [];
for (let i = 0; i < primCount; i++) {
const entry = { index: i, prim: primitives[i] };
switch (primitives[i].category) {
case 'generator': generators.push(entry); break;
case 'processor': processors.push(entry); break;
case 'converter': converters.push(entry); break;
case 'timing': timingMods.push(entry); break;
}
}
// ── Compute param offsets per primitive ──
const paramOffsets = new Array(primCount);
let offset = 0;
for (let i = 0; i < primCount; i++) {
paramOffsets[i] = offset;
offset += primitives[i].paramCount;
}
// ── Helper: run a single primitive ──
const self = this;
function runPrimitive(entry, inputPattern) {
const idx = entry.index;
const prim = entry.prim;
const pOffset = paramOffsets[idx];
const pCount = prim.paramCount;
// Slice params for this primitive
const primParams = new Float32Array(pCount);
for (let p = 0; p < pCount; p++) {
primParams[p] = pOffset + p < params.length ? +params[pOffset + p] : prim.paramSchema[p].default;
}
// Fork a deterministic PRNG for this primitive
const primRNG = fork(masterPRNG, idx);
// Get current state
const state = self._primitiveStates[idx] || prim.getState();
// Process
const result = prim.process(primParams, inputPattern, state, primRNG);
// Store updated state
self._primitiveStates[idx] = result.nextState;
return result.patternDesc;
}
// ── Phase 1: Generators ──
let pattern;
if (generators.length === 0) {
// Default pattern: all steps triggered
pattern = createPattern(stepCount);
for (let i = 0; i < stepCount; i++) {
pattern.steps[i].trigger = true;
}
} else if (generators.length === 1) {
// Single generator — no merge needed
pattern = runPrimitive(generators[0], createPattern(stepCount));
} else {
// Multiple generators — run each, then merge
let merged = runPrimitive(generators[0], createPattern(stepCount));
for (let g = 1; g < generators.length; g++) {
const next = runPrimitive(generators[g], createPattern(stepCount));
merged = mergePatterns(merged, next, this.generatorCombineMode);
}
pattern = merged;
}
// ── Phase 2: Processors ──
for (let i = 0; i < processors.length; i++) {
pattern = runPrimitive(processors[i], pattern);
}
// ── Phase 3: Converters ──
for (let i = 0; i < converters.length; i++) {
pattern = runPrimitive(converters[i], pattern);
}
// ── Phase 4: Timing modifiers ──
for (let i = 0; i < timingMods.length; i++) {
pattern = runPrimitive(timingMods[i], pattern);
}
return pattern;
}
// ── State management (for freeze) ──────────────────────────────
/**
* Get serializable state for all primitives in the chain.
* @returns {Array<Object>}
*/
getState() {
const states = new Array(this._primitives.length);
for (let i = 0; i < this._primitives.length; i++) {
states[i] = this._primitiveStates[i] || this._primitives[i].getState();
}
return states;
}
/**
* Restore all primitive states from a previously serialized state array.
* @param {Array<Object>} states
*/
setState(states) {
if (!Array.isArray(states)) {
throw new TypeError('setState expects an array of state objects');
}
const len = Math.min(states.length, this._primitives.length);
for (let i = 0; i < len; i++) {
this._primitives[i].setState(states[i]);
this._primitiveStates[i] = states[i];
}
}
/**
* Get the master PRNG seed.
* @returns {number}
*/
getMasterSeed() {
return this._masterSeed;
}
/**
* Set the master PRNG seed.
* @param {number} seed - 32-bit integer seed
*/
setMasterSeed(seed) {
this._masterSeed = seed >>> 0;
}
}