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).
214 lines
6.8 KiB
JavaScript
214 lines
6.8 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|