feat(shapeseq): implement Layer 0 foundation modules
Five independent modules with no interdependencies: - event-bus.js: namespaced pub/sub (seq.*/ml.*/ui.*) with wildcard subscriptions and automatic AudioContext/performance.now timestamps - prng.js: seedable mulberry32 PRNG with pure-functional API, fork() for independent per-primitive streams, serializable state for freeze - pattern.js: symbolic pattern description data structure with create/clone/merge/validate, additive and multiplicative merge modes - param-map.js: maps fixed 16-output MLP to variable-count primitive params via linear interpolation, with optional per-param min/max scaling - seq-iml.js: factory for second WasmIML instance (2 inputs, 16 outputs, [16,16,16] hidden layers) following existing imlJoy/imlHand pattern All modules are ES modules with no build step. PRNG, pattern, and param-map are port-ready (explicit state, typed arrays, no closures).
This commit is contained in:
parent
ddb6b77abb
commit
e3b94d644f
5 changed files with 638 additions and 0 deletions
117
playground/js/shapeseq/event-bus.js
Normal file
117
playground/js/shapeseq/event-bus.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// ShapeSeq Namespaced Event Bus
|
||||||
|
// Lightweight pub/sub with seq.*/ml.*/ui.* namespaces and wildcard support.
|
||||||
|
|
||||||
|
// ── Event name constants ────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const SEQ = Object.freeze({
|
||||||
|
STEP: 'seq.step',
|
||||||
|
NOTE_ON: 'seq.noteOn',
|
||||||
|
NOTE_OFF: 'seq.noteOff',
|
||||||
|
PARAM_CHANGE: 'seq.paramChange',
|
||||||
|
LOOP_START: 'seq.loopStart',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ML = Object.freeze({
|
||||||
|
TRAINED: 'ml.trained',
|
||||||
|
FROZEN: 'ml.frozen',
|
||||||
|
UNFROZEN: 'ml.unfrozen',
|
||||||
|
DELTA_UPDATE: 'ml.deltaUpdate',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UI = Object.freeze({
|
||||||
|
PARAM_SELECT: 'ui.paramSelect',
|
||||||
|
CHAIN_EDIT: 'ui.chainEdit',
|
||||||
|
PRESET_LOAD: 'ui.presetLoad',
|
||||||
|
FREEZE_TOGGLE: 'ui.freezeToggle',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Bus implementation ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export class EventBus {
|
||||||
|
/** @param {AudioContext} [audioCtx] - optional, used for seq.* timestamps */
|
||||||
|
constructor(audioCtx) {
|
||||||
|
this._listeners = new Map(); // event -> Set<callback>
|
||||||
|
this._wildcards = new Map(); // namespace prefix (e.g. "seq") -> Set<callback>
|
||||||
|
this._audioCtx = audioCtx ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provide or replace the AudioContext used for seq.* timestamps. */
|
||||||
|
setAudioContext(ctx) {
|
||||||
|
this._audioCtx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to an event or a wildcard namespace.
|
||||||
|
* on('seq.step', cb) — exact match
|
||||||
|
* on('seq.*', cb) — all events in the seq namespace
|
||||||
|
*/
|
||||||
|
on(event, callback) {
|
||||||
|
if (event.endsWith('.*')) {
|
||||||
|
const ns = event.slice(0, -2); // "seq.*" -> "seq"
|
||||||
|
if (!this._wildcards.has(ns)) this._wildcards.set(ns, new Set());
|
||||||
|
this._wildcards.get(ns).add(callback);
|
||||||
|
} else {
|
||||||
|
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
||||||
|
this._listeners.get(event).add(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsubscribe. Mirrors the on() signature. */
|
||||||
|
off(event, callback) {
|
||||||
|
if (event.endsWith('.*')) {
|
||||||
|
const ns = event.slice(0, -2);
|
||||||
|
const set = this._wildcards.get(ns);
|
||||||
|
if (set) {
|
||||||
|
set.delete(callback);
|
||||||
|
if (set.size === 0) this._wildcards.delete(ns);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const set = this._listeners.get(event);
|
||||||
|
if (set) {
|
||||||
|
set.delete(callback);
|
||||||
|
if (set.size === 0) this._listeners.delete(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event.
|
||||||
|
* A `timestamp` field is added automatically:
|
||||||
|
* - seq.* events use AudioContext.currentTime (seconds)
|
||||||
|
* - all others use performance.now() (milliseconds)
|
||||||
|
*/
|
||||||
|
emit(event, data = {}) {
|
||||||
|
const ns = event.split('.')[0];
|
||||||
|
const stamped = Object.assign({ timestamp: this._stamp(ns) }, data);
|
||||||
|
|
||||||
|
// Exact listeners
|
||||||
|
const exact = this._listeners.get(event);
|
||||||
|
if (exact) {
|
||||||
|
for (const cb of exact) cb(stamped, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard listeners for this namespace
|
||||||
|
const wild = this._wildcards.get(ns);
|
||||||
|
if (wild) {
|
||||||
|
for (const cb of wild) cb(stamped, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── private ──
|
||||||
|
|
||||||
|
_stamp(ns) {
|
||||||
|
if (ns === 'seq' && this._audioCtx) return this._audioCtx.currentTime;
|
||||||
|
return performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Singleton convenience ───────────────────────────────────────────
|
||||||
|
|
||||||
|
let _default = null;
|
||||||
|
|
||||||
|
/** Return (and lazily create) the shared default bus. */
|
||||||
|
export function getDefaultBus(audioCtx) {
|
||||||
|
if (!_default) _default = new EventBus(audioCtx);
|
||||||
|
else if (audioCtx) _default.setAudioContext(audioCtx);
|
||||||
|
return _default;
|
||||||
|
}
|
||||||
94
playground/js/shapeseq/param-map.js
Normal file
94
playground/js/shapeseq/param-map.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
// ShapeSeq param mapping layer
|
||||||
|
// Maps fixed-size MLP outputs (16 values [0,1]) to variable-count primitive params
|
||||||
|
//
|
||||||
|
// The sequence MLP always outputs a fixed number of values (default 16).
|
||||||
|
// The primitive chain has a variable number of params depending on which
|
||||||
|
// primitives are active. This module bridges the two via automatic distribution:
|
||||||
|
// - N <= mlpOutputCount: each param gets one dedicated output
|
||||||
|
// - N > mlpOutputCount: outputs distributed via linear interpolation
|
||||||
|
//
|
||||||
|
// Port-ready: Float32Array, no closures, pure functions.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a param mapper instance for a given MLP output count.
|
||||||
|
* @param {number} mlpOutputCount - number of MLP outputs (e.g. 16)
|
||||||
|
* @returns {{ map: Function, mapWithSchema: Function, mlpOutputCount: number }}
|
||||||
|
*/
|
||||||
|
export function createParamMap(mlpOutputCount) {
|
||||||
|
if (!Number.isInteger(mlpOutputCount) || mlpOutputCount < 1) {
|
||||||
|
throw new Error('mlpOutputCount must be a positive integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mlpOutputCount,
|
||||||
|
map,
|
||||||
|
mapWithSchema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map MLP outputs to N primitive params via automatic distribution.
|
||||||
|
*
|
||||||
|
* If paramCount <= mlpOutputCount, each param gets one dedicated output
|
||||||
|
* (first paramCount outputs used, rest ignored).
|
||||||
|
*
|
||||||
|
* If paramCount > mlpOutputCount, outputs are distributed via linear
|
||||||
|
* interpolation so that the first param maps to the first output and the
|
||||||
|
* last param maps to the last output, with intermediate params interpolated.
|
||||||
|
*
|
||||||
|
* @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1]
|
||||||
|
* @param {number} paramCount - number of primitive params to produce
|
||||||
|
* @returns {Float32Array} mapped values [0,1], length = paramCount
|
||||||
|
*/
|
||||||
|
export function map(mlpOutputs, paramCount) {
|
||||||
|
const mlpCount = mlpOutputs.length;
|
||||||
|
const result = new Float32Array(paramCount);
|
||||||
|
|
||||||
|
if (paramCount === 0) return result;
|
||||||
|
|
||||||
|
if (paramCount <= mlpCount) {
|
||||||
|
// Direct mapping: each param gets one dedicated output
|
||||||
|
for (let i = 0; i < paramCount; i++) {
|
||||||
|
result[i] = mlpOutputs[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Interpolated mapping: spread mlpCount outputs across paramCount params
|
||||||
|
// param[i] maps to a fractional position in the output array
|
||||||
|
// param[0] -> output[0], param[paramCount-1] -> output[mlpCount-1]
|
||||||
|
const scale = paramCount > 1 ? (mlpCount - 1) / (paramCount - 1) : 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < paramCount; i++) {
|
||||||
|
const pos = i * scale;
|
||||||
|
const lo = pos | 0; // floor
|
||||||
|
const hi = lo + 1 < mlpCount ? lo + 1 : lo;
|
||||||
|
const frac = pos - lo;
|
||||||
|
result[i] = mlpOutputs[lo] + (mlpOutputs[hi] - mlpOutputs[lo]) * frac;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map MLP outputs to primitive params and apply per-param min/max scaling.
|
||||||
|
*
|
||||||
|
* Each param schema defines { min, max } (both [0,1]). The mapped [0,1]
|
||||||
|
* value is scaled into [min, max] for each param.
|
||||||
|
*
|
||||||
|
* @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1]
|
||||||
|
* @param {Array<{ min: number, max: number }>} paramSchemas - per-param range definitions
|
||||||
|
* @returns {Float32Array} scaled values, length = paramSchemas.length
|
||||||
|
*/
|
||||||
|
export function mapWithSchema(mlpOutputs, paramSchemas) {
|
||||||
|
const paramCount = paramSchemas.length;
|
||||||
|
const mapped = map(mlpOutputs, paramCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < paramCount; i++) {
|
||||||
|
const schema = paramSchemas[i];
|
||||||
|
const min = schema.min !== undefined ? schema.min : 0;
|
||||||
|
const max = schema.max !== undefined ? schema.max : 1;
|
||||||
|
mapped[i] = min + mapped[i] * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
214
playground/js/shapeseq/pattern.js
Normal file
214
playground/js/shapeseq/pattern.js
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* ShapeSeq Pattern Description Data Structure
|
||||||
|
*
|
||||||
|
* The foundational data type for the ShapeSeq sequencing system.
|
||||||
|
* A pattern description is the symbolic output of the primitive chain --
|
||||||
|
* a complete loop description that the clock engine steps through.
|
||||||
|
*
|
||||||
|
* Port-ready: typed arrays where possible, explicit construction, no closures.
|
||||||
|
*
|
||||||
|
* @module shapeseq/pattern
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Step defaults ---
|
||||||
|
|
||||||
|
const DEFAULT_TRIGGER = false;
|
||||||
|
const DEFAULT_PITCH = 0.5;
|
||||||
|
const DEFAULT_VELOCITY = 0.7;
|
||||||
|
const DEFAULT_ACCENT = false;
|
||||||
|
const DEFAULT_TIME_OFFSET = 0.0;
|
||||||
|
const DEFAULT_SUBDIVISIONS = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single step with default values.
|
||||||
|
*
|
||||||
|
* @returns {{ trigger: boolean, pitch: number, velocity: number, accent: boolean, timeOffset: number, subdivisions: number }}
|
||||||
|
*/
|
||||||
|
export function createStep() {
|
||||||
|
return {
|
||||||
|
trigger: DEFAULT_TRIGGER,
|
||||||
|
pitch: DEFAULT_PITCH,
|
||||||
|
velocity: DEFAULT_VELOCITY,
|
||||||
|
accent: DEFAULT_ACCENT,
|
||||||
|
timeOffset: DEFAULT_TIME_OFFSET,
|
||||||
|
subdivisions: DEFAULT_SUBDIVISIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pattern description with the given step count, all steps at defaults.
|
||||||
|
*
|
||||||
|
* @param {number} stepCount - Number of steps (must be positive integer)
|
||||||
|
* @returns {{ steps: Array, stepCount: number, metadata: Object }}
|
||||||
|
*/
|
||||||
|
export function createPattern(stepCount) {
|
||||||
|
const count = stepCount | 0; // coerce to int
|
||||||
|
if (count < 1) {
|
||||||
|
throw new RangeError('stepCount must be >= 1, got ' + stepCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = new Array(count);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
steps[i] = createStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: steps,
|
||||||
|
stepCount: count,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep clone a pattern description.
|
||||||
|
* Needed for chain processing where each primitive transforms a copy.
|
||||||
|
*
|
||||||
|
* @param {{ steps: Array, stepCount: number, metadata: Object }} pattern
|
||||||
|
* @returns {{ steps: Array, stepCount: number, metadata: Object }}
|
||||||
|
*/
|
||||||
|
export function clonePattern(pattern) {
|
||||||
|
const count = pattern.stepCount;
|
||||||
|
const srcSteps = pattern.steps;
|
||||||
|
const steps = new Array(count);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const s = srcSteps[i];
|
||||||
|
steps[i] = {
|
||||||
|
trigger: s.trigger,
|
||||||
|
pitch: s.pitch,
|
||||||
|
velocity: s.velocity,
|
||||||
|
accent: s.accent,
|
||||||
|
timeOffset: s.timeOffset,
|
||||||
|
subdivisions: s.subdivisions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shallow clone metadata (one level deep for plain-object metadata)
|
||||||
|
const srcMeta = pattern.metadata;
|
||||||
|
const metadata = {};
|
||||||
|
const keys = Object.keys(srcMeta);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
metadata[keys[i]] = srcMeta[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: steps,
|
||||||
|
stepCount: count,
|
||||||
|
metadata: metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two patterns together.
|
||||||
|
*
|
||||||
|
* Both patterns must have the same stepCount.
|
||||||
|
* Returns a new pattern (does not mutate inputs).
|
||||||
|
*
|
||||||
|
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternA
|
||||||
|
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternB
|
||||||
|
* @param {'additive'|'multiplicative'} mode
|
||||||
|
* - 'additive': triggers OR'd, pitch/velocity averaged
|
||||||
|
* - 'multiplicative': triggers AND'd, pitch/velocity averaged
|
||||||
|
* @returns {{ steps: Array, stepCount: number, metadata: Object }}
|
||||||
|
*/
|
||||||
|
export function mergePatterns(patternA, patternB, mode) {
|
||||||
|
if (patternA.stepCount !== patternB.stepCount) {
|
||||||
|
throw new RangeError(
|
||||||
|
'Cannot merge patterns with different step counts: ' +
|
||||||
|
patternA.stepCount + ' vs ' + patternB.stepCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mode !== 'additive' && mode !== 'multiplicative') {
|
||||||
|
throw new TypeError("mode must be 'additive' or 'multiplicative', got '" + mode + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = patternA.stepCount;
|
||||||
|
const stepsA = patternA.steps;
|
||||||
|
const stepsB = patternB.steps;
|
||||||
|
const steps = new Array(count);
|
||||||
|
const isAdditive = mode === 'additive';
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const a = stepsA[i];
|
||||||
|
const b = stepsB[i];
|
||||||
|
|
||||||
|
const trigger = isAdditive ? (a.trigger || b.trigger) : (a.trigger && b.trigger);
|
||||||
|
|
||||||
|
steps[i] = {
|
||||||
|
trigger: trigger,
|
||||||
|
pitch: (a.pitch + b.pitch) * 0.5,
|
||||||
|
velocity: (a.velocity + b.velocity) * 0.5,
|
||||||
|
accent: isAdditive ? (a.accent || b.accent) : (a.accent && b.accent),
|
||||||
|
timeOffset: (a.timeOffset + b.timeOffset) * 0.5,
|
||||||
|
subdivisions: Math.max(a.subdivisions, b.subdivisions),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: steps,
|
||||||
|
stepCount: count,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a specific step in a pattern (mutates the pattern in place).
|
||||||
|
*
|
||||||
|
* Only the fields present in stepData are updated; others remain unchanged.
|
||||||
|
*
|
||||||
|
* @param {{ steps: Array, stepCount: number, metadata: Object }} pattern
|
||||||
|
* @param {number} index - Step index (0-based)
|
||||||
|
* @param {Object} stepData - Partial step data to apply
|
||||||
|
*/
|
||||||
|
export function setStep(pattern, index, stepData) {
|
||||||
|
const idx = index | 0;
|
||||||
|
if (idx < 0 || idx >= pattern.stepCount) {
|
||||||
|
throw new RangeError('Step index ' + index + ' out of range [0, ' + (pattern.stepCount - 1) + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = pattern.steps[idx];
|
||||||
|
|
||||||
|
if (stepData.trigger !== undefined) step.trigger = !!stepData.trigger;
|
||||||
|
if (stepData.pitch !== undefined) step.pitch = +stepData.pitch;
|
||||||
|
if (stepData.velocity !== undefined) step.velocity = +stepData.velocity;
|
||||||
|
if (stepData.accent !== undefined) step.accent = !!stepData.accent;
|
||||||
|
if (stepData.timeOffset !== undefined) step.timeOffset = +stepData.timeOffset;
|
||||||
|
if (stepData.subdivisions !== undefined) step.subdivisions = stepData.subdivisions | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a pattern description structure.
|
||||||
|
*
|
||||||
|
* Checks:
|
||||||
|
* - pattern is an object with steps array, stepCount int, metadata object
|
||||||
|
* - steps.length === stepCount
|
||||||
|
* - Each step has all required fields with correct types and in-range values
|
||||||
|
*
|
||||||
|
* @param {*} pattern
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function validatePattern(pattern) {
|
||||||
|
if (pattern == null || typeof pattern !== 'object') return false;
|
||||||
|
if (typeof pattern.stepCount !== 'number' || (pattern.stepCount | 0) < 1) return false;
|
||||||
|
if (pattern.stepCount !== (pattern.stepCount | 0)) return false;
|
||||||
|
if (!Array.isArray(pattern.steps)) return false;
|
||||||
|
if (pattern.steps.length !== pattern.stepCount) return false;
|
||||||
|
if (pattern.metadata == null || typeof pattern.metadata !== 'object') return false;
|
||||||
|
|
||||||
|
const steps = pattern.steps;
|
||||||
|
const count = pattern.stepCount;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const s = steps[i];
|
||||||
|
if (s == null || typeof s !== 'object') return false;
|
||||||
|
if (typeof s.trigger !== 'boolean') return false;
|
||||||
|
if (typeof s.pitch !== 'number' || s.pitch < 0 || s.pitch > 1) return false;
|
||||||
|
if (typeof s.velocity !== 'number' || s.velocity < 0 || s.velocity > 1) return false;
|
||||||
|
if (typeof s.accent !== 'boolean') return false;
|
||||||
|
if (typeof s.timeOffset !== 'number' || s.timeOffset < -0.5 || s.timeOffset > 0.5) return false;
|
||||||
|
if (typeof s.subdivisions !== 'number' || (s.subdivisions | 0) < 1 || (s.subdivisions | 0) > 4) return false;
|
||||||
|
if (s.subdivisions !== (s.subdivisions | 0)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
137
playground/js/shapeseq/prng.js
Normal file
137
playground/js/shapeseq/prng.js
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* ShapeSeq PRNG — seedable pseudo-random number generator (mulberry32)
|
||||||
|
*
|
||||||
|
* Port-ready: explicit state as plain objects, no closures in hot path,
|
||||||
|
* all state is serializable. Pure-functional next/nextInt/fork — no mutation.
|
||||||
|
*
|
||||||
|
* @module shapeseq/prng
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Mulberry32 core ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single step of the mulberry32 PRNG.
|
||||||
|
* Pure function: takes a 32-bit state, returns { value, nextState }.
|
||||||
|
*
|
||||||
|
* @param {number} state - unsigned 32-bit integer
|
||||||
|
* @returns {{ value: number, nextState: number }}
|
||||||
|
* value: float in [0, 1)
|
||||||
|
* nextState: next 32-bit state
|
||||||
|
*/
|
||||||
|
function mulberry32Step(state) {
|
||||||
|
let t = (state + 0x6D2B79F5) | 0;
|
||||||
|
const nextState = t >>> 0; // the incremented state IS the new state
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
const raw = ((t ^ (t >>> 14)) >>> 0);
|
||||||
|
return { value: raw / 0x100000000, nextState };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public API ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a PRNG instance from a seed.
|
||||||
|
*
|
||||||
|
* @param {number} seed - any 32-bit integer (will be coerced to unsigned)
|
||||||
|
* @returns {{ state: number }} serializable PRNG state object
|
||||||
|
*/
|
||||||
|
export function createPRNG(seed) {
|
||||||
|
return { state: (seed >>> 0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce the next random float in [0, 1). Pure — does not mutate input.
|
||||||
|
*
|
||||||
|
* @param {{ state: number }} prng
|
||||||
|
* @returns {{ value: number, nextState: { state: number } }}
|
||||||
|
*/
|
||||||
|
export function next(prng) {
|
||||||
|
const { value, nextState } = mulberry32Step(prng.state);
|
||||||
|
return { value, nextState: { state: nextState } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce a random integer in [min, max] (inclusive). Pure — does not mutate input.
|
||||||
|
*
|
||||||
|
* @param {{ state: number }} prng
|
||||||
|
* @param {number} min - inclusive lower bound (integer)
|
||||||
|
* @param {number} max - inclusive upper bound (integer)
|
||||||
|
* @returns {{ value: number, nextState: { state: number } }}
|
||||||
|
*/
|
||||||
|
export function nextInt(prng, min, max) {
|
||||||
|
const { value, nextState } = next(prng);
|
||||||
|
const range = max - min + 1;
|
||||||
|
const intVal = min + Math.floor(value * range);
|
||||||
|
return { value: intVal, nextState };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive an independent PRNG stream from the current state + an id.
|
||||||
|
* Used to give each primitive in a chain its own deterministic stream.
|
||||||
|
*
|
||||||
|
* The child seed is produced by hashing the current state with the id
|
||||||
|
* using a simple but effective mixing function (splitmix-style).
|
||||||
|
*
|
||||||
|
* @param {{ state: number }} prng
|
||||||
|
* @param {number|string} id - primitive position index or string identifier
|
||||||
|
* @returns {{ state: number }} a new, independent PRNG state
|
||||||
|
*/
|
||||||
|
export function fork(prng, id) {
|
||||||
|
// Convert string ids to a numeric hash
|
||||||
|
let idNum;
|
||||||
|
if (typeof id === 'string') {
|
||||||
|
idNum = 0;
|
||||||
|
for (let i = 0; i < id.length; i++) {
|
||||||
|
idNum = ((idNum << 5) - idNum + id.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
idNum = idNum >>> 0;
|
||||||
|
} else {
|
||||||
|
idNum = id >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix state + id using splitmix-style finalizer
|
||||||
|
let h = (prng.state ^ idNum) >>> 0;
|
||||||
|
h = (h + 0x9E3779B9) >>> 0;
|
||||||
|
h = Math.imul(h ^ (h >>> 16), 0x85EBCA6B) >>> 0;
|
||||||
|
h = Math.imul(h ^ (h >>> 13), 0xC2B2AE35) >>> 0;
|
||||||
|
h = (h ^ (h >>> 16)) >>> 0;
|
||||||
|
|
||||||
|
return { state: h };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get serializable state from a PRNG instance (for freeze/restore).
|
||||||
|
*
|
||||||
|
* @param {{ state: number }} prng
|
||||||
|
* @returns {{ state: number }}
|
||||||
|
*/
|
||||||
|
export function getState(prng) {
|
||||||
|
return { state: prng.state };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a PRNG instance from previously saved state.
|
||||||
|
*
|
||||||
|
* @param {{ state: number }} _prng - ignored (stateless restore)
|
||||||
|
* @param {{ state: number }} savedState
|
||||||
|
* @returns {{ state: number }}
|
||||||
|
*/
|
||||||
|
export function setState(_prng, savedState) {
|
||||||
|
return { state: savedState.state >>> 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cryptographically random seed for initial seeding.
|
||||||
|
* Uses crypto.getRandomValues when available, falls back to Date.now().
|
||||||
|
*
|
||||||
|
* @returns {number} unsigned 32-bit integer
|
||||||
|
*/
|
||||||
|
export function randomSeed() {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
||||||
|
const buf = new Uint32Array(1);
|
||||||
|
crypto.getRandomValues(buf);
|
||||||
|
return buf[0];
|
||||||
|
}
|
||||||
|
// Fallback: not cryptographic, but sufficient for musical randomness
|
||||||
|
return (Date.now() * 2654435761) >>> 0;
|
||||||
|
}
|
||||||
76
playground/js/shapeseq/seq-iml.js
Normal file
76
playground/js/shapeseq/seq-iml.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// ShapeSeq — Sequence NISPS Instance
|
||||||
|
//
|
||||||
|
// Factory for creating a second WasmIML instance dedicated to sequence control.
|
||||||
|
// The MLP has a fixed 16-element output which a downstream param mapping layer
|
||||||
|
// (not in this module) fans out to however many primitive params the current
|
||||||
|
// chain requires.
|
||||||
|
//
|
||||||
|
// Input routing is configurable and wired externally (default: hand tracking
|
||||||
|
// features 0+1). This module only creates the IML instance — it does not
|
||||||
|
// subscribe to any input source. The integration layer (see meml-9tf) is
|
||||||
|
// responsible for calling setInputs() with routed values each frame.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// import { createSequenceIML } from './shapeseq/seq-iml.js';
|
||||||
|
// const seqIML = await createSequenceIML();
|
||||||
|
// // Per frame:
|
||||||
|
// seqIML.setInputs([x, y]);
|
||||||
|
// seqIML.process();
|
||||||
|
// const params16 = seqIML.getOutputs();
|
||||||
|
|
||||||
|
import { WasmIML } from '../nisps/nisps-wasm.js';
|
||||||
|
|
||||||
|
// Fixed architecture for the sequence MLP.
|
||||||
|
// 2 inputs (routed externally), 16 outputs (fixed — param mapping fans out).
|
||||||
|
// Smaller network than the synth MLP ([3,32,48,64,126]):
|
||||||
|
// three hidden layers of 16 neurons keeps inference cheap while providing
|
||||||
|
// enough capacity for 16 continuous outputs.
|
||||||
|
const SEQ_N_INPUTS = 2;
|
||||||
|
const SEQ_N_OUTPUTS = 16;
|
||||||
|
const SEQ_HIDDEN_LAYERS = [16, 16, 16];
|
||||||
|
|
||||||
|
// Training hyperparameters — same defaults as the synth IML
|
||||||
|
const SEQ_MAX_ITERATIONS = 1000;
|
||||||
|
const SEQ_LEARNING_RATE = 1.0;
|
||||||
|
const SEQ_CONVERGENCE_THRESHOLD = 0.00001;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a WasmIML instance configured for sequence control.
|
||||||
|
*
|
||||||
|
* Follows the same creation pattern as imlJoy / imlHand in a-app.js:
|
||||||
|
* const iml = await WasmIML.create(nInputs, nOutputs, hiddenLayers, ...);
|
||||||
|
*
|
||||||
|
* The returned object is a standard WasmIML instance with the full interface:
|
||||||
|
* setInput / setInputs, getOutputs, process,
|
||||||
|
* addExample, clearDataset, train, trainAsync,
|
||||||
|
* randomiseWeights (drawWeights), moveWeights,
|
||||||
|
* exampleCount, destroy, etc.
|
||||||
|
*
|
||||||
|
* Additionally exposes SEQ_N_INPUTS, SEQ_N_OUTPUTS, SEQ_HIDDEN_LAYERS as
|
||||||
|
* properties on the returned object for introspection by downstream code.
|
||||||
|
*
|
||||||
|
* @returns {Promise<WasmIML>} — the sequence IML instance (augmented with
|
||||||
|
* .SEQ_N_INPUTS, .SEQ_N_OUTPUTS, .SEQ_HIDDEN_LAYERS)
|
||||||
|
*/
|
||||||
|
export async function createSequenceIML() {
|
||||||
|
const seqIML = await WasmIML.create(
|
||||||
|
SEQ_N_INPUTS,
|
||||||
|
SEQ_N_OUTPUTS,
|
||||||
|
SEQ_HIDDEN_LAYERS,
|
||||||
|
SEQ_MAX_ITERATIONS,
|
||||||
|
SEQ_LEARNING_RATE,
|
||||||
|
SEQ_CONVERGENCE_THRESHOLD
|
||||||
|
);
|
||||||
|
|
||||||
|
seqIML.setLogger(msg => console.log('[NISPS:seq]', msg));
|
||||||
|
|
||||||
|
// Attach architecture metadata for introspection
|
||||||
|
seqIML.SEQ_N_INPUTS = SEQ_N_INPUTS;
|
||||||
|
seqIML.SEQ_N_OUTPUTS = SEQ_N_OUTPUTS;
|
||||||
|
seqIML.SEQ_HIDDEN_LAYERS = SEQ_HIDDEN_LAYERS;
|
||||||
|
|
||||||
|
return seqIML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export constants for use by other modules (e.g., param mapping layer)
|
||||||
|
export { SEQ_N_INPUTS, SEQ_N_OUTPUTS, SEQ_HIDDEN_LAYERS };
|
||||||
Loading…
Reference in a new issue