primitives.js (505 lines): All 8 sequencing primitives extending Primitive base class: - EuclideanRhythm: Bjorklund-distributed trigger patterns - ProbabilityGate: PRNG-based trigger filtering + accent assignment - PitchWalker: stateful constrained random walk with gravity - Ratchet: probabilistic step subdivision (1-4x) - SwingGroove: alternating-step timing offsets (max triplet feel) - DensityMorph: trigger placement with clustering control - IntervalLock: 11-scale pitch quantizer (chromatic→diminished) - VelocityShaper: 5 curve types with depth/phase control Includes PRIMITIVE_REGISTRY for chain builder UI. chain.js (319 lines): Sequential pipeline chain evaluator. Buckets primitives by category (generators→processors→converters→ timing), forks deterministic PRNG per primitive, merges multiple generators via additive (OR) or multiplicative (AND) mode (configurable in real time). Flat param distribution by chain position. Full state serialization for freeze support.
319 lines
10 KiB
JavaScript
319 lines
10 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|