chore: delete retired-playground artefacts and root relics
Phase 1 of the 2026-07 simplification audit, group 1. All verified dead by independent grep across scripts/, .github/workflows/, codegen/, manifold/, firmware/, vcv/, nisps/ and README.md before removal. - S23 / L55: playground/ — only dist/ was tracked (1.2 MB of bundled JS, sourcemaps and a second copy of nisps.wasm). The app itself is preserved on branch archive/playground-solidjs and tag playground-solidjs-final. - S29: the root Playwright rig — playwright.config.js, package.json, package-lock.json and tests/e2e/. It served ./playground, a page that no longer exists anywhere in the tree, and was invoked by nothing. NOTE: the live suite is manifold/tests/e2e/ with manifold/playwright.config.ts, which is untouched; every CI Playwright step runs with working-directory: manifold. - L48: NISPS_CORE_EXTRACTION_PLAN.md + NISPS_CORE_TASKS.md (818 lines of extraction relics; docs/specs/plans/ is the sanctioned home for plans). - L36: data/ — two CSVs from a retired era, plus a tracked LibreOffice lock file. - L32: .claude/worktrees/agent-ae87fe47/ only. The audit's wording invites deleting .claude/worktrees/ wholesale; that would have been destructive — the directory holds 12 LIVE registered git worktrees (332 MB), four of them on branches with unpushed commits. Verified agent-ae87fe47 is NOT registered before removing it; every live worktree is left intact. Cleaning up the rest is a separate operator decision. - ST10 fallout: dropped the dead `port-solidjs` branch trigger from ci.yml and the dead `feat/manifold-mission` / `feat/vcv-dist` triggers from vcv-plugin.yml — those branches are stale and the SolidJS target is retired. tests/cpp/ is untouched. Gates: run-all-tests.sh ALL GREEN.
This commit is contained in:
parent
aa60fbb466
commit
abb569b287
39 changed files with 3 additions and 6073 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,319 +0,0 @@
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
/**
|
|
||||||
* ShapeSeq Primitive Base Class and Param Schema System
|
|
||||||
*
|
|
||||||
* Base class for all sequencing primitives (generators, processors,
|
|
||||||
* timing modifiers, converters). Defines the param schema format,
|
|
||||||
* symbolic process() interface, and state management for freeze support.
|
|
||||||
*
|
|
||||||
* Port-ready: explicit state, no closures, typed arrays where possible.
|
|
||||||
*
|
|
||||||
* @module shapeseq/primitive
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ── Valid primitive categories ──────────────────────────────────────
|
|
||||||
|
|
||||||
export const CATEGORIES = Object.freeze([
|
|
||||||
'generator',
|
|
||||||
'processor',
|
|
||||||
'timing',
|
|
||||||
'converter',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ── Param schema defaults ───────────────────────────────────────────
|
|
||||||
|
|
||||||
const DEFAULT_SCALED_RANGE = 0.3;
|
|
||||||
|
|
||||||
// ── Boundary enforcement helpers ────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clamp a value to [0, 1].
|
|
||||||
* @param {number} v
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
function clamp01(v) {
|
|
||||||
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap a value into [0, 1) with modular arithmetic.
|
|
||||||
* @param {number} v
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
function wrap01(v) {
|
|
||||||
const m = v % 1;
|
|
||||||
return m < 0 ? m + 1 : m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply boundary enforcement to a raw param value.
|
|
||||||
*
|
|
||||||
* @param {number} value - raw [0,1] value (or delta-adjusted value)
|
|
||||||
* @param {{ boundary: string, scaledRange?: number }} schema - param schema entry
|
|
||||||
* @param {number|null} frozenValue - frozen value for 'scaled' boundary (null if not frozen)
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export function applyBoundary(value, schema, frozenValue) {
|
|
||||||
switch (schema.boundary) {
|
|
||||||
case 'wrap':
|
|
||||||
return wrap01(value);
|
|
||||||
case 'scaled': {
|
|
||||||
if (frozenValue === null || frozenValue === undefined) {
|
|
||||||
return clamp01(value);
|
|
||||||
}
|
|
||||||
const range = schema.scaledRange !== undefined ? schema.scaledRange : DEFAULT_SCALED_RANGE;
|
|
||||||
const lo = frozenValue - range;
|
|
||||||
const hi = frozenValue + range;
|
|
||||||
// Map [0,1] input to [lo, hi], then clamp to [0,1]
|
|
||||||
const mapped = lo + value * (hi - lo);
|
|
||||||
return clamp01(mapped);
|
|
||||||
}
|
|
||||||
case 'clamp':
|
|
||||||
default:
|
|
||||||
return clamp01(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Param schema validation ─────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a single param schema entry.
|
|
||||||
* Throws on invalid entries for fast fail during development.
|
|
||||||
*
|
|
||||||
* @param {{ name: string, default: number, boundary: string, scaledRange?: number }} entry
|
|
||||||
* @param {number} index - position in schema array (for error messages)
|
|
||||||
*/
|
|
||||||
function validateSchemaEntry(entry, index) {
|
|
||||||
if (!entry || typeof entry !== 'object') {
|
|
||||||
throw new TypeError('paramSchema[' + index + '] must be an object');
|
|
||||||
}
|
|
||||||
if (typeof entry.name !== 'string' || entry.name.length === 0) {
|
|
||||||
throw new TypeError('paramSchema[' + index + '].name must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (typeof entry.default !== 'number' || entry.default < 0 || entry.default > 1) {
|
|
||||||
throw new RangeError('paramSchema[' + index + '].default must be in [0,1], got ' + entry.default);
|
|
||||||
}
|
|
||||||
if (entry.boundary !== 'clamp' && entry.boundary !== 'wrap' && entry.boundary !== 'scaled') {
|
|
||||||
throw new TypeError(
|
|
||||||
"paramSchema[" + index + "].boundary must be 'clamp', 'wrap', or 'scaled', got '" + entry.boundary + "'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entry.boundary === 'scaled') {
|
|
||||||
const sr = entry.scaledRange;
|
|
||||||
if (sr !== undefined && (typeof sr !== 'number' || sr <= 0 || sr > 1)) {
|
|
||||||
throw new RangeError('paramSchema[' + index + '].scaledRange must be in (0,1], got ' + sr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Primitive base class ────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class Primitive {
|
|
||||||
/**
|
|
||||||
* @param {string} name - unique identifier for this primitive type
|
|
||||||
* @param {string} category - one of CATEGORIES
|
|
||||||
* @param {Array<{ name: string, default: number, boundary: string, scaledRange?: number }>} paramSchema
|
|
||||||
*/
|
|
||||||
constructor(name, category, paramSchema) {
|
|
||||||
if (typeof name !== 'string' || name.length === 0) {
|
|
||||||
throw new TypeError('Primitive name must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (CATEGORIES.indexOf(category) === -1) {
|
|
||||||
throw new TypeError(
|
|
||||||
"Primitive category must be one of [" + CATEGORIES.join(', ') + "], got '" + category + "'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(paramSchema)) {
|
|
||||||
throw new TypeError('paramSchema must be an array');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each entry
|
|
||||||
for (let i = 0; i < paramSchema.length; i++) {
|
|
||||||
validateSchemaEntry(paramSchema[i], i);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
this.name = name;
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
this.category = category;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Frozen copy of the param schema. Each entry:
|
|
||||||
* { name: string, default: number, boundary: 'clamp'|'wrap'|'scaled', scaledRange?: number }
|
|
||||||
* @type {Array<Object>}
|
|
||||||
*/
|
|
||||||
this.paramSchema = Object.freeze(paramSchema.map(function (entry) {
|
|
||||||
const frozen = {
|
|
||||||
name: entry.name,
|
|
||||||
default: entry.default,
|
|
||||||
boundary: entry.boundary,
|
|
||||||
};
|
|
||||||
if (entry.boundary === 'scaled') {
|
|
||||||
frozen.scaledRange = entry.scaledRange !== undefined ? entry.scaledRange : DEFAULT_SCALED_RANGE;
|
|
||||||
}
|
|
||||||
return Object.freeze(frozen);
|
|
||||||
}));
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
this._seed = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Param utilities ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total number of parameters this primitive exposes.
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
get paramCount() {
|
|
||||||
return this.paramSchema.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get default param values as a Float32Array, one per schema entry.
|
|
||||||
* @returns {Float32Array}
|
|
||||||
*/
|
|
||||||
getDefaults() {
|
|
||||||
const count = this.paramSchema.length;
|
|
||||||
const defaults = new Float32Array(count);
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
defaults[i] = this.paramSchema[i].default;
|
|
||||||
}
|
|
||||||
return defaults;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Symbolic processing ─────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a pattern description. Subclasses MUST override this.
|
|
||||||
*
|
|
||||||
* - Generators ignore patternDesc and create a new one (using createPattern())
|
|
||||||
* - Processors/timing modifiers clone and transform patternDesc
|
|
||||||
* - The rng param is a PRNG state from prng.js; consume via next(rng)
|
|
||||||
* and return the consumed state in the result
|
|
||||||
*
|
|
||||||
* @param {Float32Array|Array<number>} params - param values, one per schema entry, each [0,1]
|
|
||||||
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - input pattern
|
|
||||||
* @param {Object} state - primitive-specific state (from previous process() call or getState())
|
|
||||||
* @param {{ state: number }} rng - PRNG state object from prng.js
|
|
||||||
* @returns {{ patternDesc: { steps: Array, stepCount: number, metadata: Object }, nextState: Object }}
|
|
||||||
*/
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
void params; void patternDesc; void state; void rng;
|
|
||||||
throw new Error(this.name + '.process() must be overridden by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── State management (for freeze) ──────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get serializable state for this primitive.
|
|
||||||
* Stateless primitives return {}. Stateful primitives (e.g. Pitch Walker)
|
|
||||||
* override to include their internal state.
|
|
||||||
*
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
getState() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore primitive state from a previously serialized state object.
|
|
||||||
* Stateless primitives are a no-op. Stateful primitives override.
|
|
||||||
*
|
|
||||||
* @param {Object} _state
|
|
||||||
*/
|
|
||||||
setState(_state) {
|
|
||||||
// no-op for stateless primitives
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the PRNG seed associated with this primitive.
|
|
||||||
* Used by freeze-as-algorithm to replay identical sequences.
|
|
||||||
*
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
getSeed() {
|
|
||||||
return this._seed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the PRNG seed for this primitive.
|
|
||||||
*
|
|
||||||
* @param {number} seed - 32-bit integer seed
|
|
||||||
*/
|
|
||||||
setSeed(seed) {
|
|
||||||
this._seed = seed >>> 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
/**
|
|
||||||
* ShapeSeq Sequencing Primitives
|
|
||||||
*
|
|
||||||
* All 8 primitives for the ShapeSeq generative sequencing system.
|
|
||||||
* Each extends Primitive and implements process(params, patternDesc, state, rng).
|
|
||||||
*
|
|
||||||
* Port-ready: explicit state, no closures, seeded PRNG, typed arrays.
|
|
||||||
*
|
|
||||||
* @module shapeseq/primitives
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Primitive } from './primitive.js';
|
|
||||||
import { createPattern, clonePattern, setStep } from './pattern.js';
|
|
||||||
import { next, nextInt } from './prng.js';
|
|
||||||
|
|
||||||
// ── Helper: map [0,1] float to integer range [lo, hi] ──────────────
|
|
||||||
|
|
||||||
function mapToInt(value, lo, hi) {
|
|
||||||
const clamped = value < 0 ? 0 : value > 1 ? 1 : value;
|
|
||||||
return lo + Math.floor(clamped * (hi - lo + 1 - 1e-9));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 1. EuclideanRhythm ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bjorklund algorithm: distribute `pulses` as evenly as possible
|
|
||||||
* across `steps`, then apply rotation.
|
|
||||||
*/
|
|
||||||
function bjorklund(steps, pulses) {
|
|
||||||
if (pulses >= steps) {
|
|
||||||
const result = new Array(steps);
|
|
||||||
for (let i = 0; i < steps; i++) result[i] = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (pulses <= 0) {
|
|
||||||
const result = new Array(steps);
|
|
||||||
for (let i = 0; i < steps; i++) result[i] = false;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build pattern using Bjorklund's algorithm
|
|
||||||
let groups = [];
|
|
||||||
for (let i = 0; i < pulses; i++) groups.push([true]);
|
|
||||||
for (let i = 0; i < steps - pulses; i++) groups.push([false]);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const remainder = groups.length - pulses;
|
|
||||||
if (remainder <= 1) break;
|
|
||||||
const minLen = Math.min(pulses, remainder);
|
|
||||||
const newGroups = [];
|
|
||||||
for (let i = 0; i < minLen; i++) {
|
|
||||||
newGroups.push(groups[i].concat(groups[groups.length - 1 - i]));
|
|
||||||
}
|
|
||||||
// Keep any leftovers
|
|
||||||
const leftStart = minLen;
|
|
||||||
const leftEnd = groups.length - minLen;
|
|
||||||
for (let i = leftStart; i < leftEnd; i++) {
|
|
||||||
newGroups.push(groups[i]);
|
|
||||||
}
|
|
||||||
groups = newGroups;
|
|
||||||
pulses = minLen;
|
|
||||||
if (pulses <= 1) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten groups
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < groups.length; i++) {
|
|
||||||
for (let j = 0; j < groups[i].length; j++) {
|
|
||||||
result.push(groups[i][j]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EuclideanRhythm extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('EuclideanRhythm', 'generator', [
|
|
||||||
{ name: 'steps', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'pulses', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'rotation', default: 0.0, boundary: 'wrap' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const stepCount = patternDesc.stepCount;
|
|
||||||
const steps = mapToInt(params[0], 2, stepCount);
|
|
||||||
const pulses = mapToInt(params[1], 0, steps);
|
|
||||||
const rotation = mapToInt(params[2], 0, steps - 1);
|
|
||||||
|
|
||||||
const rhythm = bjorklund(steps, pulses);
|
|
||||||
const pattern = createPattern(stepCount);
|
|
||||||
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
|
||||||
if (i < steps) {
|
|
||||||
const srcIdx = (i - rotation + steps) % steps;
|
|
||||||
if (rhythm[srcIdx]) {
|
|
||||||
setStep(pattern, i, { trigger: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Steps beyond `steps` remain untriggered (default)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 2. ProbabilityGate ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class ProbabilityGate extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('ProbabilityGate', 'processor', [
|
|
||||||
{ name: 'density', default: 0.7, boundary: 'clamp' },
|
|
||||||
{ name: 'accentProbability', default: 0.3, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const density = params[0];
|
|
||||||
const accentProb = params[1];
|
|
||||||
const pattern = clonePattern(patternDesc);
|
|
||||||
let currentRng = rng;
|
|
||||||
|
|
||||||
for (let i = 0; i < pattern.stepCount; i++) {
|
|
||||||
const step = pattern.steps[i];
|
|
||||||
if (step.trigger) {
|
|
||||||
// Coin flip for survival
|
|
||||||
const r1 = next(currentRng);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
|
|
||||||
if (r1.value >= density) {
|
|
||||||
step.trigger = false;
|
|
||||||
step.accent = false;
|
|
||||||
} else {
|
|
||||||
// Accent coin flip
|
|
||||||
const r2 = next(currentRng);
|
|
||||||
currentRng = r2.nextState;
|
|
||||||
step.accent = r2.value < accentProb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 3. PitchWalker ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class PitchWalker extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('PitchWalker', 'generator', [
|
|
||||||
{ name: 'stepSize', default: 0.3, boundary: 'clamp' },
|
|
||||||
{ name: 'directionBias', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'gravity', default: 0.3, boundary: 'clamp' },
|
|
||||||
{ name: 'range', default: 0.8, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
this._position = 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return { position: this._position };
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(savedState) {
|
|
||||||
if (savedState && typeof savedState.position === 'number') {
|
|
||||||
this._position = savedState.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const stepSize = params[0];
|
|
||||||
const directionBias = params[1];
|
|
||||||
const gravity = params[2];
|
|
||||||
const range = params[3];
|
|
||||||
|
|
||||||
// Restore position from state if provided
|
|
||||||
let position = (state && typeof state.position === 'number')
|
|
||||||
? state.position
|
|
||||||
: this._position;
|
|
||||||
|
|
||||||
const pattern = createPattern(patternDesc.stepCount);
|
|
||||||
let currentRng = rng;
|
|
||||||
|
|
||||||
// Use incoming pattern's triggers if available, otherwise all triggered
|
|
||||||
const srcSteps = patternDesc.steps;
|
|
||||||
|
|
||||||
for (let i = 0; i < patternDesc.stepCount; i++) {
|
|
||||||
const triggered = srcSteps[i].trigger;
|
|
||||||
|
|
||||||
if (triggered) {
|
|
||||||
// Random walk step
|
|
||||||
const r1 = next(currentRng);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
|
|
||||||
// Direction: bias + gravity toward center
|
|
||||||
const gravityPull = (0.5 - position) * gravity;
|
|
||||||
const biasOffset = (directionBias - 0.5) * 2; // [-1, 1]
|
|
||||||
const direction = biasOffset + gravityPull;
|
|
||||||
|
|
||||||
// Random component: [-1, 1] scaled by stepSize
|
|
||||||
const randomComponent = (r1.value * 2 - 1) * stepSize * range;
|
|
||||||
const delta = direction * stepSize * 0.5 + randomComponent;
|
|
||||||
|
|
||||||
position = position + delta;
|
|
||||||
// Clamp to [0, 1]
|
|
||||||
if (position < 0) position = 0;
|
|
||||||
if (position > 1) position = 1;
|
|
||||||
|
|
||||||
setStep(pattern, i, { trigger: true, pitch: position });
|
|
||||||
}
|
|
||||||
// Untriggered steps keep default pitch, trigger=false
|
|
||||||
}
|
|
||||||
|
|
||||||
this._position = position;
|
|
||||||
|
|
||||||
return {
|
|
||||||
patternDesc: pattern,
|
|
||||||
nextState: { position: position },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 4. Ratchet ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class Ratchet extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('Ratchet', 'timing', [
|
|
||||||
{ name: 'maxDivision', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'probability', default: 0.5, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const maxDiv = mapToInt(params[0], 1, 4);
|
|
||||||
const probability = params[1];
|
|
||||||
const pattern = clonePattern(patternDesc);
|
|
||||||
let currentRng = rng;
|
|
||||||
|
|
||||||
for (let i = 0; i < pattern.stepCount; i++) {
|
|
||||||
const step = pattern.steps[i];
|
|
||||||
if (step.trigger) {
|
|
||||||
const r1 = next(currentRng);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
|
|
||||||
if (r1.value < probability && maxDiv > 1) {
|
|
||||||
// Pick a subdivision count in [2, maxDiv]
|
|
||||||
const r2 = nextInt(currentRng, 2, maxDiv);
|
|
||||||
currentRng = r2.nextState;
|
|
||||||
step.subdivisions = r2.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 5. SwingGroove ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class SwingGroove extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('SwingGroove', 'timing', [
|
|
||||||
{ name: 'swingAmount', default: 0.0, boundary: 'clamp' },
|
|
||||||
{ name: 'swingGrid', default: 0.0, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const swingAmount = params[0];
|
|
||||||
const pattern = clonePattern(patternDesc);
|
|
||||||
|
|
||||||
// Max swing = 0.33 (triplet feel)
|
|
||||||
const maxOffset = 0.33;
|
|
||||||
const offset = swingAmount * maxOffset;
|
|
||||||
|
|
||||||
// Apply swing to every other step (odd-indexed steps)
|
|
||||||
for (let i = 1; i < pattern.stepCount; i += 2) {
|
|
||||||
pattern.steps[i].timeOffset = offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 6. DensityMorph ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class DensityMorph extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('DensityMorph', 'generator', [
|
|
||||||
{ name: 'density', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'clustering', default: 0.0, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const density = params[0];
|
|
||||||
const clustering = params[1];
|
|
||||||
const stepCount = patternDesc.stepCount;
|
|
||||||
const pattern = createPattern(stepCount);
|
|
||||||
const numTriggers = Math.floor(density * stepCount);
|
|
||||||
|
|
||||||
if (numTriggers <= 0) {
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
if (numTriggers >= stepCount) {
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
|
||||||
setStep(pattern, i, { trigger: true });
|
|
||||||
}
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentRng = rng;
|
|
||||||
|
|
||||||
if (clustering < 0.01) {
|
|
||||||
// Even spread: Euclidean-like placement
|
|
||||||
for (let i = 0; i < numTriggers; i++) {
|
|
||||||
const idx = Math.floor((i * stepCount) / numTriggers);
|
|
||||||
setStep(pattern, idx, { trigger: true });
|
|
||||||
}
|
|
||||||
} else if (clustering > 0.99) {
|
|
||||||
// Full clustering: contiguous burst
|
|
||||||
const r1 = nextInt(currentRng, 0, stepCount - 1);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
const startPos = r1.value;
|
|
||||||
for (let i = 0; i < numTriggers; i++) {
|
|
||||||
const idx = (startPos + i) % stepCount;
|
|
||||||
setStep(pattern, idx, { trigger: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Interpolate: place triggers with clustering-dependent spread
|
|
||||||
// Use a "center of mass" approach:
|
|
||||||
// Pick a random center, then distribute triggers around it
|
|
||||||
// with spread inversely proportional to clustering
|
|
||||||
const r1 = next(currentRng);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
const center = r1.value * stepCount;
|
|
||||||
|
|
||||||
// Spread factor: low clustering = large spread, high = tight
|
|
||||||
const spreadRadius = (1 - clustering) * stepCount * 0.5;
|
|
||||||
|
|
||||||
// Score each step by distance from center (wrapping)
|
|
||||||
const scores = new Float32Array(stepCount);
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
|
||||||
// Wrapped distance from center
|
|
||||||
let dist = Math.abs(i - center);
|
|
||||||
if (dist > stepCount * 0.5) dist = stepCount - dist;
|
|
||||||
// Add small random jitter to break ties
|
|
||||||
const r2 = next(currentRng);
|
|
||||||
currentRng = r2.nextState;
|
|
||||||
scores[i] = dist / (spreadRadius + 0.001) + r2.value * 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the numTriggers steps with lowest scores
|
|
||||||
const indices = new Array(stepCount);
|
|
||||||
for (let i = 0; i < stepCount; i++) indices[i] = i;
|
|
||||||
indices.sort(function (a, b) { return scores[a] - scores[b]; });
|
|
||||||
|
|
||||||
for (let i = 0; i < numTriggers; i++) {
|
|
||||||
setStep(pattern, indices[i], { trigger: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 7. IntervalLock ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const SCALES = [
|
|
||||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // chromatic
|
|
||||||
[0, 2, 4, 5, 7, 9, 11], // major
|
|
||||||
[0, 2, 3, 5, 7, 8, 10], // natural minor
|
|
||||||
[0, 2, 3, 5, 7, 8, 11], // harmonic minor
|
|
||||||
[0, 2, 4, 7, 9], // pentatonic major
|
|
||||||
[0, 3, 5, 7, 10], // pentatonic minor
|
|
||||||
[0, 3, 5, 6, 7, 10], // blues
|
|
||||||
[0, 2, 3, 5, 7, 9, 10], // dorian
|
|
||||||
[0, 2, 4, 5, 7, 9, 10], // mixolydian
|
|
||||||
[0, 2, 4, 6, 8, 10], // whole tone
|
|
||||||
[0, 2, 3, 5, 6, 8, 9, 11], // diminished
|
|
||||||
];
|
|
||||||
|
|
||||||
export class IntervalLock extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('IntervalLock', 'converter', [
|
|
||||||
{ name: 'root', default: 0.0, boundary: 'clamp' },
|
|
||||||
{ name: 'mode', default: 0.0, boundary: 'clamp' },
|
|
||||||
{ name: 'octaveRange', default: 0.25, boundary: 'clamp' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const root = mapToInt(params[0], 0, 11);
|
|
||||||
const scaleIdx = mapToInt(params[1], 0, SCALES.length - 1);
|
|
||||||
const octaveRange = mapToInt(params[2], 1, 4);
|
|
||||||
const scale = SCALES[scaleIdx];
|
|
||||||
|
|
||||||
const pattern = clonePattern(patternDesc);
|
|
||||||
|
|
||||||
// Build the full set of MIDI notes in this scale + root + range
|
|
||||||
const notes = [];
|
|
||||||
for (let oct = 0; oct < octaveRange; oct++) {
|
|
||||||
for (let i = 0; i < scale.length; i++) {
|
|
||||||
const midiNote = root + scale[i] + oct * 12;
|
|
||||||
if (midiNote <= 127) {
|
|
||||||
notes.push(midiNote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notes.length === 0) {
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < pattern.stepCount; i++) {
|
|
||||||
const step = pattern.steps[i];
|
|
||||||
// Quantize pitch [0,1] to nearest note in our scale
|
|
||||||
const targetIdx = Math.round(step.pitch * (notes.length - 1));
|
|
||||||
const clampedIdx = targetIdx < 0 ? 0 : targetIdx >= notes.length ? notes.length - 1 : targetIdx;
|
|
||||||
// Store as MIDI note / 127 to stay in [0,1]
|
|
||||||
step.pitch = notes[clampedIdx] / 127;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 8. VelocityShaper ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class VelocityShaper extends Primitive {
|
|
||||||
constructor() {
|
|
||||||
super('VelocityShaper', 'processor', [
|
|
||||||
{ name: 'curveType', default: 0.0, boundary: 'clamp' },
|
|
||||||
{ name: 'depth', default: 0.5, boundary: 'clamp' },
|
|
||||||
{ name: 'phase', default: 0.0, boundary: 'wrap' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
|
||||||
const curveIdx = mapToInt(params[0], 0, 4);
|
|
||||||
const depth = params[1];
|
|
||||||
const phase = params[2];
|
|
||||||
const pattern = clonePattern(patternDesc);
|
|
||||||
const stepCount = pattern.stepCount;
|
|
||||||
let currentRng = rng;
|
|
||||||
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
|
||||||
const step = pattern.steps[i];
|
|
||||||
if (!step.trigger) continue;
|
|
||||||
|
|
||||||
// Phase-shifted position
|
|
||||||
const pos = ((i / stepCount) + phase) % 1;
|
|
||||||
let shapeValue;
|
|
||||||
|
|
||||||
switch (curveIdx) {
|
|
||||||
case 0: // flat
|
|
||||||
shapeValue = 1.0;
|
|
||||||
break;
|
|
||||||
case 1: // accent-every-N (accent every 4th step)
|
|
||||||
shapeValue = ((i + Math.floor(phase * stepCount)) % 4 === 0) ? 1.0 : 0.5;
|
|
||||||
break;
|
|
||||||
case 2: // crescendo
|
|
||||||
shapeValue = pos;
|
|
||||||
break;
|
|
||||||
case 3: // decrescendo
|
|
||||||
shapeValue = 1.0 - pos;
|
|
||||||
break;
|
|
||||||
case 4: { // random
|
|
||||||
const r1 = next(currentRng);
|
|
||||||
currentRng = r1.nextState;
|
|
||||||
shapeValue = r1.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
shapeValue = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply depth: interpolate between uniform (1.0) and shaped
|
|
||||||
// depth=0 means all same velocity (base), depth=1 means full shape
|
|
||||||
const baseVelocity = 0.7;
|
|
||||||
const shaped = shapeValue;
|
|
||||||
step.velocity = baseVelocity * (1 - depth) + shaped * depth;
|
|
||||||
|
|
||||||
// Clamp
|
|
||||||
if (step.velocity < 0) step.velocity = 0;
|
|
||||||
if (step.velocity > 1) step.velocity = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Registry ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const PRIMITIVE_REGISTRY = {
|
|
||||||
EuclideanRhythm: EuclideanRhythm,
|
|
||||||
ProbabilityGate: ProbabilityGate,
|
|
||||||
PitchWalker: PitchWalker,
|
|
||||||
Ratchet: Ratchet,
|
|
||||||
SwingGroove: SwingGroove,
|
|
||||||
DensityMorph: DensityMorph,
|
|
||||||
IntervalLock: IntervalLock,
|
|
||||||
VelocityShaper: VelocityShaper,
|
|
||||||
};
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
/**
|
|
||||||
* ShapeSeq Engine — central orchestrator
|
|
||||||
*
|
|
||||||
* Wires together the sequence MLP, param mapping, primitive chain,
|
|
||||||
* projection layer, clock engine, and C15 bridge.
|
|
||||||
*
|
|
||||||
* Main loop (triggered by setSequenceInputs):
|
|
||||||
* 1. Forward inputs to sequenceIML
|
|
||||||
* 2. Run MLP inference to get 16 outputs
|
|
||||||
* 3. Map 16 outputs to N primitive params via param-map
|
|
||||||
* 4. Evaluate the chain to produce a pattern description
|
|
||||||
* 5. Apply projection transforms
|
|
||||||
* 6. Schedule the pattern on the clock
|
|
||||||
*
|
|
||||||
* Bridge integration:
|
|
||||||
* - Subscribes to seq.noteOn / seq.noteOff on the event bus
|
|
||||||
* - Forwards to C15Bridge.noteOn / noteOff
|
|
||||||
* - Tracks active notes to avoid orphans
|
|
||||||
*
|
|
||||||
* @module shapeseq/sequencer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createSequenceIML, SEQ_N_OUTPUTS } from './seq-iml.js';
|
|
||||||
import { Chain } from './chain.js';
|
|
||||||
import { ClockEngine } from './clock.js';
|
|
||||||
import { map } from './param-map.js';
|
|
||||||
import { createProjectionChain, applyProjection, PRESETS } from './projection.js';
|
|
||||||
import { SEQ } from './event-bus.js';
|
|
||||||
import {
|
|
||||||
EuclideanRhythm,
|
|
||||||
ProbabilityGate,
|
|
||||||
PitchWalker,
|
|
||||||
IntervalLock,
|
|
||||||
VelocityShaper,
|
|
||||||
} from './primitives.js';
|
|
||||||
|
|
||||||
// ── Defaults ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const DEFAULT_BPM = 120;
|
|
||||||
const DEFAULT_STEP_COUNT = 8;
|
|
||||||
const DEFAULT_MASTER_SEED = 42;
|
|
||||||
const DEFAULT_SPREAD = 0.6;
|
|
||||||
|
|
||||||
// ── ShapeSeqEngine ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class ShapeSeqEngine {
|
|
||||||
/**
|
|
||||||
* @param {{ audioContext: AudioContext, eventBus: import('./event-bus.js').EventBus, c15Bridge: import('../synth/c15-bridge.js').C15Bridge }} opts
|
|
||||||
*/
|
|
||||||
constructor({ audioContext, eventBus, c15Bridge }) {
|
|
||||||
if (!audioContext) throw new TypeError('ShapeSeqEngine requires an audioContext');
|
|
||||||
if (!eventBus) throw new TypeError('ShapeSeqEngine requires an eventBus');
|
|
||||||
if (!c15Bridge) throw new TypeError('ShapeSeqEngine requires a c15Bridge');
|
|
||||||
|
|
||||||
/** @private */ this._audioCtx = audioContext;
|
|
||||||
/** @private */ this._bus = eventBus;
|
|
||||||
/** @private */ this._c15 = c15Bridge;
|
|
||||||
|
|
||||||
/** @private */ this._sequenceIML = null;
|
|
||||||
/** @private */ this._chain = null;
|
|
||||||
/** @private */ this._clock = null;
|
|
||||||
/** @private */ this._projectionChain = null;
|
|
||||||
|
|
||||||
/** @private */ this._stepCount = DEFAULT_STEP_COUNT;
|
|
||||||
/** @private */ this._masterSeed = DEFAULT_MASTER_SEED;
|
|
||||||
/** @private */ this._playing = false;
|
|
||||||
/** @private */ this._initialized = false;
|
|
||||||
|
|
||||||
// Track active notes for orphan prevention
|
|
||||||
/** @private @type {Set<number>} */
|
|
||||||
this._activeNotes = new Set();
|
|
||||||
|
|
||||||
// Bound handlers for event bus (stored for cleanup)
|
|
||||||
/** @private */
|
|
||||||
this._onNoteOn = (data) => this._handleNoteOn(data);
|
|
||||||
/** @private */
|
|
||||||
this._onNoteOff = (data) => this._handleNoteOff(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize all subsystems: create sequence IML, default chain,
|
|
||||||
* clock, and projection chain. Must be called before start().
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
// 1. Create the sequence MLP
|
|
||||||
this._sequenceIML = await createSequenceIML();
|
|
||||||
|
|
||||||
// Randomize weights with default spread
|
|
||||||
this._sequenceIML.drawWeights(DEFAULT_SPREAD);
|
|
||||||
|
|
||||||
// 2. Create the default primitive chain
|
|
||||||
this._chain = new Chain();
|
|
||||||
this._chain.addPrimitive(new EuclideanRhythm());
|
|
||||||
this._chain.addPrimitive(new ProbabilityGate());
|
|
||||||
this._chain.addPrimitive(new PitchWalker());
|
|
||||||
this._chain.addPrimitive(new IntervalLock());
|
|
||||||
this._chain.addPrimitive(new VelocityShaper());
|
|
||||||
this._chain.setMasterSeed(this._masterSeed);
|
|
||||||
|
|
||||||
// 3. Set up the clock
|
|
||||||
this._clock = new ClockEngine(this._audioCtx, this._bus);
|
|
||||||
this._clock.bpm = DEFAULT_BPM;
|
|
||||||
|
|
||||||
// 4. Create default projection chain (expressive preset)
|
|
||||||
const result = createProjectionChain(PRESETS.expressive);
|
|
||||||
if (!result.valid) {
|
|
||||||
throw new Error('Default projection chain invalid: ' + result.error);
|
|
||||||
}
|
|
||||||
this._projectionChain = result;
|
|
||||||
|
|
||||||
// 5. Subscribe to event bus for C15 bridge integration
|
|
||||||
this._bus.on(SEQ.NOTE_ON, this._onNoteOn);
|
|
||||||
this._bus.on(SEQ.NOTE_OFF, this._onNoteOff);
|
|
||||||
|
|
||||||
this._initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the clock. Requires init() to have been called.
|
|
||||||
*/
|
|
||||||
start() {
|
|
||||||
if (!this._initialized) {
|
|
||||||
throw new Error('ShapeSeqEngine.start() called before init()');
|
|
||||||
}
|
|
||||||
if (this._playing) return;
|
|
||||||
|
|
||||||
this._playing = true;
|
|
||||||
this._clock.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the clock and release all active notes.
|
|
||||||
*/
|
|
||||||
stop() {
|
|
||||||
if (!this._playing) return;
|
|
||||||
|
|
||||||
this._playing = false;
|
|
||||||
this._clock.stop();
|
|
||||||
this._releaseAllNotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full cleanup: stop playback, unsubscribe from events, destroy IML.
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this.stop();
|
|
||||||
|
|
||||||
// Unsubscribe from event bus
|
|
||||||
this._bus.off(SEQ.NOTE_ON, this._onNoteOn);
|
|
||||||
this._bus.off(SEQ.NOTE_OFF, this._onNoteOff);
|
|
||||||
|
|
||||||
// Destroy the sequence IML instance
|
|
||||||
if (this._sequenceIML) {
|
|
||||||
this._sequenceIML.destroy();
|
|
||||||
this._sequenceIML = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._chain = null;
|
|
||||||
this._clock = null;
|
|
||||||
this._projectionChain = null;
|
|
||||||
this._initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Configuration ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the clock tempo.
|
|
||||||
* @param {number} bpm
|
|
||||||
*/
|
|
||||||
setTempo(bpm) {
|
|
||||||
if (this._clock) {
|
|
||||||
this._clock.setTempo(bpm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the number of steps in the generated pattern.
|
|
||||||
* @param {number} count
|
|
||||||
*/
|
|
||||||
setStepCount(count) {
|
|
||||||
const c = Math.max(1, count | 0);
|
|
||||||
this._stepCount = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the projection preset by name.
|
|
||||||
* @param {'expressive'|'percussive'|'fullRange'} presetName
|
|
||||||
*/
|
|
||||||
setProjectionPreset(presetName) {
|
|
||||||
const preset = PRESETS[presetName];
|
|
||||||
if (!preset) {
|
|
||||||
throw new Error('Unknown projection preset: ' + presetName);
|
|
||||||
}
|
|
||||||
const result = createProjectionChain(preset);
|
|
||||||
if (!result.valid) {
|
|
||||||
throw new Error('Projection chain invalid: ' + result.error);
|
|
||||||
}
|
|
||||||
this._projectionChain = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Chain access (for UI binding) ──────────────────────────────────
|
|
||||||
|
|
||||||
/** @returns {Chain} */
|
|
||||||
getChain() { return this._chain; }
|
|
||||||
|
|
||||||
/** @returns {ClockEngine} */
|
|
||||||
getClock() { return this._clock; }
|
|
||||||
|
|
||||||
/** @returns {WasmIML} */
|
|
||||||
getSequenceIML() { return this._sequenceIML; }
|
|
||||||
|
|
||||||
// ── Input routing ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feed new input values to the sequence MLP and run the full pipeline:
|
|
||||||
* MLP inference -> param mapping -> chain evaluation -> projection -> clock scheduling.
|
|
||||||
*
|
|
||||||
* Call this each frame with the routed input values (e.g., [x, y]).
|
|
||||||
*
|
|
||||||
* @param {number[]} values - input array (typically [x, y])
|
|
||||||
*/
|
|
||||||
setSequenceInputs(values) {
|
|
||||||
if (!this._initialized || !this._sequenceIML) return;
|
|
||||||
|
|
||||||
// 1. Forward inputs to the sequence IML
|
|
||||||
this._sequenceIML.setInputs(values);
|
|
||||||
|
|
||||||
// 2. Run MLP inference
|
|
||||||
this._sequenceIML.process();
|
|
||||||
|
|
||||||
// 3. Get the 16 MLP outputs
|
|
||||||
const mlpOutputs = this._sequenceIML.getOutputs();
|
|
||||||
|
|
||||||
// 4. Map 16 outputs to N primitive params
|
|
||||||
const paramCount = this._chain.totalParamCount;
|
|
||||||
const mappedParams = map(mlpOutputs, paramCount);
|
|
||||||
|
|
||||||
// 5. Evaluate the chain to produce a pattern description
|
|
||||||
const patternDesc = this._chain.evaluate(mappedParams, this._stepCount, this._masterSeed);
|
|
||||||
|
|
||||||
// 6. Apply projection transforms
|
|
||||||
const projectedPattern = applyProjection(this._projectionChain, patternDesc);
|
|
||||||
|
|
||||||
// 7. Schedule the pattern on the clock
|
|
||||||
this._clock.schedulePattern(projectedPattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ML control ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** @returns {boolean} */
|
|
||||||
get isPlaying() {
|
|
||||||
return this._playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Bridge integration (private) ───────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle seq.noteOn events from the event bus.
|
|
||||||
* Converts [0,1] pitch to MIDI note number and forwards to C15.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision }
|
|
||||||
*/
|
|
||||||
_handleNoteOn(data) {
|
|
||||||
// pitch comes from the projection layer; after RangeMap it's already
|
|
||||||
// in MIDI note range (e.g., 48-84). Round to nearest integer.
|
|
||||||
const midiNote = Math.round(data.pitch) | 0;
|
|
||||||
const velocity = data.velocity;
|
|
||||||
|
|
||||||
// Clamp to valid MIDI range
|
|
||||||
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
|
|
||||||
const vel = velocity < 0 ? 0 : velocity > 1 ? 1 : velocity;
|
|
||||||
|
|
||||||
this._c15.noteOn(note, vel);
|
|
||||||
this._activeNotes.add(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle seq.noteOff events from the event bus.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Object} data - { pitch, velocity, stepIndex, time }
|
|
||||||
*/
|
|
||||||
_handleNoteOff(data) {
|
|
||||||
const midiNote = Math.round(data.pitch) | 0;
|
|
||||||
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
|
|
||||||
|
|
||||||
this._c15.noteOff(note);
|
|
||||||
this._activeNotes.delete(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release all currently active notes to avoid orphaned noteOns.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_releaseAllNotes() {
|
|
||||||
for (const note of this._activeNotes) {
|
|
||||||
this._c15.noteOff(note);
|
|
||||||
}
|
|
||||||
this._activeNotes.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
/**
|
|
||||||
* ShapeSeq Circular Step Visualizer
|
|
||||||
*
|
|
||||||
* Renders steps arranged in a circle (heptagon, tridecagon, etc.)
|
|
||||||
* with pitch mapped to radial distance, velocity to node size,
|
|
||||||
* and accent to color brightness.
|
|
||||||
*
|
|
||||||
* Designed for 60fps rendering — no allocations in the render loop.
|
|
||||||
* Port-ready: explicit state, no closures in hot paths.
|
|
||||||
*
|
|
||||||
* @module shapeseq/step-viz
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SEQ } from './event-bus.js';
|
|
||||||
|
|
||||||
// ── Constants (pre-allocated, shared across instances) ──────────────
|
|
||||||
|
|
||||||
const TWO_PI = Math.PI * 2;
|
|
||||||
const HALF_PI = Math.PI * 0.5;
|
|
||||||
|
|
||||||
// Color constants
|
|
||||||
const COLOR_INACTIVE = 'rgba(255, 255, 255, 0.15)';
|
|
||||||
const COLOR_ACTIVE = '#00ccff';
|
|
||||||
const COLOR_CURRENT = '#ff6a00';
|
|
||||||
const COLOR_ACCENT = '#ffcc00';
|
|
||||||
const COLOR_BG = '#0d0d0d';
|
|
||||||
|
|
||||||
// Glow colors (pre-computed rgba strings)
|
|
||||||
const GLOW_ACTIVE = 'rgba(0, 204, 255, 0.3)';
|
|
||||||
const GLOW_CURRENT = 'rgba(255, 106, 0, 0.4)';
|
|
||||||
const GLOW_ACCENT = 'rgba(255, 204, 0, 0.35)';
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
const PADDING_RATIO = 0.08; // canvas padding as fraction of min dimension
|
|
||||||
const OUTER_RADIUS_RATIO = 0.90; // outer ring at 90% of available radius
|
|
||||||
const INNER_RADIUS_RATIO = 0.30; // inner ring at 30% of available radius
|
|
||||||
|
|
||||||
// Node sizing
|
|
||||||
const NODE_MIN_RADIUS = 4;
|
|
||||||
const NODE_MAX_RADIUS = 18;
|
|
||||||
const NODE_OUTLINE_WIDTH = 1.5;
|
|
||||||
|
|
||||||
// Current-step indicator
|
|
||||||
const INDICATOR_EXTRA_RADIUS = 8;
|
|
||||||
const INDICATOR_LINE_WIDTH = 2;
|
|
||||||
|
|
||||||
// Center dot
|
|
||||||
const CENTER_DOT_RADIUS = 3;
|
|
||||||
|
|
||||||
// ── StepVisualizer ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export class StepVisualizer {
|
|
||||||
/**
|
|
||||||
* @param {{ canvas: HTMLCanvasElement, eventBus: import('./event-bus.js').EventBus }} opts
|
|
||||||
*/
|
|
||||||
constructor({ canvas, eventBus }) {
|
|
||||||
this._canvas = canvas;
|
|
||||||
this._ctx = canvas.getContext('2d');
|
|
||||||
this._bus = eventBus;
|
|
||||||
|
|
||||||
// State
|
|
||||||
this._pattern = null; // current pattern description
|
|
||||||
this._currentStep = -1; // playback position (-1 = none)
|
|
||||||
this._width = 0;
|
|
||||||
this._height = 0;
|
|
||||||
this._cx = 0; // center x
|
|
||||||
this._cy = 0; // center y
|
|
||||||
this._maxRadius = 0; // max ring radius in pixels
|
|
||||||
|
|
||||||
// Pre-allocated arrays to avoid per-frame allocation.
|
|
||||||
// Sized lazily when pattern is set.
|
|
||||||
this._nodeX = null; // Float64Array — screen x per step
|
|
||||||
this._nodeY = null; // Float64Array — screen y per step
|
|
||||||
this._nodeR = null; // Float64Array — rendered radius per step
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
this._tapCallback = null;
|
|
||||||
this._onPointerDown = this._handlePointerDown.bind(this);
|
|
||||||
|
|
||||||
// Event bus subscription
|
|
||||||
this._onStep = this._handleStep.bind(this);
|
|
||||||
this._bus.on(SEQ.STEP, this._onStep);
|
|
||||||
|
|
||||||
// Canvas interaction
|
|
||||||
this._canvas.addEventListener('pointerdown', this._onPointerDown);
|
|
||||||
|
|
||||||
// Initial sizing
|
|
||||||
this.resize(canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Public API ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the displayed pattern.
|
|
||||||
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc
|
|
||||||
*/
|
|
||||||
setPattern(patternDesc) {
|
|
||||||
this._pattern = patternDesc;
|
|
||||||
const count = patternDesc ? patternDesc.stepCount : 0;
|
|
||||||
|
|
||||||
// (Re)allocate coordinate buffers only when step count changes
|
|
||||||
if (!this._nodeX || this._nodeX.length !== count) {
|
|
||||||
this._nodeX = new Float64Array(count);
|
|
||||||
this._nodeY = new Float64Array(count);
|
|
||||||
this._nodeR = new Float64Array(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._computeLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the playback position.
|
|
||||||
* @param {number} index — step index (0-based), or -1 for none
|
|
||||||
*/
|
|
||||||
setCurrentStep(index) {
|
|
||||||
this._currentStep = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw one frame. Call from requestAnimationFrame.
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const ctx = this._ctx;
|
|
||||||
const w = this._width;
|
|
||||||
const h = this._height;
|
|
||||||
|
|
||||||
// Clear
|
|
||||||
ctx.fillStyle = COLOR_BG;
|
|
||||||
ctx.fillRect(0, 0, w, h);
|
|
||||||
|
|
||||||
if (!this._pattern || this._pattern.stepCount === 0) return;
|
|
||||||
|
|
||||||
const steps = this._pattern.steps;
|
|
||||||
const count = this._pattern.stepCount;
|
|
||||||
const cx = this._cx;
|
|
||||||
const cy = this._cy;
|
|
||||||
|
|
||||||
// Draw connecting ring (subtle guide circle at midpoint radius)
|
|
||||||
const midRadius = this._maxRadius * ((OUTER_RADIUS_RATIO + INNER_RADIUS_RATIO) * 0.5);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx, cy, midRadius, 0, TWO_PI);
|
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// Center dot
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx, cy, CENTER_DOT_RADIUS, 0, TWO_PI);
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Draw connector line from center to current step
|
|
||||||
if (this._currentStep >= 0 && this._currentStep < count) {
|
|
||||||
const si = this._currentStep;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cx, cy);
|
|
||||||
ctx.lineTo(this._nodeX[si], this._nodeY[si]);
|
|
||||||
ctx.strokeStyle = 'rgba(255, 106, 0, 0.2)';
|
|
||||||
ctx.lineWidth = INDICATOR_LINE_WIDTH;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw step nodes
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const step = steps[i];
|
|
||||||
const nx = this._nodeX[i];
|
|
||||||
const ny = this._nodeY[i];
|
|
||||||
const nr = this._nodeR[i];
|
|
||||||
const isCurrent = i === this._currentStep;
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
// Outer glow for current step
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(nx, ny, nr + INDICATOR_EXTRA_RADIUS, 0, TWO_PI);
|
|
||||||
ctx.fillStyle = GLOW_CURRENT;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step.trigger) {
|
|
||||||
// Glow behind active nodes
|
|
||||||
if (!isCurrent) {
|
|
||||||
const glowColor = step.accent ? GLOW_ACCENT : GLOW_ACTIVE;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(nx, ny, nr + 4, 0, TWO_PI);
|
|
||||||
ctx.fillStyle = glowColor;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filled node
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(nx, ny, nr, 0, TWO_PI);
|
|
||||||
if (isCurrent) {
|
|
||||||
ctx.fillStyle = COLOR_CURRENT;
|
|
||||||
} else if (step.accent) {
|
|
||||||
ctx.fillStyle = COLOR_ACCENT;
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = COLOR_ACTIVE;
|
|
||||||
}
|
|
||||||
ctx.fill();
|
|
||||||
} else {
|
|
||||||
// Dim outline only for untriggered steps
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(nx, ny, nr, 0, TWO_PI);
|
|
||||||
ctx.strokeStyle = isCurrent ? COLOR_CURRENT : COLOR_INACTIVE;
|
|
||||||
ctx.lineWidth = NODE_OUTLINE_WIDTH;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle canvas resize.
|
|
||||||
* @param {number} width — CSS pixels
|
|
||||||
* @param {number} height — CSS pixels
|
|
||||||
*/
|
|
||||||
resize(width, height) {
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
this._canvas.width = width * dpr;
|
|
||||||
this._canvas.height = height * dpr;
|
|
||||||
this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
||||||
|
|
||||||
this._width = width;
|
|
||||||
this._height = height;
|
|
||||||
this._cx = width * 0.5;
|
|
||||||
this._cy = height * 0.5;
|
|
||||||
|
|
||||||
const minDim = Math.min(width, height);
|
|
||||||
this._maxRadius = (minDim * 0.5) * (1 - PADDING_RATIO * 2);
|
|
||||||
|
|
||||||
this._computeLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a tap callback.
|
|
||||||
* @param {function(number): void} callback — receives step index
|
|
||||||
*/
|
|
||||||
onStepTap(callback) {
|
|
||||||
this._tapCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from event bus and remove DOM listeners.
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this._bus.off(SEQ.STEP, this._onStep);
|
|
||||||
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
||||||
this._tapCallback = null;
|
|
||||||
this._pattern = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Private ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-compute node positions from current pattern + canvas size.
|
|
||||||
* Called when pattern or size changes — NOT per frame.
|
|
||||||
*/
|
|
||||||
_computeLayout() {
|
|
||||||
if (!this._pattern || !this._nodeX) return;
|
|
||||||
|
|
||||||
const steps = this._pattern.steps;
|
|
||||||
const count = this._pattern.stepCount;
|
|
||||||
const cx = this._cx;
|
|
||||||
const cy = this._cy;
|
|
||||||
const maxR = this._maxRadius;
|
|
||||||
const outerR = maxR * OUTER_RADIUS_RATIO;
|
|
||||||
const innerR = maxR * INNER_RADIUS_RATIO;
|
|
||||||
const radiusRange = outerR - innerR;
|
|
||||||
|
|
||||||
// Angular step: start at top (-PI/2), go clockwise
|
|
||||||
const angleStep = TWO_PI / count;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const step = steps[i];
|
|
||||||
const angle = -HALF_PI + angleStep * i;
|
|
||||||
|
|
||||||
// Pitch -> radial distance: low pitch = outer, high pitch = inner
|
|
||||||
const pitchNorm = step.pitch; // 0 = low (outer), 1 = high (inner)
|
|
||||||
const r = outerR - pitchNorm * radiusRange;
|
|
||||||
|
|
||||||
this._nodeX[i] = cx + Math.cos(angle) * r;
|
|
||||||
this._nodeY[i] = cy + Math.sin(angle) * r;
|
|
||||||
|
|
||||||
// Velocity -> node size
|
|
||||||
this._nodeR[i] = NODE_MIN_RADIUS + step.velocity * (NODE_MAX_RADIUS - NODE_MIN_RADIUS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle seq.step events from the event bus.
|
|
||||||
*/
|
|
||||||
_handleStep(data) {
|
|
||||||
if (typeof data.stepIndex === 'number') {
|
|
||||||
this._currentStep = data.stepIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle pointer down on the canvas for tap interaction.
|
|
||||||
*/
|
|
||||||
_handlePointerDown(e) {
|
|
||||||
if (!this._tapCallback || !this._pattern) return;
|
|
||||||
|
|
||||||
const rect = this._canvas.getBoundingClientRect();
|
|
||||||
const px = e.clientX - rect.left;
|
|
||||||
const py = e.clientY - rect.top;
|
|
||||||
const count = this._pattern.stepCount;
|
|
||||||
|
|
||||||
// Find closest step within hit radius
|
|
||||||
let bestIdx = -1;
|
|
||||||
let bestDistSq = Infinity;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const dx = px - this._nodeX[i];
|
|
||||||
const dy = py - this._nodeY[i];
|
|
||||||
const distSq = dx * dx + dy * dy;
|
|
||||||
// Hit area is the node radius + some tolerance
|
|
||||||
const hitR = this._nodeR[i] + 12;
|
|
||||||
if (distSq < hitR * hitR && distSq < bestDistSq) {
|
|
||||||
bestDistSq = distSq;
|
|
||||||
bestIdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestIdx >= 0) {
|
|
||||||
this._tapCallback(bestIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -16,9 +16,9 @@ name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, port-solidjs]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, port-solidjs]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
2
.github/workflows/vcv-plugin.yml
vendored
2
.github/workflows/vcv-plugin.yml
vendored
|
|
@ -12,7 +12,7 @@ name: VCV Rack plugin (cross-platform)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, feat/manifold-mission, feat/vcv-dist]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'vcv/**'
|
- 'vcv/**'
|
||||||
- '.github/workflows/vcv-plugin.yml'
|
- '.github/workflows/vcv-plugin.yml'
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
# NISPS Core Extraction Plan
|
|
||||||
|
|
||||||
Extract a platform-agnostic C++20 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever.
|
|
||||||
|
|
||||||
> **Note**: Originally planned as C++17, upgraded to C++20 during implementation to use `std::span` for efficient array views.
|
|
||||||
|
|
||||||
## What This Is
|
|
||||||
|
|
||||||
NISPS core takes N input parameters (joystick position, sensor data, audio features) and maps them to M output parameters through an interactively-trained neural network. Users teach it by example: "when I'm here in input space, I want these output values."
|
|
||||||
|
|
||||||
## Dependencies to Remove
|
|
||||||
|
|
||||||
| Dependency | Replacement |
|
|
||||||
|------------|-------------|
|
|
||||||
| `Serial.print*` | Optional log callback |
|
|
||||||
| `queue_t` (Pico SDK) | Not needed (single-threaded) |
|
|
||||||
| `WString.h` (Arduino) | `std::string` |
|
|
||||||
| `__force_inline`, `AUDIO_MEM` | No-op macros |
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
nisps/
|
|
||||||
├── mlp.hpp # MLP implementation (from memlp, cleaned)
|
|
||||||
├── dataset.hpp # Training dataset
|
|
||||||
├── iml.hpp # Interactive ML engine (~200 lines)
|
|
||||||
└── voice_space.hpp # Optional: example parameter mappings
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core API
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
namespace nisps {
|
|
||||||
|
|
||||||
template<typename Float = float>
|
|
||||||
class IML {
|
|
||||||
public:
|
|
||||||
IML(size_t n_inputs, size_t n_outputs,
|
|
||||||
std::vector<size_t> hidden_layers = {10, 10, 14});
|
|
||||||
|
|
||||||
// Input
|
|
||||||
void set_input(size_t index, Float value);
|
|
||||||
void set_inputs(const Float* values, size_t count);
|
|
||||||
|
|
||||||
// Output (valid after process())
|
|
||||||
const Float* get_outputs() const;
|
|
||||||
size_t num_outputs() const;
|
|
||||||
|
|
||||||
// Runtime
|
|
||||||
void process(); // Run inference, call at control rate
|
|
||||||
|
|
||||||
// Training workflow
|
|
||||||
enum class Mode { Inference, Training };
|
|
||||||
void set_mode(Mode mode);
|
|
||||||
void save_example(); // Store current input→output as training pair
|
|
||||||
void clear_dataset();
|
|
||||||
void randomise_weights(); // For exploration in training mode
|
|
||||||
void train(); // Blocking, runs on current dataset
|
|
||||||
|
|
||||||
// Optional
|
|
||||||
using LogFn = void(*)(const char*);
|
|
||||||
void set_logger(LogFn fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace nisps
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "nisps/iml.hpp"
|
|
||||||
|
|
||||||
nisps::IML<float> iml(3, 24); // 3 inputs (x,y,z), 24 outputs
|
|
||||||
|
|
||||||
// Control loop
|
|
||||||
void update(float x, float y, float z) {
|
|
||||||
iml.set_input(0, x);
|
|
||||||
iml.set_input(1, y);
|
|
||||||
iml.set_input(2, z);
|
|
||||||
iml.process();
|
|
||||||
|
|
||||||
const float* params = iml.get_outputs();
|
|
||||||
my_synth.set_filter_cutoff(params[0] * 10000.f);
|
|
||||||
my_synth.set_resonance(params[1]);
|
|
||||||
// ... etc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Training (triggered by user interaction)
|
|
||||||
void on_user_saves_position() {
|
|
||||||
iml.save_example();
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_user_exits_training_mode() {
|
|
||||||
iml.set_mode(nisps::IML<>::Mode::Inference);
|
|
||||||
// This triggers training internally
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Voice Spaces (Optional)
|
|
||||||
|
|
||||||
Voice spaces are just functions that interpret the raw 0-1 output parameters. Not part of core, but useful as examples:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// User-defined mapping
|
|
||||||
void apply_neve_style(const float* params, MyChannelStrip& strip) {
|
|
||||||
strip.pre_gain = 0.5f + params[0] * params[0] * 4.f;
|
|
||||||
strip.hp_freq = 30.f + params[8] * params[8] * 270.f;
|
|
||||||
strip.comp_threshold = 20.f + params[10] * -40.f;
|
|
||||||
// ... etc
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phases
|
|
||||||
|
|
||||||
### Phase 1: Get memlp building standalone (1-2 days)
|
|
||||||
|
|
||||||
1. Copy `memlp` source into `nisps/`
|
|
||||||
2. Remove `Serial.print` calls (or stub them)
|
|
||||||
3. Remove Arduino `String` usage
|
|
||||||
4. Verify it compiles with g++/clang
|
|
||||||
|
|
||||||
### Phase 2: Wrap in IML interface (2-3 days)
|
|
||||||
|
|
||||||
1. Create `iml.hpp` with the API above
|
|
||||||
2. Port state machine logic from `IMLInterface.hpp`
|
|
||||||
3. Simple test: train on XOR, verify inference works
|
|
||||||
|
|
||||||
### Phase 3: Example integration (1-2 days)
|
|
||||||
|
|
||||||
1. Command-line example that reads CSV input, outputs CSV
|
|
||||||
2. Or: minimal JUCE/SDL example with mouse input
|
|
||||||
|
|
||||||
**Total: ~1 week to something usable**
|
|
||||||
|
|
||||||
## Later (only if needed)
|
|
||||||
|
|
||||||
- Model serialization (save/load trained weights)
|
|
||||||
- Thread-safe parameter updates
|
|
||||||
- Python bindings
|
|
||||||
- WASM build
|
|
||||||
|
|
@ -1,678 +0,0 @@
|
||||||
# NISPS Core Extraction - Task Graph
|
|
||||||
|
|
||||||
Atomic tasks for extracting nisps-core. Each task is standalone and requires no decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Repository Setup
|
|
||||||
|
|
||||||
### TASK-001: Create nisps-core directory structure
|
|
||||||
**Blocked by**: None
|
|
||||||
**Description**: Create the nisps-core directory with the required structure.
|
|
||||||
**Actions**:
|
|
||||||
1. Create directory: `nisps-core/`
|
|
||||||
2. Create directory: `nisps-core/include/nisps/`
|
|
||||||
3. Create directory: `nisps-core/test/`
|
|
||||||
4. Create directory: `nisps-core/examples/`
|
|
||||||
**Verification**: Directories exist.
|
|
||||||
|
|
||||||
### TASK-002: Create CMakeLists.txt for nisps-core
|
|
||||||
**Blocked by**: TASK-001
|
|
||||||
**Description**: Create a minimal CMakeLists.txt that builds the library as header-only with test target.
|
|
||||||
**Actions**: Create `nisps-core/CMakeLists.txt` with this exact content:
|
|
||||||
```cmake
|
|
||||||
cmake_minimum_required(VERSION 3.14)
|
|
||||||
project(nisps-core VERSION 0.1.0 LANGUAGES CXX)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
|
|
||||||
# Header-only library
|
|
||||||
add_library(nisps INTERFACE)
|
|
||||||
target_include_directories(nisps INTERFACE
|
|
||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
||||||
$<INSTALL_INTERFACE:include>
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
option(NISPS_BUILD_TESTS "Build tests" ON)
|
|
||||||
if(NISPS_BUILD_TESTS)
|
|
||||||
enable_testing()
|
|
||||||
add_subdirectory(test)
|
|
||||||
endif()
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
### TASK-003: Create test CMakeLists.txt
|
|
||||||
**Blocked by**: TASK-002
|
|
||||||
**Description**: Create test/CMakeLists.txt for building tests.
|
|
||||||
**Actions**: Create `nisps-core/test/CMakeLists.txt` with this exact content:
|
|
||||||
```cmake
|
|
||||||
add_executable(nisps_test main.cpp)
|
|
||||||
target_link_libraries(nisps_test PRIVATE nisps)
|
|
||||||
add_test(NAME nisps_test COMMAND nisps_test)
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
### TASK-004: Create placeholder test file
|
|
||||||
**Blocked by**: TASK-003
|
|
||||||
**Description**: Create a minimal test file that includes the main header.
|
|
||||||
**Actions**: Create `nisps-core/test/main.cpp` with this exact content:
|
|
||||||
```cpp
|
|
||||||
#include <nisps/nisps.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::cout << "nisps-core tests placeholder\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Extract MLP Core
|
|
||||||
|
|
||||||
### TASK-010: Copy Utils.h with Arduino code removed
|
|
||||||
**Blocked by**: TASK-001
|
|
||||||
**Description**: Copy `src/memlp/Utils.h` to `nisps-core/include/nisps/utils.hpp`, removing Arduino-specific code.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Remove the `#ifdef ARDUINO` block (lines related to ENABLE_SAVE)
|
|
||||||
3. Remove the `extern "C" int getentropy` declaration at the end
|
|
||||||
4. Change header guard from `UTILS_H` to `NISPS_UTILS_HPP`
|
|
||||||
5. Wrap everything in `namespace nisps { ... }`
|
|
||||||
|
|
||||||
**Changes to make** (be exact):
|
|
||||||
- Line 1: Change `#ifndef UTILS_H` to `#ifndef NISPS_UTILS_HPP`
|
|
||||||
- Line 2: Change `#define UTILS_H` to `#define NISPS_UTILS_HPP`
|
|
||||||
- Remove lines 38-45 (the `#if defined(_WIN32)...` and `#ifdef ARDUINO` ENABLE_SAVE blocks)
|
|
||||||
- After line 57 (`namespace utils {`), keep as-is (utils is a sub-namespace)
|
|
||||||
- Before the final `#endif`, add closing brace for nisps namespace
|
|
||||||
- Remove the last line `extern "C" int getentropy...`
|
|
||||||
- Add `namespace nisps {` after the includes, before `enum ACTIVATION_FUNCTIONS`
|
|
||||||
- Add `} // namespace nisps` before `#endif // NISPS_UTILS_HPP`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/utils.hpp`
|
|
||||||
|
|
||||||
### TASK-011: Copy Loss.h
|
|
||||||
**Blocked by**: TASK-001
|
|
||||||
**Description**: Copy `src/memlp/Loss.h` to `nisps-core/include/nisps/loss.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `LOSS_H` to `NISPS_LOSS_HPP`
|
|
||||||
3. Wrap everything in `namespace nisps { ... }`
|
|
||||||
4. Update include from `"Utils.h"` to `"utils.hpp"`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/loss.hpp`
|
|
||||||
|
|
||||||
### TASK-012: Copy Node.h
|
|
||||||
**Blocked by**: TASK-010
|
|
||||||
**Description**: Copy `src/memlp/Node.h` to `nisps-core/include/nisps/node.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `NODE_H` to `NISPS_NODE_HPP`
|
|
||||||
3. Wrap everything in `namespace nisps { ... }`
|
|
||||||
4. Update include from `"Utils.h"` to `"utils.hpp"`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/node.hpp`
|
|
||||||
|
|
||||||
### TASK-013: Copy Layer.h
|
|
||||||
**Blocked by**: TASK-012
|
|
||||||
**Description**: Copy `src/memlp/Layer.h` to `nisps-core/include/nisps/layer.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `LAYER_H` to `NISPS_LAYER_HPP`
|
|
||||||
3. Wrap everything in `namespace nisps { ... }`
|
|
||||||
4. Update include from `"Node.h"` to `"node.hpp"`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/layer.hpp`
|
|
||||||
|
|
||||||
### TASK-014: Copy Sample.h
|
|
||||||
**Blocked by**: TASK-001
|
|
||||||
**Description**: Copy `src/memlp/Sample.h` to `nisps-core/include/nisps/sample.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `SAMPLE_H` to `NISPS_SAMPLE_HPP`
|
|
||||||
3. Wrap everything in `namespace nisps { ... }`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/sample.hpp`
|
|
||||||
|
|
||||||
### TASK-015: Copy MLP.h with Arduino code removed
|
|
||||||
**Blocked by**: TASK-011, TASK-013, TASK-014
|
|
||||||
**Description**: Copy `src/memlp/MLP.h` to `nisps-core/include/nisps/mlp.hpp`, removing Arduino-specific code.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `MLP_H` to `NISPS_MLP_HPP`
|
|
||||||
3. **Remove lines 17-51** (the entire `#ifdef ARDUINO` block including Serial debug macros and SD includes)
|
|
||||||
4. Add these lines after the header guard instead:
|
|
||||||
```cpp
|
|
||||||
// Debug macros (no-op by default, override before including if needed)
|
|
||||||
#ifndef NISPS_DEBUG_PRINT
|
|
||||||
#define NISPS_DEBUG_PRINT(...)
|
|
||||||
#define NISPS_DEBUG_PRINTLN(...)
|
|
||||||
#define NISPS_DEBUG_PRINTF(...)
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
5. Update includes:
|
|
||||||
- `"Layer.h"` → `"layer.hpp"`
|
|
||||||
- `"Utils.h"` → `"utils.hpp"`
|
|
||||||
- `"Loss.h"` → `"loss.hpp"`
|
|
||||||
- `"Sample.h"` → `"sample.hpp"`
|
|
||||||
6. Remove `#if ENABLE_SAVE` blocks (lines 94-96, 99-113, 115-119) - remove the conditionals but **keep** the function declarations
|
|
||||||
7. Remove `#if ENABLE_SAVE_SD` block entirely (lines 115-119)
|
|
||||||
8. Wrap everything in `namespace nisps { ... }`
|
|
||||||
9. Replace `MLP_DEBUG_PRINT` with `NISPS_DEBUG_PRINT`, `MLP_DEBUG_PRINTLN` with `NISPS_DEBUG_PRINTLN`, `MLP_DEBUG_PRINTF` with `NISPS_DEBUG_PRINTF`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/mlp.hpp`
|
|
||||||
|
|
||||||
### TASK-016: Copy MLP.cpp with Arduino code removed
|
|
||||||
**Blocked by**: TASK-015
|
|
||||||
**Description**: Copy `src/memlp/MLP.cpp` to `nisps-core/include/nisps/mlp_impl.hpp` as inline implementation.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Remove `#include "MLP.h"` (it will be included from mlp.hpp)
|
|
||||||
3. Add header guard `#ifndef NISPS_MLP_IMPL_HPP` / `#define NISPS_MLP_IMPL_HPP`
|
|
||||||
4. Wrap everything in `namespace nisps { ... }`
|
|
||||||
5. Remove all `#ifdef ARDUINO` / `#if ENABLE_SAVE` / `#if ENABLE_SAVE_SD` conditional blocks
|
|
||||||
6. For functions inside removed conditionals: keep the functions but remove the `#if` guards
|
|
||||||
7. Replace any `Serial.print*` calls with `NISPS_DEBUG_PRINT*` equivalents
|
|
||||||
8. At the end of `nisps-core/include/nisps/mlp.hpp`, add: `#include "mlp_impl.hpp"`
|
|
||||||
|
|
||||||
**Verification**: A test file that instantiates `nisps::MLP<float>` compiles and links.
|
|
||||||
|
|
||||||
### TASK-017: Copy Dataset.hpp
|
|
||||||
**Blocked by**: TASK-001
|
|
||||||
**Description**: Copy `src/memlp/Dataset.hpp` to `nisps-core/include/nisps/dataset.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Change header guard from `__DATASET_HPP__` to `NISPS_DATASET_HPP`
|
|
||||||
3. Wrap everything in `namespace nisps { ... }`
|
|
||||||
|
|
||||||
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/dataset.hpp`
|
|
||||||
|
|
||||||
### TASK-018: Copy Dataset.cpp as inline implementation
|
|
||||||
**Blocked by**: TASK-017
|
|
||||||
**Description**: Copy `src/memlp/Dataset.cpp` to `nisps-core/include/nisps/dataset_impl.hpp`.
|
|
||||||
**Actions**:
|
|
||||||
1. Copy the file
|
|
||||||
2. Remove `#include "Dataset.hpp"`
|
|
||||||
3. Add header guard `#ifndef NISPS_DATASET_IMPL_HPP` / `#define NISPS_DATASET_IMPL_HPP`
|
|
||||||
4. Wrap everything in `namespace nisps { ... }`
|
|
||||||
5. Make all functions `inline`
|
|
||||||
6. At the end of `nisps-core/include/nisps/dataset.hpp`, add: `#include "dataset_impl.hpp"`
|
|
||||||
|
|
||||||
**Verification**: A test file that instantiates `nisps::Dataset` compiles and links.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: Create IML Interface
|
|
||||||
|
|
||||||
### TASK-020: Create iml.hpp header
|
|
||||||
**Blocked by**: TASK-016, TASK-018
|
|
||||||
**Description**: Create the main IML class header based on IMLInterface.hpp.
|
|
||||||
**Actions**: Create `nisps-core/include/nisps/iml.hpp` with this exact content:
|
|
||||||
```cpp
|
|
||||||
#ifndef NISPS_IML_HPP
|
|
||||||
#define NISPS_IML_HPP
|
|
||||||
|
|
||||||
#include "mlp.hpp"
|
|
||||||
#include "dataset.hpp"
|
|
||||||
#include <vector>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace nisps {
|
|
||||||
|
|
||||||
template<typename Float = float>
|
|
||||||
class IML {
|
|
||||||
public:
|
|
||||||
enum class Mode { Inference, Training };
|
|
||||||
|
|
||||||
using LogFn = void(*)(const char*);
|
|
||||||
|
|
||||||
IML(size_t n_inputs, size_t n_outputs,
|
|
||||||
std::vector<size_t> hidden_layers = {10, 10, 14},
|
|
||||||
size_t max_iterations = 1000,
|
|
||||||
Float learning_rate = 1.0f,
|
|
||||||
Float convergence_threshold = 0.00001f);
|
|
||||||
|
|
||||||
// Input
|
|
||||||
void set_input(size_t index, Float value);
|
|
||||||
void set_inputs(const Float* values, size_t count);
|
|
||||||
|
|
||||||
// Output (valid after process())
|
|
||||||
const Float* get_outputs() const;
|
|
||||||
size_t num_inputs() const { return n_inputs_; }
|
|
||||||
size_t num_outputs() const { return n_outputs_; }
|
|
||||||
|
|
||||||
// Runtime
|
|
||||||
void process();
|
|
||||||
|
|
||||||
// Training workflow
|
|
||||||
void set_mode(Mode mode);
|
|
||||||
Mode get_mode() const { return mode_; }
|
|
||||||
void save_example();
|
|
||||||
void clear_dataset();
|
|
||||||
void randomise_weights();
|
|
||||||
|
|
||||||
// Optional logging
|
|
||||||
void set_logger(LogFn fn) { log_fn_ = fn; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void log(const char* msg) const {
|
|
||||||
if (log_fn_) log_fn_(msg);
|
|
||||||
}
|
|
||||||
void train();
|
|
||||||
|
|
||||||
size_t n_inputs_;
|
|
||||||
size_t n_outputs_;
|
|
||||||
size_t max_iterations_;
|
|
||||||
Float learning_rate_;
|
|
||||||
Float convergence_threshold_;
|
|
||||||
|
|
||||||
Mode mode_ = Mode::Inference;
|
|
||||||
bool input_updated_ = false;
|
|
||||||
bool perform_inference_ = true;
|
|
||||||
|
|
||||||
std::vector<Float> input_state_;
|
|
||||||
std::vector<Float> output_state_;
|
|
||||||
|
|
||||||
std::unique_ptr<Dataset> dataset_;
|
|
||||||
std::unique_ptr<MLP<Float>> mlp_;
|
|
||||||
typename MLP<Float>::mlp_weights stored_weights_;
|
|
||||||
bool weights_randomised_ = false;
|
|
||||||
|
|
||||||
LogFn log_fn_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace nisps
|
|
||||||
|
|
||||||
#include "iml_impl.hpp"
|
|
||||||
|
|
||||||
#endif // NISPS_IML_HPP
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
### TASK-021: Create iml_impl.hpp implementation
|
|
||||||
**Blocked by**: TASK-020
|
|
||||||
**Description**: Create the IML implementation file based on IMLInterface.hpp logic.
|
|
||||||
**Actions**: Create `nisps-core/include/nisps/iml_impl.hpp` with this exact content:
|
|
||||||
```cpp
|
|
||||||
#ifndef NISPS_IML_IMPL_HPP
|
|
||||||
#define NISPS_IML_IMPL_HPP
|
|
||||||
|
|
||||||
namespace nisps {
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
IML<Float>::IML(size_t n_inputs, size_t n_outputs,
|
|
||||||
std::vector<size_t> hidden_layers,
|
|
||||||
size_t max_iterations,
|
|
||||||
Float learning_rate,
|
|
||||||
Float convergence_threshold)
|
|
||||||
: n_inputs_(n_inputs)
|
|
||||||
, n_outputs_(n_outputs)
|
|
||||||
, max_iterations_(max_iterations)
|
|
||||||
, learning_rate_(learning_rate)
|
|
||||||
, convergence_threshold_(convergence_threshold)
|
|
||||||
{
|
|
||||||
// Build layer sizes: input + hidden + output
|
|
||||||
const size_t kBias = 1;
|
|
||||||
std::vector<size_t> layer_sizes;
|
|
||||||
layer_sizes.push_back(n_inputs + kBias);
|
|
||||||
for (size_t h : hidden_layers) {
|
|
||||||
layer_sizes.push_back(h);
|
|
||||||
}
|
|
||||||
layer_sizes.push_back(n_outputs);
|
|
||||||
|
|
||||||
// Activation functions: RELU for hidden, SIGMOID for output
|
|
||||||
std::vector<ACTIVATION_FUNCTIONS> activations;
|
|
||||||
for (size_t i = 0; i < hidden_layers.size(); ++i) {
|
|
||||||
activations.push_back(RELU);
|
|
||||||
}
|
|
||||||
activations.push_back(SIGMOID);
|
|
||||||
|
|
||||||
dataset_ = std::make_unique<Dataset>();
|
|
||||||
mlp_ = std::make_unique<MLP<Float>>(
|
|
||||||
layer_sizes,
|
|
||||||
activations,
|
|
||||||
loss::LOSS_MSE,
|
|
||||||
false, // use_constant_weight_init
|
|
||||||
0.0f // constant_weight_init
|
|
||||||
);
|
|
||||||
|
|
||||||
input_state_.resize(n_inputs, static_cast<Float>(0.5));
|
|
||||||
output_state_.resize(n_outputs, static_cast<Float>(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::set_input(size_t index, Float value) {
|
|
||||||
if (index >= n_inputs_) return;
|
|
||||||
if (value < 0) value = 0;
|
|
||||||
if (value > 1) value = 1;
|
|
||||||
input_state_[index] = value;
|
|
||||||
input_updated_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::set_inputs(const Float* values, size_t count) {
|
|
||||||
for (size_t i = 0; i < count && i < n_inputs_; ++i) {
|
|
||||||
set_input(i, values[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
const Float* IML<Float>::get_outputs() const {
|
|
||||||
return output_state_.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::process() {
|
|
||||||
if (!perform_inference_ || !input_updated_) return;
|
|
||||||
|
|
||||||
// Add bias term
|
|
||||||
std::vector<Float> input_with_bias = input_state_;
|
|
||||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
|
||||||
|
|
||||||
// Run inference
|
|
||||||
std::vector<Float> output(n_outputs_);
|
|
||||||
mlp_->GetOutput(input_with_bias, &output);
|
|
||||||
|
|
||||||
output_state_ = output;
|
|
||||||
input_updated_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::set_mode(Mode mode) {
|
|
||||||
if (mode == Mode::Inference && mode_ == Mode::Training) {
|
|
||||||
train();
|
|
||||||
}
|
|
||||||
mode_ = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::save_example() {
|
|
||||||
// First call: stop inference, user will position output
|
|
||||||
if (perform_inference_) {
|
|
||||||
perform_inference_ = false;
|
|
||||||
log("Move to desired output position...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second call: store the example
|
|
||||||
dataset_->Add(input_state_, output_state_);
|
|
||||||
perform_inference_ = true;
|
|
||||||
|
|
||||||
// Run inference with new example
|
|
||||||
std::vector<Float> input_with_bias = input_state_;
|
|
||||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
|
||||||
std::vector<Float> output(n_outputs_);
|
|
||||||
mlp_->GetOutput(input_with_bias, &output);
|
|
||||||
output_state_ = output;
|
|
||||||
|
|
||||||
log("Example saved.");
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::clear_dataset() {
|
|
||||||
if (mode_ == Mode::Training) {
|
|
||||||
dataset_->Clear();
|
|
||||||
log("Dataset cleared.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::randomise_weights() {
|
|
||||||
if (mode_ == Mode::Training) {
|
|
||||||
stored_weights_ = mlp_->GetWeights();
|
|
||||||
mlp_->DrawWeights();
|
|
||||||
weights_randomised_ = true;
|
|
||||||
|
|
||||||
// Run inference to show effect
|
|
||||||
std::vector<Float> input_with_bias = input_state_;
|
|
||||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
|
||||||
std::vector<Float> output(n_outputs_);
|
|
||||||
mlp_->GetOutput(input_with_bias, &output);
|
|
||||||
output_state_ = output;
|
|
||||||
|
|
||||||
log("Weights randomised.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Float>
|
|
||||||
void IML<Float>::train() {
|
|
||||||
// Restore weights if they were randomised
|
|
||||||
if (weights_randomised_) {
|
|
||||||
mlp_->SetWeights(stored_weights_);
|
|
||||||
weights_randomised_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto features = dataset_->GetFeatures(true); // with bias
|
|
||||||
auto& labels = dataset_->GetLabels();
|
|
||||||
|
|
||||||
if (features.empty() || labels.empty()) {
|
|
||||||
log("Empty dataset, skipping training.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
typename MLP<Float>::training_pair_t training_data(features, labels);
|
|
||||||
|
|
||||||
log("Training...");
|
|
||||||
Float loss = mlp_->Train(
|
|
||||||
training_data,
|
|
||||||
learning_rate_,
|
|
||||||
static_cast<int>(max_iterations_),
|
|
||||||
convergence_threshold_,
|
|
||||||
false // output_log
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run inference after training
|
|
||||||
std::vector<Float> input_with_bias = input_state_;
|
|
||||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
|
||||||
std::vector<Float> output(n_outputs_);
|
|
||||||
mlp_->GetOutput(input_with_bias, &output);
|
|
||||||
output_state_ = output;
|
|
||||||
|
|
||||||
log("Training complete.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace nisps
|
|
||||||
|
|
||||||
#endif // NISPS_IML_IMPL_HPP
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: Create Main Header and Test
|
|
||||||
|
|
||||||
### TASK-030: Create main nisps.hpp header
|
|
||||||
**Blocked by**: TASK-020, TASK-021
|
|
||||||
**Description**: Create the main include header that exposes the public API.
|
|
||||||
**Actions**: Create `nisps-core/include/nisps/nisps.hpp` with this exact content:
|
|
||||||
```cpp
|
|
||||||
#ifndef NISPS_HPP
|
|
||||||
#define NISPS_HPP
|
|
||||||
|
|
||||||
#include "iml.hpp"
|
|
||||||
|
|
||||||
#endif // NISPS_HPP
|
|
||||||
```
|
|
||||||
**Verification**: File exists with exact content.
|
|
||||||
|
|
||||||
### TASK-031: Create XOR training test
|
|
||||||
**Blocked by**: TASK-030
|
|
||||||
**Description**: Replace the placeholder test with an XOR training test.
|
|
||||||
**Actions**: Replace `nisps-core/test/main.cpp` with this exact content:
|
|
||||||
```cpp
|
|
||||||
#include <nisps/nisps.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
void log_callback(const char* msg) {
|
|
||||||
std::cout << "[nisps] " << msg << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::cout << "=== NISPS Core Test: XOR Training ===\n\n";
|
|
||||||
|
|
||||||
// Create IML with 2 inputs, 1 output
|
|
||||||
nisps::IML<float> iml(2, 1, {4, 4}, 5000, 1.0f, 0.0001f);
|
|
||||||
iml.set_logger(log_callback);
|
|
||||||
|
|
||||||
// Enter training mode
|
|
||||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
|
||||||
|
|
||||||
// Train on XOR pattern
|
|
||||||
// (0,0) -> 0
|
|
||||||
iml.set_input(0, 0.0f);
|
|
||||||
iml.set_input(1, 0.0f);
|
|
||||||
iml.save_example(); // First call: stop inference
|
|
||||||
// Manually set output for this example (simulating user positioning)
|
|
||||||
// We access output_state_ indirectly by calling process after training
|
|
||||||
|
|
||||||
// For this test, we'll add examples directly to dataset
|
|
||||||
// This simulates the two-step save process
|
|
||||||
|
|
||||||
// Actually, let's test the full workflow properly:
|
|
||||||
// The IML class expects: save_example() twice per example
|
|
||||||
// 1. First call stops inference
|
|
||||||
// 2. User sets output position (we can't do this externally easily)
|
|
||||||
// 3. Second call stores input->output
|
|
||||||
|
|
||||||
// For testing, let's verify the basic inference works
|
|
||||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
|
||||||
|
|
||||||
// Test inference
|
|
||||||
iml.set_input(0, 0.0f);
|
|
||||||
iml.set_input(1, 0.0f);
|
|
||||||
iml.process();
|
|
||||||
float out_00 = iml.get_outputs()[0];
|
|
||||||
|
|
||||||
iml.set_input(0, 1.0f);
|
|
||||||
iml.set_input(1, 0.0f);
|
|
||||||
iml.process();
|
|
||||||
float out_10 = iml.get_outputs()[0];
|
|
||||||
|
|
||||||
iml.set_input(0, 0.0f);
|
|
||||||
iml.set_input(1, 1.0f);
|
|
||||||
iml.process();
|
|
||||||
float out_01 = iml.get_outputs()[0];
|
|
||||||
|
|
||||||
iml.set_input(0, 1.0f);
|
|
||||||
iml.set_input(1, 1.0f);
|
|
||||||
iml.process();
|
|
||||||
float out_11 = iml.get_outputs()[0];
|
|
||||||
|
|
||||||
std::cout << "\nInference results (untrained):\n";
|
|
||||||
std::cout << " (0,0) -> " << out_00 << "\n";
|
|
||||||
std::cout << " (1,0) -> " << out_10 << "\n";
|
|
||||||
std::cout << " (0,1) -> " << out_01 << "\n";
|
|
||||||
std::cout << " (1,1) -> " << out_11 << "\n";
|
|
||||||
|
|
||||||
std::cout << "\n=== Test passed: nisps-core compiles and runs ===\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Verification**: Test compiles and runs successfully.
|
|
||||||
|
|
||||||
### TASK-032: Build and run tests
|
|
||||||
**Blocked by**: TASK-031
|
|
||||||
**Description**: Build the library and run the test.
|
|
||||||
**Actions**:
|
|
||||||
```bash
|
|
||||||
cd nisps-core
|
|
||||||
mkdir -p build && cd build
|
|
||||||
cmake ..
|
|
||||||
make
|
|
||||||
ctest --output-on-failure
|
|
||||||
```
|
|
||||||
**Verification**: All commands succeed, test outputs "Test passed".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependency Graph (ASCII)
|
|
||||||
|
|
||||||
```
|
|
||||||
TASK-001 (create dirs)
|
|
||||||
│
|
|
||||||
├──> TASK-002 (CMakeLists.txt)
|
|
||||||
│ │
|
|
||||||
│ └──> TASK-003 (test CMakeLists.txt)
|
|
||||||
│ │
|
|
||||||
│ └──> TASK-004 (placeholder test)
|
|
||||||
│
|
|
||||||
├──> TASK-010 (utils.hpp)
|
|
||||||
│ │
|
|
||||||
│ └──> TASK-011 (loss.hpp)
|
|
||||||
│ │ │
|
|
||||||
│ └──> TASK-012 (node.hpp)
|
|
||||||
│ │
|
|
||||||
│ └──> TASK-013 (layer.hpp)
|
|
||||||
│
|
|
||||||
├──> TASK-014 (sample.hpp)
|
|
||||||
│
|
|
||||||
├──> TASK-017 (dataset.hpp)
|
|
||||||
│ │
|
|
||||||
│ └──> TASK-018 (dataset_impl.hpp)
|
|
||||||
│
|
|
||||||
└───────────────────────────────────────┐
|
|
||||||
│
|
|
||||||
TASK-011 + TASK-013 + TASK-014 ────────> TASK-015 (mlp.hpp)
|
|
||||||
│
|
|
||||||
└──> TASK-016 (mlp_impl.hpp)
|
|
||||||
│
|
|
||||||
TASK-016 + TASK-018 ───────────────────────────> TASK-020 (iml.hpp)
|
|
||||||
│
|
|
||||||
└──> TASK-021 (iml_impl.hpp)
|
|
||||||
│
|
|
||||||
TASK-020 + TASK-021 ───────────────────────────────────> TASK-030 (nisps.hpp)
|
|
||||||
│
|
|
||||||
└──> TASK-031 (test)
|
|
||||||
│
|
|
||||||
└──> TASK-032 (build & run)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Path
|
|
||||||
|
|
||||||
The minimum path to a working library:
|
|
||||||
1. TASK-001 → TASK-002 → TASK-003 → TASK-004 (setup)
|
|
||||||
2. TASK-010 → TASK-012 → TASK-013 (utils → node → layer)
|
|
||||||
3. TASK-011 (loss, parallel with above)
|
|
||||||
4. TASK-014 (sample, parallel)
|
|
||||||
5. TASK-015 → TASK-016 (mlp)
|
|
||||||
6. TASK-017 → TASK-018 (dataset, parallel with 3-5)
|
|
||||||
7. TASK-020 → TASK-021 (iml)
|
|
||||||
8. TASK-030 → TASK-031 → TASK-032 (header + test + build)
|
|
||||||
|
|
||||||
**Estimated time**: 4-6 hours for a capable agent.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Checklist
|
|
||||||
|
|
||||||
After all tasks complete, these files should exist:
|
|
||||||
```
|
|
||||||
nisps-core/
|
|
||||||
├── CMakeLists.txt
|
|
||||||
├── include/
|
|
||||||
│ └── nisps/
|
|
||||||
│ ├── nisps.hpp
|
|
||||||
│ ├── iml.hpp
|
|
||||||
│ ├── iml_impl.hpp
|
|
||||||
│ ├── mlp.hpp
|
|
||||||
│ ├── mlp_impl.hpp
|
|
||||||
│ ├── dataset.hpp
|
|
||||||
│ ├── dataset_impl.hpp
|
|
||||||
│ ├── layer.hpp
|
|
||||||
│ ├── node.hpp
|
|
||||||
│ ├── loss.hpp
|
|
||||||
│ ├── sample.hpp
|
|
||||||
│ └── utils.hpp
|
|
||||||
├── test/
|
|
||||||
│ ├── CMakeLists.txt
|
|
||||||
│ └── main.cpp
|
|
||||||
└── examples/
|
|
||||||
```
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
,ck84,dharma-the-swan,04.11.2025 16:41,file:///home/ck84/snap/libreoffice/358/.config/libreoffice/4;
|
|
||||||
|
|
@ -1,424 +0,0 @@
|
||||||
Name,Component_1,Component_2
|
|
||||||
Betelginkgo,Betelgeuse,Ginkgo
|
|
||||||
Pleiadedar,Pleiades,Cedar
|
|
||||||
Helixypress,Helix Nebula,Cypress
|
|
||||||
Rigelmwood,Rigel,Elmwood
|
|
||||||
Sombreroak,Sombrero Galaxy,Oak
|
|
||||||
Hyadehorn,Hyades,Hawthorn
|
|
||||||
Triangularch,Triangulum,Larch
|
|
||||||
Vegalder,Vega,Alder
|
|
||||||
Whirlpoplar,Whirpool Galaxy,Poplar
|
|
||||||
Siriuswood,Sirius,Basswood
|
|
||||||
Oortcacia,Oort Cloud,Acacia
|
|
||||||
Denebony,Deneb,Ebony
|
|
||||||
Cartwhillow,Cartwheel Galaxy,Willow
|
|
||||||
Polariswood,Polaris,Ironwood
|
|
||||||
Craborwood,Crab Nebula,Hornbeam
|
|
||||||
Aldebaranyan,Aldebaran,Banyan
|
|
||||||
Pinwheelnut,Pinwheel Galaxy,Walnut
|
|
||||||
Antaresnut,Antares,Chestnut
|
|
||||||
Kuiperlpa,Kuiper Belt,Catalpa
|
|
||||||
Arcturuspruce,Arcturus,Spruce
|
|
||||||
Pegasafras,Pegasus,Sassafras
|
|
||||||
Capellaberry,Capella,Mulberry
|
|
||||||
Lyracacia,Lyra,Acacia
|
|
||||||
Polluxwood,Pollux,Boxwood
|
|
||||||
Scorpiumac,Scorpius,Sumac
|
|
||||||
Castorwood,Castor,Cottonwood
|
|
||||||
Dracodar,Draco,Cedar
|
|
||||||
Regulushua,Regulus,Joshua
|
|
||||||
Aquilnut,Aquila,Walnut
|
|
||||||
Spicamore,Spica,Sycamore
|
|
||||||
Cygnusswood,Cygnus,Sasswood
|
|
||||||
Proceyon,Procyon,Pecan
|
|
||||||
Perseusqoia,Perseus,Sequoia
|
|
||||||
Sagittalpa,Sagittarius,Catalpa
|
|
||||||
Tauruspen,Taurus,Aspen
|
|
||||||
Geminowan,Gemini,Rowan
|
|
||||||
Canceria,Cancer,Wisteria
|
|
||||||
Leogany,Leo,Mahogany
|
|
||||||
Virgoak,Virgo,Oak
|
|
||||||
Libraobab,Libra,Baobab
|
|
||||||
Ariescalyptus,Aries,Eucalyptus
|
|
||||||
Capricornia,Capricorn,Magnolia
|
|
||||||
Aquariusqoia,Aquarius,Sequoia
|
|
||||||
Piscesimmon,Pisces,Persimmon
|
|
||||||
Omeganolia,Omega Centauri,Magnolia
|
|
||||||
Beehivewood,Beehive Cluster,Olivewood
|
|
||||||
Pillarspress,Pillars of Creation,Cypress
|
|
||||||
Greatwillow,Great Wall,Willow
|
|
||||||
Localboobao,Local Bubble,Baobab
|
|
||||||
Termishoak,Termination Shock,Oak
|
|
||||||
Bowshocknut,Bow Shock,Chestnut
|
|
||||||
Stellarix,Stellar Wind,Tamarix
|
|
||||||
Heliopaulownia,Heliopause,Paulownia
|
|
||||||
Rochemewood,Roche Limit,Limewood
|
|
||||||
Eventhornbeam,Event Horizon,Hornbeam
|
|
||||||
Accretioak,Accretion Disk,Oak
|
|
||||||
Protoplanetree,Protoplanetary Disk,Planetree
|
|
||||||
Lagranginkgo,Lagrange Point,Ginkgo
|
|
||||||
Trojaniper,Trojan,Juniper
|
|
||||||
Gammarchwood,Gamma Ray Burst,Birchwood
|
|
||||||
Hypernowan,Hypernova,Rowan
|
|
||||||
Whitedwillow,White Dwarf,Willow
|
|
||||||
Redgianter,Red Giant,Red Alder
|
|
||||||
Superclusternut,Supercluster,Butternut
|
|
||||||
Filamenteakwood,Filament,Teakwood
|
|
||||||
Voidglass,Void,Douglass Fir
|
|
||||||
Greywallnut,Great Wall,Wallnut
|
|
||||||
Comettree,Comet,Peppertree
|
|
||||||
Asteroider,Asteroid Belt,Elder
|
|
||||||
Helixelkova,Helix Nebula,Zelkova
|
|
||||||
Orionwood,Orion Nebula,Redwood
|
|
||||||
Cassiopaple,Cassiopeia,Apple
|
|
||||||
Ursajortree,Ursa Major,Majortree
|
|
||||||
Ursaminorwood,Ursa Minor,Dogwood
|
|
||||||
Bristlecrab,Bristlecone Pine,Crab Nebula
|
|
||||||
Metasequoyasar,Metasequoia,Quasar
|
|
||||||
Monkeypleiades,Monkey Puzzle,Pleiades
|
|
||||||
Dragontree,Dragon Tree,Draco
|
|
||||||
Boojumbrero,Boojum,Sombrero Galaxy
|
|
||||||
Quiverwheel,Quiver Tree,Pinwheel
|
|
||||||
Joshuapiter,Joshua Tree,Jupiter
|
|
||||||
Wolleminebula,Wollemi Pine,Nebula
|
|
||||||
Dawndromeda,Dawn Redwood,Andromeda
|
|
||||||
Baldcygnuss,Bald Cypress,Cygnus
|
|
||||||
Zelkovega,Zelkova,Vega
|
|
||||||
Hornbeamstar,Hornbeam,Barnstar
|
|
||||||
Rowantares,Rowan,Antares
|
|
||||||
Basswoodburst,Basswood,Gamma Burst
|
|
||||||
Calabashea,Calabash,Cassiopeia
|
|
||||||
Neemeda,Neem,Andromeda
|
|
||||||
Salirius,Sal,Sirius
|
|
||||||
Tooniverse,Toon,Universe
|
|
||||||
Cassiapeia,Cassia,Cassiopeia
|
|
||||||
Yellowoodrion,Yellowwood,Orion
|
|
||||||
Blackwoodhole,Blackwood,Black Hole
|
|
||||||
Ironwoodsar,Ironwood,Quasar
|
|
||||||
Satinwhirl,Satinwood,Whirlpool
|
|
||||||
Poplarsar,Poplar,Pulsar
|
|
||||||
Cottonwoodway,Cottonwood,Milky Way
|
|
||||||
Sumacnova,Sumac,Supernova
|
|
||||||
Hazeleius,Hazel,Pegasus
|
|
||||||
Elderstar,Elder,Barnstar
|
|
||||||
Rigelbark,Rigel,Bark
|
|
||||||
Sirillow,Sirius,Willow
|
|
||||||
Altairix,Altair,Tamarix
|
|
||||||
Denebula,Deneb,Nebula
|
|
||||||
Polarispine,Polaris,Pine
|
|
||||||
Betelpine,Betelgeuse,Bristlecone
|
|
||||||
Rigellarch,Rigel,Larch
|
|
||||||
Vegamore,Vega,Sycamore
|
|
||||||
Altairnut,Altair,Chestnut
|
|
||||||
Arcturuscacia,Arcturus,Acacia
|
|
||||||
Capellaple,Capella,Apple
|
|
||||||
Polluxdogwood,Pollux,Dogwood
|
|
||||||
Castorong,Castor,Camphorwood
|
|
||||||
Regulusqoia,Regulus,Sequoia
|
|
||||||
Spicape,Spica,Grape
|
|
||||||
Procyonwood,Procyon,Sandalwood
|
|
||||||
Aldebaranyan,Aldebaran,Banyan
|
|
||||||
Antarespress,Antares,Cypress
|
|
||||||
Pleiadwood,Pleiades,Cedarwood
|
|
||||||
Hyadesmac,Hyades,Sumac
|
|
||||||
Beehivewood,Beehive,Olivewood
|
|
||||||
Orionniper,Orion,Juniper
|
|
||||||
Cassiopear,Cassiopeia,Pear
|
|
||||||
Dracoroak,Draco,Cork Oak
|
|
||||||
Lyralnut,Lyra,Walnut
|
|
||||||
Cygnust,Cygnus,Locust
|
|
||||||
Aquillow,Aquila,Willow
|
|
||||||
Perseusnut,Perseus,Butternut
|
|
||||||
Pegasuspen,Pegasus,Aspen
|
|
||||||
Andromelm,Andromeda,Elm
|
|
||||||
Triangulfir,Triangulum,Fir
|
|
||||||
Sombreruce,Sombrero,Spruce
|
|
||||||
Whirlpinecone,Whirlpool,Pine
|
|
||||||
Cartwheelnut,Cartwheel,Hazelnut
|
|
||||||
Helixelm,Helix,Elm
|
|
||||||
Crabapple,Crab Nebula,Crabapple
|
|
||||||
Pillarspen,Pillars,Aspen
|
|
||||||
Sagittariuswood,Sagittarius,Sandalwood
|
|
||||||
Scorpiuspen,Scorpius,Aspen
|
|
||||||
Taurusqoia,Taurus,Sequoia
|
|
||||||
Geminoak,Gemini,Oak
|
|
||||||
Cancerdar,Cancer,Cedar
|
|
||||||
Leowood,Leo,Boxwood
|
|
||||||
Virgowan,Virgo,Rowan
|
|
||||||
Librasswood,Libra,Basswood
|
|
||||||
Ariesnut,Aries,Chestnut
|
|
||||||
Capricornbeam,Capricorn,Hornbeam
|
|
||||||
Aquariuslder,Aquarius,Alder
|
|
||||||
Piscespress,Pisces,Cypress
|
|
||||||
Magnetardwood,Magnetar,Hardwood
|
|
||||||
Pulsarwood,Pulsar,Rosewood
|
|
||||||
Quasarberry,Quasar,Elderberry
|
|
||||||
Supernovakia,Supernova,Plumeria
|
|
||||||
Hypernovania,Hypernova,Paulownia
|
|
||||||
Blackholly,Black Hole,Holly
|
|
||||||
Whitedwarfpine,White Dwarf,White Pine
|
|
||||||
Redgiantwood,Red Giant,Giantwood
|
|
||||||
Bluegiganticacia,Blue Giant,Acacia
|
|
||||||
Neutronia,Neutron Star,Catalpa
|
|
||||||
Browndwarfnut,Brown Dwarf,Butternut
|
|
||||||
Protostarfir,Protostar,Fir
|
|
||||||
Mainsequoia,Main Sequence,Sequoia
|
|
||||||
Subergiantree,Supergiant,Tree
|
|
||||||
Yorkcloud,Oort Cloud,Cork
|
|
||||||
Kuiperlpa,Kuiper Belt,Catalpa
|
|
||||||
Heliospherical,Heliosphere,Spherical Tree
|
|
||||||
Heliopaulogany,Heliopause,Mahogany
|
|
||||||
Terminationoak,Termination Shock,Oak
|
|
||||||
Bowshockberry,Bow Shock,Hackberry
|
|
||||||
Stellarwindwood,Stellar Wind,Driftwood
|
|
||||||
Solarwindowan,Solar Wind,Rowan
|
|
||||||
Interstellarch,Interstellar Medium,Larch
|
|
||||||
Circumstellarix,Circumstellar Disk,Tamarix
|
|
||||||
Protoplanetree,Protoplanetary Disk,Plane Tree
|
|
||||||
Accretionoak,Accretion Disk,Oak
|
|
||||||
Eventhorizonwood,Event Horizon,Ironwood
|
|
||||||
Schwarzschillow,Schwarzschild Radius,Willow
|
|
||||||
Singularsimmon,Singularity,Persimmon
|
|
||||||
Rochelimewood,Roche Limit,Limewood
|
|
||||||
Lagrangewood,Lagrange Point,Rangewood
|
|
||||||
Trojantree,Trojan Asteroids,Tree
|
|
||||||
Hildazel,Hilda Group,Hazel
|
|
||||||
Amoribark,Amor Asteroids,Bark
|
|
||||||
Atenberry,Aten Asteroids,Elderberry
|
|
||||||
Apollomeria,Apollo Asteroids,Plumeria
|
|
||||||
Gammaburst Ash,Gamma Ray Burst,Ash
|
|
||||||
Xraybirch,X-ray Binary,Birch
|
|
||||||
Millisecwood,Millisecond Pulsar,Redwood
|
|
||||||
Magnetarch,Magnetar,Larch
|
|
||||||
Softgammawood,Soft Gamma Repeater,Softwood
|
|
||||||
Superclusternut,Supercluster,Clusternut
|
|
||||||
Galaxyclusterpine,Galaxy Cluster,Pine
|
|
||||||
Galaxygroupfir,Galaxy Group,Fir
|
|
||||||
Localgrowan,Local Group,Rowan
|
|
||||||
Virgoclusteroak,Virgo Cluster,Oak
|
|
||||||
Comaberrynicaceae,Coma Berenices,Berry
|
|
||||||
Greatwallnut,Great Wall,Wallnut
|
|
||||||
Sloangreatwillow,Sloan Great Wall,Willow
|
|
||||||
Herculessuperia,Hercules Supercluster,Wisteria
|
|
||||||
Shapleypress,Shapley Supercluster,Cypress
|
|
||||||
Laniakeroak,Laniakea Supercluster,Kauri Oak
|
|
||||||
Cosmicwebwood,Cosmic Web,Webwood
|
|
||||||
Filamentree,Filament,Tree
|
|
||||||
Voidwood,Void,Driftwood
|
|
||||||
Boötesvoidwood,Boötes Void,Olivewood
|
|
||||||
Sculptornut,Sculptor Void,Coconut
|
|
||||||
Greywallash,Great Wall,Ash
|
|
||||||
Localbubbaobab,Local Bubble,Baobab
|
|
||||||
Loopicacia,Local Interstellar Cloud,Acacia
|
|
||||||
Gouldspen,Gould Belt,Aspen
|
|
||||||
Orionspurmac,Orion Spur,Sumac
|
|
||||||
Sagittariusarmwood,Sagittarius Arm,Armwood
|
|
||||||
Perseusarmpine,Perseus Arm,Pine
|
|
||||||
Outerarmoak,Outer Arm,Oak
|
|
||||||
Scutumcentauruswood,Scutum-Centaurus Arm,Scutumwood
|
|
||||||
Normamalder,Norma Arm,Alder
|
|
||||||
Barredspiralex,Barred Spiral,Ilex
|
|
||||||
Ellipticacacia,Elliptical Galaxy,Acacia
|
|
||||||
Irregularnut,Irregular Galaxy,Butternut
|
|
||||||
Lenticularcorn,Lenticular Galaxy,Acorn
|
|
||||||
Dwarfnut,Dwarf Galaxy,Chestnut
|
|
||||||
Satelliteelm,Satellite Galaxy,Elm
|
|
||||||
Starburstfir,Starburst Galaxy,Fir
|
|
||||||
Seyfertree,Seyfert Galaxy,Feather Tree
|
|
||||||
Blazarwood,Blazar,Rosewood
|
|
||||||
Radiogalaxypress,Radio Galaxy,Cypress
|
|
||||||
Activenutcleus,Active Galactic Nucleus,Nut
|
|
||||||
Quasartree,Quasar,Quandong Tree
|
|
||||||
Ultraluminouswood,Ultraluminous Galaxy,Luminous Tree
|
|
||||||
Luminousredgiant,Luminous Red Galaxy,Red Giant Sequoia
|
|
||||||
Greenpeachia,Green Pea Galaxy,Peach
|
|
||||||
Polarringwood,Polar Ring Galaxy,Ringwood
|
|
||||||
Shellgalaxash,Shell Galaxy,Ash
|
|
||||||
Ringalaxypress,Ring Galaxy,Cypress
|
|
||||||
Hoagsobjectoak,Hoag's Object,Oak
|
|
||||||
Mayallsobjectree,Mayall's Object,Tree
|
|
||||||
Tadpolgalaxillow,Tadpole Galaxy,Willow
|
|
||||||
Cometgalaxalder,Comet Galaxy,Alder
|
|
||||||
Sunflowernut,Sunflower Galaxy,Sunflower
|
|
||||||
Blackeyewood,Black Eye Galaxy,Blackwood
|
|
||||||
Bodesgalaxnut,Bode's Galaxy,Coconut
|
|
||||||
Cigargalaxpress,Cigar Galaxy,Cypress
|
|
||||||
Mousesgalaxypen,Mice Galaxies,Aspen
|
|
||||||
Antennaegalaxynt,Antennae Galaxies,Antwood
|
|
||||||
Polarisstwood,Polaris,Sasswood
|
|
||||||
Rigwood,Rigel,Dogwood
|
|
||||||
Canopusnut,Canopus,Coconut
|
|
||||||
Vegamore,Vega,Sycamore
|
|
||||||
Capellalnut,Capella,Walnut
|
|
||||||
Rigelberry,Rigel,Mulberry
|
|
||||||
Procyonwood,Procyon,Cottonwood
|
|
||||||
Achernarnut,Achernar,Butternut
|
|
||||||
Betelgeuswood,Betelgeuse,Geuswood
|
|
||||||
Hadarcedarwood,Hadar,Cedarwood
|
|
||||||
Altairix,Altair,Tamarix
|
|
||||||
Alderaminwood,Alderamin,Alderwood
|
|
||||||
Denebolive,Denebola,Olive
|
|
||||||
Mimosacia,Mimosa,Acacia
|
|
||||||
Regulenut,Regulus,Filbert
|
|
||||||
Adharalnut,Adhara,Walnut
|
|
||||||
Shahelmwood,Shaula,Elm
|
|
||||||
Gacruxifir,Gacrux,Fir
|
|
||||||
Bellatrixnut,Bellatrix,Hazelnut
|
|
||||||
Elnathnut,Elnath,Chestnut
|
|
||||||
Miaplacidiumwood,Miaplacidus,Palladium
|
|
||||||
Alnitaknut,Alnitak,Coconut
|
|
||||||
Alnilamspen,Alnilam,Aspen
|
|
||||||
Aliothogany,Alioth,Mahogany
|
|
||||||
Mirzamberry,Mirzam,Mulberry
|
|
||||||
Alkaidnut,Alkaid,Butternut
|
|
||||||
Atriarch,Atria,Larch
|
|
||||||
Alhenawood,Alhena,Alderwood
|
|
||||||
Peacockahogany,Peacock,Mahogany
|
|
||||||
Mirfaknut,Mirfak,Coconut
|
|
||||||
Wezenalder,Wezen,Alder
|
|
||||||
Sargassage,Sargas,Sage
|
|
||||||
Menkarberry,Menkar,Elderberry
|
|
||||||
Eniffir,Enif,Fir
|
|
||||||
Denebalgedinut,Deneb Algedi,Butternut
|
|
||||||
Zubenhaknutbi,Zuben Hakrabi,Butternut
|
|
||||||
Alshainalnut,Alshain,Walnut
|
|
||||||
Rastabantamarind,Rastaban,Tamarind
|
|
||||||
Markabwood,Markab,Markwood
|
|
||||||
Schedarogany,Schedar,Mahogany
|
|
||||||
Almaalnutak,Almaak,Walnut
|
|
||||||
Rasalgetash,Rasalgethi,Ash
|
|
||||||
Thubanyan,Thuban,Banyan
|
|
||||||
Kausafras,Kaus Australis,Sassafras
|
|
||||||
Nunkirnut,Nunki,Coconut
|
|
||||||
Sabikwood,Sabik,Rosewood
|
|
||||||
Menkalnutnan,Menkalnan,Butternut
|
|
||||||
Asteropewood,Asterope,Redwood
|
|
||||||
Atlaspen,Atlas,Aspen
|
|
||||||
Electralder,Electra,Alder
|
|
||||||
Maiagany,Maia,Mahogany
|
|
||||||
Meropewood,Merope,Olivewood
|
|
||||||
Taugetamarix,Taygeta,Tamarix
|
|
||||||
Pleionetree,Pleione,Tree
|
|
||||||
Celaenoak,Celaeno,Oak
|
|
||||||
Alcyonedar,Alcyone,Cedar
|
|
||||||
Sterropopelnut,Sterope,Pecanut
|
|
||||||
Proximaple,Proxima Centauri,Apple
|
|
||||||
Alphacenternut,Alpha Centauri,Butternut
|
|
||||||
Barnardstaroak,Barnard's Star,Oak
|
|
||||||
Wolfwood,Wolf 359,Dogwood
|
|
||||||
Lalandelarch,Lalande 21185,Larch
|
|
||||||
Siriuswood,Sirius,Basswood
|
|
||||||
Luytenbirch,Luyten 726-8,Birch
|
|
||||||
Rossnut,Ross 154,Coconut
|
|
||||||
Rossberry,Ross 248,Elderberry
|
|
||||||
Epsilonwood,Epsilon Eridani,Sandalwood
|
|
||||||
Lacaillemwood,Lacaille 9352,Elmwood
|
|
||||||
Rosswood,Ross 128,Cottonwood
|
|
||||||
EZaquariuspen,EZ Aquarii,Aspen
|
|
||||||
Procyonix,Procyon,Phoenix
|
|
||||||
Sixtyonecygnust,61 Cygni,Locust
|
|
||||||
Struvewood,Struve 2398,Rosewood
|
|
||||||
Groombridgefir,Groombridge 34,Fir
|
|
||||||
Epsilondiwood,Epsilon Indi,Indigowood
|
|
||||||
DXcancroak,DX Cancri,Oak
|
|
||||||
Taucetialder,Tau Ceti,Alder
|
|
||||||
Luytenut,Luyten's Star,Butternut
|
|
||||||
Teegardeenspen,Teegarden's Star,Aspen
|
|
||||||
Kapteynsnut,Kapteyn's Star,Coconut
|
|
||||||
Lacaillewood,Lacaille 8760,Lacewood
|
|
||||||
Krugernut,Krüger 60,Coconut
|
|
||||||
Rosspress,Ross 614,Cypress
|
|
||||||
Wolfwoodogwood,Wolf 1061,Dogwood
|
|
||||||
Vanmaanensnut,Van Maanen's Star,Chestnut
|
|
||||||
Gliesetree,Gliese 1,Tree
|
|
||||||
Gielenseginkgo,Gliese 876,Ginkgo
|
|
||||||
Giseewood,Gliese 581,Rosewood
|
|
||||||
TRappistree,TRAPPIST-1,Trapwood
|
|
||||||
Gliesecherry,Gliese 667,Cherry
|
|
||||||
Glisewood,Gliese 832,Walnutwood
|
|
||||||
Glaesewood,Gliese 163,Olivewood
|
|
||||||
Gleasewood,Gliese 180,Teaselwood
|
|
||||||
Lessewood,Gliese 221,Lesserwood
|
|
||||||
Sleewood,Gliese 317,Sleekwood
|
|
||||||
Liewood,Gliese 357,Tree
|
|
||||||
Gleetree,Gliese 433,Tree
|
|
||||||
Graywood,Gliese 436,Graywood
|
|
||||||
Greatwood,Gliese 504,Greatwood
|
|
||||||
Greenwood,Gliese 570,Greenwood
|
|
||||||
Greshwood,Gliese 625,Ashwood
|
|
||||||
Gladewood,Gliese 649,Glade
|
|
||||||
Glowwood,Gliese 674,Glowwood
|
|
||||||
Gradewood,Gliese 682,Gradewood
|
|
||||||
Gracewood,Gliese 686,Gracewood
|
|
||||||
Grandewood,Gliese 785,Grandewood
|
|
||||||
Glintwood,Gliese 806,Flintwood
|
|
||||||
Grimwood,Gliese 832,Grimwood
|
|
||||||
Horsehead Hickory,Horsehead Nebula,Hickory
|
|
||||||
Eaglenberry,Eagle Nebula,Elderberry
|
|
||||||
Trifidogwood,Trifid Nebula,Dogwood
|
|
||||||
Lagoonwood,Lagoon Nebula,Lagoonwood
|
|
||||||
Rosettamarisk,Rosette Nebula,Tamarisk
|
|
||||||
Carinapress,Carina Nebula,Cypress
|
|
||||||
Veilbark,Veil Nebula,Bark
|
|
||||||
Northamericoak,North America Nebula,Oak
|
|
||||||
Flamingowan,Flaming Star Nebula,Rowan
|
|
||||||
Owlnut,Owl Nebula,Walnut
|
|
||||||
Catseyelm,Cat's Eye Nebula,Elm
|
|
||||||
Ringebula,Ring Nebula,Nebula
|
|
||||||
Dumbbellnut,Dumbbell Nebula,Butternut
|
|
||||||
Boomerangalm,Boomerang Nebula,Palm
|
|
||||||
Butterflypress,Butterfly Nebula,Cypress
|
|
||||||
Calasash,Calabash Nebula,Ash
|
|
||||||
Redspiderash,Red Spider Nebula,Ash
|
|
||||||
Eskimogany,Eskimo Nebula,Mahogany
|
|
||||||
Ghostjuniper,Ghost of Jupiter,Juniper
|
|
||||||
Helixogwood,Helix Nebula,Dogwood
|
|
||||||
Spiralpress,Spiral Planetary,Cypress
|
|
||||||
Littlegembark,Little Gem Nebula,Bark
|
|
||||||
Necklacacia,Necklace Nebula,Acacia
|
|
||||||
Saturnbeech,Saturn Nebula,Beech
|
|
||||||
Bluesnowbark,Blue Snowball,Bark
|
|
||||||
Twinjet Fir,Twin Jet Nebula,Fir
|
|
||||||
Hourglasswood,Hourglass Nebula,Boxwood
|
|
||||||
Eggwood,Egg Nebula,Dogwood
|
|
||||||
Boxtree,Box Nebula,Boxtree
|
|
||||||
Crescentpress,Crescent Nebula,Cypress
|
|
||||||
Bubblebark,Bubble Nebula,Bark
|
|
||||||
Thorwood,Thor's Helmet,Thornwood
|
|
||||||
Snailgum,Snail Nebula,Sweetgum
|
|
||||||
Conepinepress,Cone Nebula,Pine
|
|
||||||
Irisbeech,Iris Nebula,Beech
|
|
||||||
Witchheadash,Witch Head Nebula,Ash
|
|
||||||
Barnardloopwood,Barnard's Loop,Loopwood
|
|
||||||
Californiapress,California Nebula,Cypress
|
|
||||||
Heartbark,Heart Nebula,Bark
|
|
||||||
Soulspuce,Soul Nebula,Spruce
|
|
||||||
Pacmangany,Pacman Nebula,Mahogany
|
|
||||||
Monkeyheadnut,Monkey Head Nebula,Monkeynut
|
|
||||||
Wizardlm,Wizard Nebula,Elm
|
|
||||||
Tulipwood,Tulip Nebula,Tulipwood
|
|
||||||
Crescentbark,Crescent Nebula,Bark
|
|
||||||
Pelicangum,Pelican Nebula,Gum
|
|
||||||
Elephantstrunkwood,Elephant's Trunk,Trunkwood
|
|
||||||
Tuningforkpress,Tuning Fork Galaxy,Cypress
|
|
||||||
Sombrerowood,Sombrero Galaxy,Sombrerowood
|
|
||||||
Blackeyesycamore,Black Eye Galaxy,Sycamore
|
|
||||||
Sunflowernut,Sunflower Galaxy,Coconut
|
|
||||||
Pinwheelder,Pinwheel Galaxy,Elder
|
|
||||||
Fireworksgalaxyalder,Fireworks Galaxy,Alder
|
|
||||||
Spindlepress,Spindle Galaxy,Cypress
|
|
||||||
Bodesgalaxnut,Bode's Galaxy,Butternut
|
|
||||||
Cigargalaxbirch,Cigar Galaxy,Birch
|
|
||||||
Whirlpoolepress,Whirlpool Galaxy,Cypress
|
|
||||||
Cartwheelalder,Cartwheel Galaxy,Alder
|
|
||||||
Tadpolewood,Tadpole Galaxy,Tadwood
|
|
||||||
Antenaennut,Antennae Galaxies,Nut
|
|
||||||
Mousesgalaxielm,Mice Galaxies,Elm
|
|
||||||
Atomspeachachy,Atom's Peace Galaxy,Peachy
|
|
||||||
Malinsgalaxnut,Malin 1,Nut
|
|
||||||
Hoagsobjecteakwood,Hoag's Object,Teakwood
|
|
||||||
Ringgalaxynut,Ring Galaxy,Donut
|
|
||||||
Polarringiron,Polar Ring Galaxy,Ironwood
|
|
||||||
Silversliverwood,Silver Sliver Galaxy,Silverwood
|
|
||||||
Needlepress,Needle Galaxy,Cypress
|
|
||||||
Hamburgerash,Hamburger Galaxy,Ash
|
|
||||||
|
|
|
@ -1,420 +0,0 @@
|
||||||
Name,Component_1,Component_2
|
|
||||||
Pleiadeiba,Pleiades,Ceiba
|
|
||||||
Pau Brasilica,Pau Brasil,Crab Nebula
|
|
||||||
Araucarina,Araucaria,Carina Nebula
|
|
||||||
Quebracho X-1,Quebracho,Cygnus X-1
|
|
||||||
Lapachomeda,Lapacho,Andromeda
|
|
||||||
Guanacastellum,Guanacaste,Magellanic Cloud
|
|
||||||
Trianguanum,Triangulum,Guanacaste
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibalaxy,Ceiba,Galaxy
|
|
||||||
Horsehead Brasil,Horsehead,Pau Brasil
|
|
||||||
Monkeypuzzle Void,Monkey Puzzle,Cosmic Void
|
|
||||||
Quebrachion,Quebracho,Orion
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubber Rosette,Rubber Tree,Rosette Nebula
|
|
||||||
Cashewmeda,Cashew,Andromeda
|
|
||||||
Brazil Nutnova,Brazil Nut,Supernova
|
|
||||||
Cecropiades,Cecropia,Pleiades
|
|
||||||
Balsombrero,Balsa,Sombrero Galaxy
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipe Helix,Ipe,Helix Nebula
|
|
||||||
Kapoksagittarius,Kapok,Sagittarius
|
|
||||||
Cedrella Cluster,Cedrella,Star Cluster
|
|
||||||
Alerce Lyra,Alerce,Lyra
|
|
||||||
Mahogalactica,Mahogany,Galactic Center
|
|
||||||
Pinwheelnut,Pinwheel,Brazil Nut
|
|
||||||
Tarantularch,Tarantula Nebula,Larch
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Pau Brassiopeia,Pau Brasil,Cassiopeia
|
|
||||||
Araucarion,Araucaria,Orion
|
|
||||||
Quebrachole,Quebracho,Black Hole
|
|
||||||
Lapachoway,Lapacho,Milky Way
|
|
||||||
Guanasar,Guanacaste,Quasar
|
|
||||||
Jacaranebula,Jacaranda,Nebula
|
|
||||||
Ceiba Centauri,Ceiba,Alpha Centauri
|
|
||||||
Brazilbeehive,Brazil Nut,Beehive Cluster
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachydes,Lapacho,Hyades
|
|
||||||
Rubberway,Rubber Tree,Milky Way
|
|
||||||
Cashewsar,Cashew,Quasar
|
|
||||||
Nutmeda,Brazil Nut,Andromeda
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsanova,Balsa,Supernova
|
|
||||||
Rosettewood,Rosette,Rosewood
|
|
||||||
Ipegalaxy,Ipe,Galaxy
|
|
||||||
Kapoknebula,Kapok,Nebula
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercentauri,Alerce,Centaurus
|
|
||||||
Mahogacrab,Mahogany,Crab Nebula
|
|
||||||
Eaglebrasil,Eagle Nebula,Pau Brasil
|
|
||||||
Tarantuleia,Tarantula Nebula,Ceiba
|
|
||||||
Whirlpoolnut,Whirlpool,Brazil Nut
|
|
||||||
Araucagalaxy,Araucaria,Galaxy
|
|
||||||
Quebrachiades,Quebracho,Pleiades
|
|
||||||
Lapachomeda,Lapacho,Andromeda
|
|
||||||
Guanacasteroid,Guanacaste,Asteroid
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Tadpolebrasil,Tadpole,Pau Brasil
|
|
||||||
Monkeynova,Monkey Puzzle,Supernova
|
|
||||||
Quebrachygnus,Quebracho,Cygnus
|
|
||||||
Lapachoacentauri,Lapacho,Alpha Centauri
|
|
||||||
Rubberhole,Rubber Tree,Black Hole
|
|
||||||
Cashewnova,Cashew,Supernova
|
|
||||||
Brazilbutterfly,Brazil Nut,Butterfly Cluster
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsagittarius,Balsa,Sagittarius
|
|
||||||
Rosewoodrion,Rosewood,Orion
|
|
||||||
Ipeleiades,Ipe,Pleiades
|
|
||||||
Kapoksombrero,Kapok,Sombrero Galaxy
|
|
||||||
Cedrellipse,Cedrella,Elliptical Galaxy
|
|
||||||
Alercegalaxy,Alerce,Galaxy
|
|
||||||
Mahogaring,Mahogany,Ring Nebula
|
|
||||||
Cartwheeiba,Cartwheel,Ceiba
|
|
||||||
Pau Brasilian,Pau Brasil,Cassiopeian
|
|
||||||
Araucarionebula,Araucaria,Orion Nebula
|
|
||||||
Quebrasar,Quebracho,Quasar
|
|
||||||
Lapachomega,Lapacho,Omega Centauri
|
|
||||||
Guanacastway,Guanacaste,Milky Way
|
|
||||||
Jacaranovae,Jacaranda,Supernova
|
|
||||||
Ceibachomeda,Ceiba,Andromeda
|
|
||||||
Sunflowerbrasil,Sunflower Galaxy,Pau Brasil
|
|
||||||
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
|
|
||||||
Quebrachoxima,Quebracho,Proxima
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubberbula,Rubber Tree,Nebula
|
|
||||||
Cashewcluster,Cashew,Star Cluster
|
|
||||||
Nutway,Brazil Nut,Milky Way
|
|
||||||
Cecropianebula,Cecropia,Nebula
|
|
||||||
Balsagara,Balsa,Sagittarius A*
|
|
||||||
Rosewoodraco,Rosewood,Draco
|
|
||||||
Ipelipse,Ipe,Ellipse
|
|
||||||
Kapokagoon,Kapok,Lagoon Nebula
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercerion,Alerce,Orion
|
|
||||||
Mahogacarina,Mahogany,Carina
|
|
||||||
Trifidnut,Trifid Nebula,Brazil Nut
|
|
||||||
Tarantulacho,Tarantula,Lapacho
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Pau Brasilion,Pau Brasil,Orion
|
|
||||||
Araucariades,Araucaria,Pleiades
|
|
||||||
Quebrachoelix,Quebracho,Helix
|
|
||||||
Lapacholia,Lapacho,Magnolia
|
|
||||||
Guanacasar,Guanacaste,Quasar
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Titan Brasil,Titan,Pau Brasil
|
|
||||||
Monkeynebula,Monkey Puzzle,Nebula
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Rubbersar,Rubber Tree,Quasar
|
|
||||||
Cashewmeda,Cashew,Andromeda
|
|
||||||
Nutcluster,Brazil Nut,Cluster
|
|
||||||
Cecropioway,Cecropia,Milky Way
|
|
||||||
Balsanebula,Balsa,Nebula
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipegalaxy,Ipe,Galaxy
|
|
||||||
Kapoksar,Kapok,Quasar
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercenova,Alerce,Supernova
|
|
||||||
Mahogalactic,Mahogany,Galactic
|
|
||||||
Sculptorbrasil,Sculptor Galaxy,Pau Brasil
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Araucarionway,Araucaria,Milky Way
|
|
||||||
Quebrachion,Quebracho,Orion
|
|
||||||
Lapachohole,Lapacho,Black Hole
|
|
||||||
Guanacastromeda,Guanacaste,Andromeda
|
|
||||||
Jacaranebula,Jacaranda,Nebula
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
Europa Brasil,Europa,Pau Brasil
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachnova,Quebracho,Supernova
|
|
||||||
Lapachoides,Lapacho,Hyades
|
|
||||||
Rubbermeda,Rubber Tree,Andromeda
|
|
||||||
Cashewnova,Cashew,Supernova
|
|
||||||
Nutway,Brazil Nut,Milky Way
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsaquila,Balsa,Aquila
|
|
||||||
Rosewoodsar,Rosewood,Quasar
|
|
||||||
Ipecentauri,Ipe,Alpha Centauri
|
|
||||||
Kapokgalaxy,Kapok,Galaxy
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercemeda,Alerce,Andromeda
|
|
||||||
Mahoganova,Mahogany,Supernova
|
|
||||||
Boomerangbrasil,Boomerang Nebula,Pau Brasil
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Pau Brasilades,Pau Brasil,Pleiades
|
|
||||||
Araucagalaxy,Araucaria,Galaxy
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Guanacastellum,Guanacaste,Magellanic
|
|
||||||
Jacaranovae,Jacaranda,Supernova
|
|
||||||
Ceibaway,Ceiba,Milky Way
|
|
||||||
Enceladnut,Enceladus,Brazil Nut
|
|
||||||
Monkeynebula,Monkey Puzzle,Nebula
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachonova,Lapacho,Supernova
|
|
||||||
Rubberion,Rubber Tree,Orion
|
|
||||||
Cashewgalaxy,Cashew,Galaxy
|
|
||||||
Nutsagittarius,Brazil Nut,Sagittarius
|
|
||||||
Cecropia A*,Cecropia,Sagittarius A*
|
|
||||||
Balsanova,Balsa,Supernova
|
|
||||||
Rosewoodmeda,Rosewood,Andromeda
|
|
||||||
Ipesar,Ipe,Quasar
|
|
||||||
Kapokway,Kapok,Milky Way
|
|
||||||
Cedrellipse,Cedrella,Ellipse
|
|
||||||
Alercenebula,Alerce,Nebula
|
|
||||||
Mahogaring,Mahogany,Ring Nebula
|
|
||||||
Cat's Eye Brasil,Cat's Eye,Pau Brasil
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Araucarionebula,Araucaria,Orion Nebula
|
|
||||||
Quebrachoides,Quebracho,Hyades
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibaneta,Ceiba,Planetary Nebula
|
|
||||||
Ganymede Brasil,Ganymede,Pau Brasil
|
|
||||||
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
|
|
||||||
Quebrachonova,Quebracho,Supernova
|
|
||||||
Lapachomeda,Lapacho,Andromeda
|
|
||||||
Rubbersar,Rubber Tree,Quasar
|
|
||||||
Cashewnebula,Cashew,Nebula
|
|
||||||
Nutgalaxy,Brazil Nut,Galaxy
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsagittarius,Balsa,Sagittarius
|
|
||||||
Rosewoodion,Rosewood,Orion
|
|
||||||
Ipeleiades,Ipe,Pleiades
|
|
||||||
Kapoknova,Kapok,Supernova
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercegalaxy,Alerce,Galaxy
|
|
||||||
Mahogacrab,Mahogany,Crab
|
|
||||||
Dumbbell Brasil,Dumbbell Nebula,Pau Brasil
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Pau Brasilway,Pau Brasil,Milky Way
|
|
||||||
Araucarina,Araucaria,Carina
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Guanacastromeda,Guanacaste,Andromeda
|
|
||||||
Jacaranebula,Jacaranda,Nebula
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Io Brasil,Io,Pau Brasil
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Rubbernova,Rubber Tree,Supernova
|
|
||||||
Cashewmeda,Cashew,Andromeda
|
|
||||||
Nutnebula,Brazil Nut,Nebula
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsasar,Balsa,Quasar
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipegalaxy,Ipe,Galaxy
|
|
||||||
Kapokmeda,Kapok,Andromeda
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercenova,Alerce,Supernova
|
|
||||||
Mahogalagoon,Mahogany,Lagoon
|
|
||||||
Crescent Brasil,Crescent Nebula,Pau Brasil
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
Araucarionway,Araucaria,Milky Way
|
|
||||||
Quebrachion,Quebracho,Orion
|
|
||||||
Lapachohole,Lapacho,Black Hole
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacaranovo,Jacaranda,Supernova
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Callisto Brasil,Callisto,Pau Brasil
|
|
||||||
Monkeynebula,Monkey Puzzle,Nebula
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachonova,Lapacho,Supernova
|
|
||||||
Rubbermeda,Rubber Tree,Andromeda
|
|
||||||
Cashewgalaxy,Cashew,Galaxy
|
|
||||||
Nutway,Brazil Nut,Milky Way
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsanebula,Balsa,Nebula
|
|
||||||
Rosewoodsar,Rosewood,Quasar
|
|
||||||
Ipecentauri,Ipe,Centauri
|
|
||||||
Kapokgalaxy,Kapok,Galaxy
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercemeda,Alerce,Andromeda
|
|
||||||
Mahoganova,Mahogany,Supernova
|
|
||||||
Flame Brasil,Flame Nebula,Pau Brasil
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Pau Brasilion,Pau Brasil,Orion
|
|
||||||
Araucagalaxy,Araucaria,Galaxy
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Guanacastway,Guanacaste,Milky Way
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Veil Brasil,Veil Nebula,Pau Brasil
|
|
||||||
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
|
|
||||||
Quebrachonova,Quebracho,Supernova
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Rubberion,Rubber Tree,Orion
|
|
||||||
Cashewnova,Cashew,Supernova
|
|
||||||
Nutgalaxy,Brazil Nut,Galaxy
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsomeda,Balsa,Andromeda
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipesar,Ipe,Quasar
|
|
||||||
Kapoknebula,Kapok,Nebula
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercenebula,Alerce,Nebula
|
|
||||||
Mahogaring,Mahogany,Ring
|
|
||||||
Pelican Brasil,Pelican Nebula,Pau Brasil
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
Araucarionebula,Araucaria,Orion Nebula
|
|
||||||
Quebrachoides,Quebracho,Hyades
|
|
||||||
Lapachomeda,Lapacho,Andromeda
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacaranovo,Jacaranda,Supernova
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Cone Brasil,Cone Nebula,Pau Brasil
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubbernova,Rubber Tree,Supernova
|
|
||||||
Cashewmeda,Cashew,Andromeda
|
|
||||||
Nutway,Brazil Nut,Milky Way
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsagalaxy,Balsa,Galaxy
|
|
||||||
Rosewoodion,Rosewood,Orion
|
|
||||||
Ipeleiades,Ipe,Pleiades
|
|
||||||
Kapokgalaxy,Kapok,Galaxy
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercegalaxy,Alerce,Galaxy
|
|
||||||
Mahoganova,Mahogany,Supernova
|
|
||||||
Witch Head Brasil,Witch Head,Pau Brasil
|
|
||||||
Ceiboway,Ceiba,Milky Way
|
|
||||||
Pau Brasilmeda,Pau Brasil,Andromeda
|
|
||||||
Araucarina,Araucaria,Carina
|
|
||||||
Quebrachonebula,Quebracho,Nebula
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Guanacastromeda,Guanacaste,Andromeda
|
|
||||||
Jacaranebula,Jacaranda,Nebula
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Iris Brasil,Iris Nebula,Pau Brasil
|
|
||||||
Monkeynova,Monkey Puzzle,Supernova
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubbermeda,Rubber Tree,Andromeda
|
|
||||||
Cashewgalaxy,Cashew,Galaxy
|
|
||||||
Nutnebula,Brazil Nut,Nebula
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsanovo,Balsa,Supernova
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipegalaxy,Ipe,Galaxy
|
|
||||||
Kapokmeda,Kapok,Andromeda
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercenova,Alerce,Supernova
|
|
||||||
Mahogagalaxy,Mahogany,Galaxy
|
|
||||||
Bubble Brasil,Bubble Nebula,Pau Brasil
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Araucarionway,Araucaria,Milky Way
|
|
||||||
Quebrachion,Quebracho,Orion
|
|
||||||
Lapachohole,Lapacho,Black Hole
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacaranovo,Jacaranda,Supernova
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
North America Brasil,North America,Pau Brasil
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachonova,Lapacho,Supernova
|
|
||||||
Rubbersar,Rubber Tree,Quasar
|
|
||||||
Cashewnebula,Cashew,Nebula
|
|
||||||
Nutgalaxy,Brazil Nut,Galaxy
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsoway,Balsa,Milky Way
|
|
||||||
Rosewoodsar,Rosewood,Quasar
|
|
||||||
Ipecentauri,Ipe,Centauri
|
|
||||||
Kapokgalaxy,Kapok,Galaxy
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercemeda,Alerce,Andromeda
|
|
||||||
Mahoganova,Mahogany,Supernova
|
|
||||||
Pacman Brasil,Pacman Nebula,Pau Brasil
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Pau Brasilades,Pau Brasil,Pleiades
|
|
||||||
Araucagalaxy,Araucaria,Galaxy
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Guanacastway,Guanacaste,Milky Way
|
|
||||||
Jacarandromeda,Jacaranda,Andromeda
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Soul Brasil,Soul Nebula,Pau Brasil
|
|
||||||
Monkeynebula,Monkey Puzzle,Nebula
|
|
||||||
Quebrachonova,Quebracho,Supernova
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Rubberion,Rubber Tree,Orion
|
|
||||||
Cashewnova,Cashew,Supernova
|
|
||||||
Nutway,Brazil Nut,Milky Way
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsameda,Balsa,Andromeda
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipesar,Ipe,Quasar
|
|
||||||
Kapoknebula,Kapok,Nebula
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercenebula,Alerce,Nebula
|
|
||||||
Mahogaring,Mahogany,Ring
|
|
||||||
Heart Brasil,Heart Nebula,Pau Brasil
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
Araucarionebula,Araucaria,Orion Nebula
|
|
||||||
Quebrachoides,Quebracho,Hyades
|
|
||||||
Lapachomeda,Lapacho,Andromeda
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacaranovo,Jacaranda,Supernova
|
|
||||||
Ceiboway,Ceiba,Milky Way
|
|
||||||
Elephant Trunk Brasil,Elephant Trunk,Pau Brasil
|
|
||||||
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubbernova,Rubber Tree,Supernova
|
|
||||||
Cashewmeda,Cashew,Andromeda
|
|
||||||
Nutgalaxy,Brazil Nut,Galaxy
|
|
||||||
Cecropiomeda,Cecropia,Andromeda
|
|
||||||
Balsagalaxy,Balsa,Galaxy
|
|
||||||
Rosewoodion,Rosewood,Orion
|
|
||||||
Ipeleiades,Ipe,Pleiades
|
|
||||||
Kapokgalaxy,Kapok,Galaxy
|
|
||||||
Cedrellaxy,Cedrella,Galaxy
|
|
||||||
Alercegalaxy,Alerce,Galaxy
|
|
||||||
Mahoganova,Mahogany,Supernova
|
|
||||||
Omega Brasil,Omega Nebula,Pau Brasil
|
|
||||||
Ceibulsar,Ceiba,Pulsar
|
|
||||||
Pau Brasilion,Pau Brasil,Orion
|
|
||||||
Araucarina,Araucaria,Carina
|
|
||||||
Quebrachonebula,Quebracho,Nebula
|
|
||||||
Lapachogalaxy,Lapacho,Galaxy
|
|
||||||
Guanacastromeda,Guanacaste,Andromeda
|
|
||||||
Jacaranebula,Jacaranda,Nebula
|
|
||||||
Ceibanova,Ceiba,Supernova
|
|
||||||
Blinking Brasil,Blinking Nebula,Pau Brasil
|
|
||||||
Monkeypuzzar,Monkey Puzzle,Quasar
|
|
||||||
Quebrachomeda,Quebracho,Andromeda
|
|
||||||
Lapachosar,Lapacho,Quasar
|
|
||||||
Rubbermeda,Rubber Tree,Andromeda
|
|
||||||
Cashewgalaxy,Cashew,Galaxy
|
|
||||||
Nutnebula,Brazil Nut,Nebula
|
|
||||||
Cecropiagalaxy,Cecropia,Galaxy
|
|
||||||
Balsanovo,Balsa,Supernova
|
|
||||||
Rosewoodway,Rosewood,Milky Way
|
|
||||||
Ipegalaxy,Ipe,Galaxy
|
|
||||||
Kapokmeda,Kapok,Andromeda
|
|
||||||
Cedrellaneta,Cedrella,Planetary Nebula
|
|
||||||
Alercenova,Alerce,Supernova
|
|
||||||
Mahogagalaxy,Mahogany,Galaxy
|
|
||||||
Ghost Brasil,Ghost Nebula,Pau Brasil
|
|
||||||
Ceibomeda,Ceiba,Andromeda
|
|
||||||
Araucarionway,Araucaria,Milky Way
|
|
||||||
Quebrachion,Quebracho,Orion
|
|
||||||
Lapachohole,Lapacho,Black Hole
|
|
||||||
Guanacastar,Guanacaste,Quasar
|
|
||||||
Jacaranovo,Jacaranda,Supernova
|
|
||||||
Ceibagalaxy,Ceiba,Galaxy
|
|
||||||
Eskimo Brasil,Eskimo Nebula,Pau Brasil
|
|
||||||
Monkeynebula,Monkey Puzzle,Nebula
|
|
||||||
Quebrachogalaxy,Quebracho,Galaxy
|
|
||||||
Lapachonova,Lapacho,Supernova
|
|
||||||
Rubbersar,Rubber Tree,Quasar
|
|
||||||
Cashewnebula,Cashew,Nebula
|
|
||||||
Nutgalaxy,Brazil Nut,Galaxy
|
|
||||||
Cecropioway,Cecropia,Milky Way
|
|
||||||
|
78
package-lock.json
generated
78
package-lock.json
generated
|
|
@ -1,78 +0,0 @@
|
||||||
{
|
|
||||||
"name": "memlnaut-nisps-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "memlnaut-nisps-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.59.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test": {
|
|
||||||
"version": "1.59.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
|
||||||
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright": "1.59.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright": {
|
|
||||||
"version": "1.59.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
|
||||||
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.59.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright-core": {
|
|
||||||
"version": "1.59.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
|
||||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
package.json
13
package.json
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "memlnaut-nisps-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"test": "playwright test",
|
|
||||||
"test:ui": "playwright test --ui",
|
|
||||||
"test:headed": "playwright test --headed"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.59.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
._root_1attb_1{max-width:1200px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-5)}._header_1attb_9{display:flex;flex-direction:column;gap:var(--sp-2)}._header_1attb_9 h1{margin:0;font-size:var(--fs-xl);color:var(--accent)}._lede_1attb_21{color:var(--fg-mute);margin:0;max-width:720px}._grid_1attb_27{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:var(--sp-4)}._card_1attb_33{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-3);padding:var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-3);min-height:280px}._cardHead_1attb_44{display:flex;flex-direction:column;gap:var(--sp-1);border-bottom:1px solid var(--line);padding-bottom:var(--sp-2)}._cardTitle_1attb_52{margin:0;font-size:var(--fs-md);color:var(--fg)}._cardDesc_1attb_58{margin:0;font-size:var(--fs-xs);color:var(--fg-mute)}._cardBody_1attb_64{display:flex;flex-direction:column;align-items:stretch;flex:1;justify-content:flex-start}._bank_3njyn_1{display:flex;flex-direction:column;gap:var(--sp-3);background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-2);padding:var(--sp-3)}._title_3njyn_11{font-size:var(--fs-md);margin:0 0 var(--sp-2) 0;color:var(--fg)}._section_3njyn_17{display:flex;flex-direction:column;gap:var(--sp-2)}._sectionHeader_3njyn_23{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-1) var(--sp-2);background:var(--bg-2);border:1px solid var(--line);text-align:left;width:100%;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.08em;color:var(--fg-mute)}._caret_3njyn_38{width:1ch;display:inline-block}._sectionName_3njyn_43{flex:1}._sectionCount_3njyn_47{color:var(--fg-dim)}._list_3njyn_51{display:flex;flex-direction:column;gap:var(--sp-2);padding:var(--sp-1) 0}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
playground/dist/assets/index-C1-a2ZPT.js
vendored
3
playground/dist/assets/index-C1-a2ZPT.js
vendored
File diff suppressed because one or more lines are too long
1
playground/dist/assets/index-C1-a2ZPT.js.map
vendored
1
playground/dist/assets/index-C1-a2ZPT.js.map
vendored
File diff suppressed because one or more lines are too long
1
playground/dist/assets/index-D19lnkK_.css
vendored
1
playground/dist/assets/index-D19lnkK_.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +0,0 @@
|
||||||
(function(){"use strict";class f extends AudioWorkletProcessor{instance=null;engineHandle=0;engineId="thru";muted=!0;inLPtr=0;inRPtr=0;outLPtr=0;outRPtr=0;idPtr=0;paramsPtr=0;inLView=null;inRView=null;outLView=null;outRView=null;paramsView=null;idView=null;mem=null;pendingParams=null;constructor(){super(),this.port.onmessage=i=>this.onMessage_(i.data)}async onMessage_(i){if(i.kind==="init")try{await this.init_(i.wasmBinary,i.sampleRate),this.post_({kind:"ready"})}catch(r){this.post_({kind:"error",message:r instanceof Error?r.message:String(r)})}else i.kind==="engine"?this.switchEngine_(i.engineId):i.kind==="params"?this.applyParams_(i.params):i.kind==="mute"&&(this.muted=i.muted)}post_(i){this.port.postMessage(i)}async init_(i,r){const t=new WebAssembly.Memory({initial:128,maximum:4096,shared:!1}),a=await WebAssembly.compile(i),h=WebAssembly.Module.imports(a),n={};for(const e of h){n[e.module]||(n[e.module]={});const s=n[e.module];e.kind==="function"?(e.name,s[e.name]=(...o)=>0):e.kind==="memory"?s[e.name]=t:e.kind==="table"?s[e.name]=new WebAssembly.Table({element:"anyfunc",initial:0}):e.kind==="global"&&(s[e.name]=new WebAssembly.Global({value:"i32",mutable:!0},0))}for(const e of h){const s=n[e.module];e.name==="a"&&e.kind==="function"&&(s[e.name]=()=>{throw new Error("wasm aborted")}),e.name==="b"&&e.kind==="function"&&(s[e.name]=o=>0)}const w=await WebAssembly.instantiate(a,n),_=WebAssembly.Module.exports(a),P=new Map;for(const e of _)P.set(e.name,e.name);const p=w.exports;function u(...e){for(const s of e){const o=p[s];if(typeof o=="function")return o}throw new Error(`worklet: missing wasm export, tried: ${e.join(", ")}`)}function m(...e){return u(...e)}const l=u("_malloc","malloc"),g=m("_free","free"),y=u("_nisps_engine_create"),R=m("_nisps_engine_destroy"),b=m("_nisps_engine_set_params"),L=m("_nisps_engine_process_block");let d=null;for(const e of _)if(e.kind==="memory"){const s=p[e.name];if(s instanceof WebAssembly.Memory){d=s;break}}this.mem=d??t,this.instance={exports:{memory:this.mem,malloc:l,free:g,_nisps_engine_create:(e,s)=>y(e,s),_nisps_engine_destroy:e=>R(e),_nisps_engine_set_params:(e,s,o)=>b(e,s,o),_nisps_engine_process_block:(e,s,o,V,A,C)=>L(e,s,o,V,A,C)}},this.inLPtr=l(128*4),this.inRPtr=l(128*4),this.outLPtr=l(128*4),this.outRPtr=l(128*4),this.paramsPtr=l(256*4),this.idPtr=l(32);const c=this.mem.buffer;this.inLView=new Float32Array(c,this.inLPtr,128),this.inRView=new Float32Array(c,this.inRPtr,128),this.outLView=new Float32Array(c,this.outLPtr,128),this.outRView=new Float32Array(c,this.outRPtr,128),this.paramsView=new Float32Array(c,this.paramsPtr,256),this.idView=new Uint8Array(c,this.idPtr,32),this.spawnEngine_("thru",r),this.pendingParams&&(this.applyParams_(this.pendingParams),this.pendingParams=null),this.muted=!1}spawnEngine_(i,r){if(!this.instance||!this.idView)return;this.engineHandle&&(this.instance.exports._nisps_engine_destroy(this.engineHandle),this.engineHandle=0);const a=new TextEncoder().encode(i);this.idView.fill(0),this.idView.set(a.subarray(0,Math.min(a.length,31))),this.engineHandle=this.instance.exports._nisps_engine_create(this.idPtr,r),this.engineId=i}switchEngine_(i){this.spawnEngine_(i,sampleRate)}applyParams_(i){if(!this.instance||!this.paramsView){this.pendingParams=i;return}const r=Math.min(i.length,256);for(let t=0;t<r;++t)this.paramsView[t]=i[t];this.engineHandle&&this.instance.exports._nisps_engine_set_params(this.engineHandle,this.paramsPtr,r)}process(i,r){const t=r[0];if(!t||t.length===0)return!0;const a=t[0],h=t.length>1?t[1]:t[0];if(this.muted||!this.instance||!this.engineHandle||!this.inLView||!this.outLView||!this.outRView||!this.inRView)return a.fill(0),t.length>1&&h.fill(0),!0;const n=i[0];return n&&n[0]?this.inLView.set(n[0].subarray(0,128)):this.inLView.fill(0),n&&n[1]?this.inRView.set(n[1].subarray(0,128)):n&&n[0]?this.inRView.set(n[0].subarray(0,128)):this.inRView.fill(0),this.instance.exports._nisps_engine_process_block(this.engineHandle,this.inLPtr,this.inRPtr,this.outLPtr,this.outRPtr,128),a.set(this.outLView.subarray(0,a.length)),t.length>1&&h.set(this.outRView.subarray(0,h.length)),!0}}registerProcessor("nisps-processor",f)})();
|
|
||||||
//# sourceMappingURL=nisps-processor-BuKuNOY1.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
2
playground/dist/assets/wasm-iml-FZSgVMmn.js
vendored
2
playground/dist/assets/wasm-iml-FZSgVMmn.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +0,0 @@
|
||||||
(function(){"use strict";if(typeof window>"u"&&typeof self<"u"&&typeof self.importScripts<"u"){let h=function(t,s,n,u){if(!e)throw new Error("worker module not loaded");c!==a&&(i&&e._free(i),i=e._malloc(a*4),c=a),t.length!==g&&(l&&e._free(l),l=e._malloc(t.length*4),g=t.length),s.length!==_&&(f&&e._free(f),f=e._malloc(s.length*4),_=s.length),n.length!==w&&(o&&e._free(o),o=n.length>0?e._malloc(n.length*4):0,w=n.length),new Float32Array(e.HEAPF32.buffer,i,a).set(u),new Float32Array(e.HEAPF32.buffer,l,t.length).set(t),new Float32Array(e.HEAPF32.buffer,f,s.length).set(s),o&&new Float32Array(e.HEAPF32.buffer,o,n.length).set(n)},p=function(t){if(!e)return{kind:"error",requestId:t.requestId,message:"worker not initialised"};try{h(t.features,t.labels,t.sampleWeights,t.weights),e._nisps_ml_set_weights(r,i),e._nisps_ml_clear_examples(r);const s=t.inputSize,n=t.outputSize,u=t.features.length/s;for(let d=0;d<u;++d){const S=l+d*s*4,H=f+d*n*4;e._nisps_ml_add_example(r,S,H)}const F=t.sampleWeights.length>0?o:0,m=e._nisps_ml_train(r,t.lr,t.maxIter,t.minErr,F);e._nisps_ml_get_weights(r,i);const A=new Float32Array(e.HEAPF32.buffer,i,a),P=new Float32Array(A),E=new Float32Array([m]);return{kind:"result",requestId:t.requestId,loss:m,weights:P,lossHistory:E}}catch(s){return{kind:"error",requestId:t.requestId,message:s instanceof Error?s.message:String(s)}}},y=function(){e&&(r&&(e._nisps_ml_destroy(r),r=0),i&&(e._free(i),i=0),l&&(e._free(l),l=0),f&&(e._free(f),f=0),o&&(e._free(o),o=0),e=null)},e=null,r=0,a=0,i=0,c=0,l=0,g=0,f=0,_=0,o=0,w=0;async function k(t){const s=await import(new URL("/nisps.js",self.location.origin).toString());e=await(s.default??s.createNispsModule)({locateFile:u=>u.endsWith(".wasm")?new URL("/nisps.wasm",self.location.origin).toString():u}),r=e._nisps_ml_create(0,0,0,0,t>>>0),a=e._nisps_ml_weight_count(r)}self.addEventListener("message",async t=>{const s=t.data;if(s.kind==="init")try{await k(s.seed),self.postMessage({kind:"ready"})}catch(n){self.postMessage({kind:"error",requestId:0,message:n instanceof Error?n.message:String(n)})}else if(s.kind==="train"){const n=p(s);n.kind==="result"?self.postMessage(n,[n.weights.buffer,n.lossHistory.buffer]):self.postMessage(n)}else s.kind==="dispose"&&y()})}})();
|
|
||||||
//# sourceMappingURL=wasm-worker-OATBBHaT.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
14
playground/dist/index.html
vendored
14
playground/dist/index.html
vendored
|
|
@ -1,14 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
||||||
<meta name="theme-color" content="#0d0d0d" />
|
|
||||||
<title>MEMLNaut Playground</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-C1-a2ZPT.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D19lnkK_.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
2
playground/dist/nisps.js
vendored
2
playground/dist/nisps.js
vendored
File diff suppressed because one or more lines are too long
BIN
playground/dist/nisps.wasm
vendored
BIN
playground/dist/nisps.wasm
vendored
Binary file not shown.
|
|
@ -1,27 +0,0 @@
|
||||||
const { defineConfig } = require('@playwright/test');
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
testDir: './tests/e2e',
|
|
||||||
timeout: 30_000,
|
|
||||||
expect: { timeout: 10_000 },
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:7331',
|
|
||||||
headless: true,
|
|
||||||
// WASM + AudioContext require a stable origin
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
},
|
|
||||||
// Start a static file server against the playground/ dir before tests run.
|
|
||||||
// python3 -m http.server serves directory listings and static assets fine.
|
|
||||||
// WASM files need correct MIME type — Python's server handles .wasm correctly.
|
|
||||||
webServer: {
|
|
||||||
command: 'python3 -m http.server 7331',
|
|
||||||
cwd: './playground',
|
|
||||||
url: 'http://localhost:7331',
|
|
||||||
reuseExistingServer: true,
|
|
||||||
timeout: 10_000,
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
||||||
],
|
|
||||||
reporter: [['list'], ['html', { open: 'never' }]],
|
|
||||||
});
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
/**
|
|
||||||
* Engine switching e2e tests — verify that all three synth engines
|
|
||||||
* (C15 Shaper-Feedback, Additive, FM Matrix) can be selected, initialised,
|
|
||||||
* and driven by the NISPS ML engine correctly.
|
|
||||||
*
|
|
||||||
* Also tests the EOC chain integration across engine switches.
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp, statusText } = require('./helpers');
|
|
||||||
|
|
||||||
// Engine metadata expected from the switcher
|
|
||||||
const ENGINES = {
|
|
||||||
'shaper-feedback': { displayName: 'C15 Shaper-Feedback', paramCount: 126 },
|
|
||||||
'additive': { displayName: 'Additive', paramCount: 48 },
|
|
||||||
'fm': { displayName: 'FM Matrix', paramCount: 55 },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: switch to an engine via the engine switcher UI.
|
|
||||||
* Opens the synth drawer, clicks the engine card, accepts any confirm dialog.
|
|
||||||
*/
|
|
||||||
async function switchEngine(page, engineId) {
|
|
||||||
// Open synth drawer if not already open
|
|
||||||
const drawer = page.locator('#drawer-synth');
|
|
||||||
if (await drawer.evaluate(el => el.classList.contains('hidden'))) {
|
|
||||||
await page.click('[data-drawer="synth"]');
|
|
||||||
}
|
|
||||||
// Click the engine card
|
|
||||||
page.once('dialog', dialog => dialog.accept());
|
|
||||||
await page.click(`.engine-card[data-engine-id="${engineId}"]`);
|
|
||||||
// Wait for the switcher to mark it active
|
|
||||||
await expect(page.locator(`.engine-card[data-engine-id="${engineId}"]`)).toHaveClass(/active/, { timeout: 15_000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: get the current engine id from the debug probe.
|
|
||||||
*/
|
|
||||||
async function getActiveEngineId(page) {
|
|
||||||
return page.evaluate(() => window.__nisps?.activeEngine?.id ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: get the current MLP output count.
|
|
||||||
*/
|
|
||||||
async function getOutputCount(page) {
|
|
||||||
return page.evaluate(() => window.__nisps?.getOutputs()?.length ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Expose activeEngine on the debug probe so tests can query it
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
test.describe('Engine switching', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
// Extend the debug probe with engine-related getters
|
|
||||||
await page.evaluate(() => {
|
|
||||||
if (window.__nisps) {
|
|
||||||
// These are closures over the module-scoped vars in a-app.js,
|
|
||||||
// but we can read them via the existing probe's eocChain and other refs.
|
|
||||||
// The probe already exposes getOutputs() which reflects N_OUTPUTS.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Default state', () => {
|
|
||||||
test('default engine is C15 Shaper-Feedback', async ({ page }) => {
|
|
||||||
const btnText = await page.locator('#synth-mode-btn').textContent();
|
|
||||||
// The synth mode button should reflect the default engine (may say "Synth" or the engine name)
|
|
||||||
expect(btnText).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('default output count is 126 (C15)', async ({ page }) => {
|
|
||||||
const count = await getOutputCount(page);
|
|
||||||
// Default mode is visual (20 outputs), not synth
|
|
||||||
// Switch to synth mode first
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
const synthCount = await getOutputCount(page);
|
|
||||||
expect(synthCount).toBe(126);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap shows 126 cells in synth mode', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
|
|
||||||
expect(cellCount).toBe(126);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Engine switcher UI', () => {
|
|
||||||
test('synth drawer contains engine cards', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="synth"]');
|
|
||||||
const cards = page.locator('.engine-card');
|
|
||||||
// C15 shaper-feedback, additive, fm, modular
|
|
||||||
await expect(cards).toHaveCount(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('C15 card is active by default', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="synth"]');
|
|
||||||
const c15Card = page.locator('.engine-card[data-engine-id="shaper-feedback"]');
|
|
||||||
await expect(c15Card).toHaveClass(/active/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('additive and FM cards are not active by default', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="synth"]');
|
|
||||||
await expect(page.locator('.engine-card[data-engine-id="additive"]')).not.toHaveClass(/active/);
|
|
||||||
await expect(page.locator('.engine-card[data-engine-id="fm"]')).not.toHaveClass(/active/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Switch to Additive engine', () => {
|
|
||||||
test('switching resizes MLP to 48 outputs', async ({ page }) => {
|
|
||||||
// Go to synth mode first
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Switch engine
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const count = await getOutputCount(page);
|
|
||||||
expect(count).toBe(48);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap rebuilds with 48 cells', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
|
|
||||||
expect(cellCount).toBe(48);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('outputs are bounded [0, 1]', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
expect(outputs).toHaveLength(48);
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('different inputs produce different outputs', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const out1 = await page.evaluate(() => {
|
|
||||||
window.__nisps.setInputs(0.1, 0.1);
|
|
||||||
return window.__nisps.getOutputs();
|
|
||||||
});
|
|
||||||
const out2 = await page.evaluate(() => {
|
|
||||||
window.__nisps.setInputs(0.9, 0.9);
|
|
||||||
return window.__nisps.getOutputs();
|
|
||||||
});
|
|
||||||
const anyDiff = out1.some((v, i) => Math.abs(v - out2[i]) > 0.001);
|
|
||||||
expect(anyDiff).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('training works with additive param count', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const loss = await page.evaluate(() => {
|
|
||||||
const nisps = window.__nisps;
|
|
||||||
// Add two contrasting examples
|
|
||||||
nisps.iml.addExample([0.1, 0.1], new Array(48).fill(0.2));
|
|
||||||
nisps.iml.addExample([0.9, 0.9], new Array(48).fill(0.8));
|
|
||||||
return nisps.train();
|
|
||||||
});
|
|
||||||
expect(loss).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(loss).toBeLessThan(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Switch to FM Matrix engine', () => {
|
|
||||||
test('switching resizes MLP to 55 outputs', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const count = await getOutputCount(page);
|
|
||||||
expect(count).toBe(55);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap rebuilds with 55 cells', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
|
|
||||||
expect(cellCount).toBe(55);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('outputs are bounded [0, 1]', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
expect(outputs).toHaveLength(55);
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('training works with FM param count', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const loss = await page.evaluate(() => {
|
|
||||||
const nisps = window.__nisps;
|
|
||||||
nisps.iml.addExample([0.2, 0.8], new Array(55).fill(0.3));
|
|
||||||
nisps.iml.addExample([0.8, 0.2], new Array(55).fill(0.7));
|
|
||||||
return nisps.train();
|
|
||||||
});
|
|
||||||
expect(loss).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(loss).toBeLessThan(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Round-trip switching', () => {
|
|
||||||
test('switching C15 → Additive → C15 restores 126 outputs', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Switch to additive
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await getOutputCount(page)).toBe(48);
|
|
||||||
|
|
||||||
// Switch back to C15
|
|
||||||
await switchEngine(page, 'shaper-feedback');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await getOutputCount(page)).toBe(126);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('switching C15 → FM → Additive → C15 restores correctly each time', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await getOutputCount(page)).toBe(55);
|
|
||||||
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await getOutputCount(page)).toBe(48);
|
|
||||||
|
|
||||||
await switchEngine(page, 'shaper-feedback');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await getOutputCount(page)).toBe(126);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Warm-start weight preservation', () => {
|
|
||||||
test('hidden layer weights are preserved across resize', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Get weights before switch (first 100 hidden-layer weights)
|
|
||||||
const beforeWeights = await page.evaluate(() => {
|
|
||||||
return window.__nisps.getWeights().slice(0, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Switch to additive (48 outputs, hidden layers unchanged)
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Get weights after — first 100 should be identical (hidden layer prefix)
|
|
||||||
const afterWeights = await page.evaluate(() => {
|
|
||||||
return window.__nisps.getWeights().slice(0, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hidden layer weights should be preserved (warm-start)
|
|
||||||
let matchCount = 0;
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
if (Math.abs(beforeWeights[i] - afterWeights[i]) < 1e-6) matchCount++;
|
|
||||||
}
|
|
||||||
// Allow some tolerance — at least 90% should match
|
|
||||||
expect(matchCount).toBeGreaterThan(90);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('EOC chain across engine switches', () => {
|
|
||||||
test('EOC drawer is accessible from all engines', async ({ page }) => {
|
|
||||||
// Open EOC drawer with default C15
|
|
||||||
await page.click('[data-drawer="eoc"]');
|
|
||||||
await expect(page.locator('#drawer-eoc')).not.toHaveClass(/hidden/);
|
|
||||||
|
|
||||||
// Switch to additive
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// EOC drawer should still be accessible — engine switch may have closed it
|
|
||||||
// so click the dock icon; if already open, clicking toggles closed then re-open
|
|
||||||
const eocDrawer = page.locator('#drawer-eoc');
|
|
||||||
if (await eocDrawer.evaluate(el => el.classList.contains('hidden'))) {
|
|
||||||
await page.click('[data-drawer="eoc"]');
|
|
||||||
}
|
|
||||||
await expect(eocDrawer).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NISPS mode selector is present in EOC drawer', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="eoc"]');
|
|
||||||
const modeButtons = page.locator('.eoc-nisps-bar .pill-opt');
|
|
||||||
const count = await modeButtons.count();
|
|
||||||
expect(count).toBe(4); // Bypass, Shared, Linked, Independent
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('SynthVisualizer with different engines', () => {
|
|
||||||
test('synth-vis-canvas is visible in synth mode for all engines', async ({ page }) => {
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check canvas is visible for C15
|
|
||||||
const canvas = page.locator('#synth-vis-canvas');
|
|
||||||
await expect(canvas).toBeVisible();
|
|
||||||
|
|
||||||
// Switch to additive — canvas should remain visible
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await expect(canvas).toBeVisible();
|
|
||||||
|
|
||||||
// Switch to FM — canvas should remain visible
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await expect(canvas).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('No console errors during engine switch', () => {
|
|
||||||
test('switching to additive produces no errors', async ({ page }) => {
|
|
||||||
const errors = [];
|
|
||||||
page.on('pageerror', err => errors.push(err.message));
|
|
||||||
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Filter out known non-critical errors
|
|
||||||
const critical = errors.filter(e =>
|
|
||||||
!e.includes('ResizeObserver') && !e.includes('net::ERR')
|
|
||||||
);
|
|
||||||
expect(critical).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('switching to FM produces no errors', async ({ page }) => {
|
|
||||||
const errors = [];
|
|
||||||
page.on('pageerror', err => errors.push(err.message));
|
|
||||||
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const critical = errors.filter(e =>
|
|
||||||
!e.includes('ResizeObserver') && !e.includes('net::ERR')
|
|
||||||
);
|
|
||||||
expect(critical).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('round-trip switching produces no errors', async ({ page }) => {
|
|
||||||
const errors = [];
|
|
||||||
page.on('pageerror', err => errors.push(err.message));
|
|
||||||
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
await switchEngine(page, 'additive');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await switchEngine(page, 'fm');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await switchEngine(page, 'shaper-feedback');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const critical = errors.filter(e =>
|
|
||||||
!e.includes('ResizeObserver') && !e.includes('net::ERR')
|
|
||||||
);
|
|
||||||
expect(critical).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
/**
|
|
||||||
* Shared helpers for e2e tests.
|
|
||||||
*/
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a-immersive with ?debug=1 and wait for the WASM engine to
|
|
||||||
* initialise and expose window.__nisps.
|
|
||||||
*
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {string} extraParams - additional query string, e.g. '&preset=beginner-1'
|
|
||||||
*/
|
|
||||||
async function loadApp(page, extraParams = '') {
|
|
||||||
// Clear app state but mark help as seen so the overlay doesn't block clicks.
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.removeItem('nisps-a-immersive');
|
|
||||||
localStorage.setItem('nisps-help-seen', '1');
|
|
||||||
});
|
|
||||||
await page.goto(`/a-immersive.html?debug=1${extraParams}`);
|
|
||||||
// Wait until the debug probe is ready (WASM init is async).
|
|
||||||
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current text content of the status line.
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function statusText(page) {
|
|
||||||
return page.locator('#status-text').textContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { loadApp, statusText };
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/**
|
|
||||||
* Input pipeline tests — joystick → MLP inputs → outputs → heatmap.
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp } = require('./helpers');
|
|
||||||
|
|
||||||
test.describe('Joystick → output pipeline', () => {
|
|
||||||
test('different input positions produce different outputs', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(0.1, 0.1));
|
|
||||||
const out1 = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(0.9, 0.9));
|
|
||||||
const out2 = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
|
|
||||||
const anyChanged = out1.some((v, i) => Math.abs(v - out2[i]) > 0.0001);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all outputs remain in [0, 1] across input positions', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const corners = [[0, 0], [0, 1], [1, 0], [1, 1], [0.5, 0.5]];
|
|
||||||
for (const [x, y] of corners) {
|
|
||||||
await page.evaluate(([x, y]) => window.__nisps.setInputs(x, y), [x, y]);
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap bar widths change when inputs change', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
|
|
||||||
const widths1 = await page.evaluate(() =>
|
|
||||||
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move to far corner
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(0.95, 0.05));
|
|
||||||
|
|
||||||
const widths2 = await page.evaluate(() =>
|
|
||||||
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
|
|
||||||
);
|
|
||||||
|
|
||||||
const anyChanged = widths1.some((w, i) => w !== widths2[i]);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mouse drag on joystick container updates MLP inputs', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
|
|
||||||
const box = await page.locator('#joystick-container').boundingBox();
|
|
||||||
const cx = box.x + box.width / 2;
|
|
||||||
const cy = box.y + box.height / 2;
|
|
||||||
|
|
||||||
// Drag from center towards bottom-right
|
|
||||||
await page.mouse.move(cx, cy);
|
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(cx + box.width * 0.3, cy + box.height * 0.3, { steps: 10 });
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
// After drag, at least one input axis should have moved from 0.5
|
|
||||||
const [x, y] = await page.evaluate(() => [
|
|
||||||
window.__nisps.iml.inputState[0],
|
|
||||||
window.__nisps.iml.inputState[1],
|
|
||||||
]);
|
|
||||||
const moved = Math.abs(x - 0.5) > 0.01 || Math.abs(y - 0.5) > 0.01;
|
|
||||||
expect(moved).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setInputs clamps values to [0, 1]', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(-5, 99));
|
|
||||||
const [x, y] = await page.evaluate(() => [
|
|
||||||
window.__nisps.iml.inputState[0],
|
|
||||||
window.__nisps.iml.inputState[1],
|
|
||||||
]);
|
|
||||||
expect(x).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(x).toBeLessThanOrEqual(1);
|
|
||||||
expect(y).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(y).toBeLessThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('after training, moving inputs produces smoothly varying outputs', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
|
|
||||||
// Load calm-to-chaotic preset (3 examples spanning the input space)
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await page.click('[data-preset="calm-to-chaotic"]');
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => document.getElementById('status-text').textContent.includes('loss'),
|
|
||||||
{ timeout: 15_000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sample 5 positions and verify all outputs are bounded
|
|
||||||
const positions = [0.0, 0.25, 0.5, 0.75, 1.0];
|
|
||||||
for (const t of positions) {
|
|
||||||
await page.evaluate((t) => window.__nisps.setInputs(t, 1 - t), t);
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
expect(outputs).toHaveLength(126);
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
/**
|
|
||||||
* ML engine sanity tests — verify the WASM IML behaves correctly:
|
|
||||||
* - outputs are always bounded [0, 1]
|
|
||||||
* - randomize produces different outputs
|
|
||||||
* - thumbs-up captures the current rawParamValues as the training label
|
|
||||||
* - training completes and produces a finite loss
|
|
||||||
* - thumbs-down moves weights and changes outputs
|
|
||||||
* - async training (triggered by thumbs-up) updates the status line
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp, statusText } = require('./helpers');
|
|
||||||
|
|
||||||
// Two contrasting examples with known inputs and all-low / all-high targets.
|
|
||||||
const EXAMPLE_LOW = { input: [0.1, 0.9], output: new Array(126).fill(0.1) };
|
|
||||||
const EXAMPLE_HIGH = { input: [0.9, 0.1], output: new Array(126).fill(0.9) };
|
|
||||||
|
|
||||||
test.describe('ML engine (WASM IML)', () => {
|
|
||||||
test('probe is exposed after WASM init', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const probe = await page.evaluate(() => typeof window.__nisps);
|
|
||||||
expect(probe).toBe('object');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initial outputs are all in [0, 1]', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
expect(outputs).toHaveLength(126);
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initial state is 0 examples, untrained', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const count = await page.evaluate(() => window.__nisps.getExampleCount());
|
|
||||||
expect(count).toBe(0);
|
|
||||||
const loss = await page.evaluate(() => window.__nisps.getLoss());
|
|
||||||
expect(loss).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('randomize changes outputs', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const before = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
await page.evaluate(() => window.__nisps.randomise());
|
|
||||||
const after = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.001);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('thumbs-up increments example count by 1', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(() => window.__nisps.thumbsUp());
|
|
||||||
// Give async training a moment to start but we only need to check example count
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
const count = await page.evaluate(() => window.__nisps.getExampleCount());
|
|
||||||
expect(count).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('thumbs-up captures current input position and all 126 output values', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
// Set a known joystick position via the probe
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(0.25, 0.75));
|
|
||||||
await page.evaluate(() => window.__nisps.thumbsUp());
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
|
|
||||||
const { features, labels } = await page.evaluate(() => ({
|
|
||||||
features: window.__nisps.iml.dataset.features,
|
|
||||||
labels: window.__nisps.iml.dataset.labels,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(features).toHaveLength(1);
|
|
||||||
expect(labels).toHaveLength(1);
|
|
||||||
|
|
||||||
// Input dimension = 2 (joystick x/y, pipeline-processed)
|
|
||||||
expect(features[0]).toHaveLength(2);
|
|
||||||
// The input pipeline may transform values; inputs must stay in [0, 1]
|
|
||||||
expect(features[0][0]).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(features[0][0]).toBeLessThanOrEqual(1);
|
|
||||||
expect(features[0][1]).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(features[0][1]).toBeLessThanOrEqual(1);
|
|
||||||
|
|
||||||
// Labels = all 126 output values, captured from rawParamValues at click time
|
|
||||||
expect(labels[0]).toHaveLength(126);
|
|
||||||
for (const v of labels[0]) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sync train() returns a finite non-negative loss', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const loss = await page.evaluate(() => window.__nisps.train());
|
|
||||||
expect(typeof loss).toBe('number');
|
|
||||||
expect(isFinite(loss)).toBe(true);
|
|
||||||
expect(loss).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('training with contrasting examples produces a lower loss than initial', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
// Initial inference — loss is null (never trained), so randomise to get a baseline
|
|
||||||
await page.evaluate(() => window.__nisps.randomise());
|
|
||||||
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const loss1 = await page.evaluate(() => window.__nisps.train());
|
|
||||||
const loss2 = await page.evaluate(() => window.__nisps.train());
|
|
||||||
|
|
||||||
// Second training run on same data should converge further (loss2 <= loss1)
|
|
||||||
expect(loss2).toBeLessThanOrEqual(loss1 + 1e-6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('status line reflects example count and loss after training', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
window.__nisps.train();
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
// updateStatus() is called inside trainModel()
|
|
||||||
const text = await page.locator('#status-text').textContent();
|
|
||||||
expect(text).toContain('2 examples');
|
|
||||||
expect(text).toContain('loss');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('thumbs-down changes outputs (weight noise)', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const before = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
await page.evaluate(() => window.__nisps.thumbsDown());
|
|
||||||
const after = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.0001);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('async training via thumbs-up button updates status with loss', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('#btn-thumbsup');
|
|
||||||
// Wait for the async training to complete and status to update
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => document.getElementById('status-text').textContent.includes('loss'),
|
|
||||||
{ timeout: 15_000 }
|
|
||||||
);
|
|
||||||
const text = await page.locator('#status-text').textContent();
|
|
||||||
expect(text).toContain('1 example');
|
|
||||||
expect(text).toContain('loss');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clear examples resets to 0 and marks untrained', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.train();
|
|
||||||
}, [EXAMPLE_LOW]);
|
|
||||||
expect(await page.evaluate(() => window.__nisps.getExampleCount())).toBe(1);
|
|
||||||
|
|
||||||
await page.evaluate(() => window.__nisps.clearExamples());
|
|
||||||
expect(await page.evaluate(() => window.__nisps.getExampleCount())).toBe(0);
|
|
||||||
const text = await page.locator('#status-text').textContent();
|
|
||||||
expect(text).toContain('0 examples');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
/**
|
|
||||||
* Modular mode e2e tests.
|
|
||||||
*
|
|
||||||
* These tests exercise the modular engine end-to-end via the debug probe
|
|
||||||
* (?debug=1, window.__nisps).
|
|
||||||
*
|
|
||||||
* NB: modular paramMeta = 4 ADSR × 4 + 8 LFO × 2 + 48 × 10 matrix = 512.
|
|
||||||
* Every matrix cell is in paramMeta so `modular-ui.updateLive()` can
|
|
||||||
* mirror live MLP outputs into the matrix DOM. Silence-on-joystick
|
|
||||||
* regressions are prevented at the DSP level: each sub-engine's
|
|
||||||
* `amp_val = clamp(base_amp + max(0, mod_amp))`, so matrix d08_amp
|
|
||||||
* cells can only add to the amp floor and base_amp=1.0 (default)
|
|
||||||
* keeps the voice audible regardless of what the MLP outputs.
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp } = require('./helpers');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch the active engine to `modular` via the engine-switcher UI.
|
|
||||||
* Waits until window.__nisps.activeEngineId === 'modular'.
|
|
||||||
*/
|
|
||||||
async function switchToModular(page) {
|
|
||||||
// Open the synth drawer first (needed to see the engine cards).
|
|
||||||
const drawer = page.locator('#drawer-synth');
|
|
||||||
if (await drawer.evaluate(el => el.classList.contains('hidden'))) {
|
|
||||||
await page.click('[data-drawer="synth"]');
|
|
||||||
}
|
|
||||||
page.once('dialog', d => d.accept());
|
|
||||||
await page.click('.engine-card[data-engine-id="modular"]');
|
|
||||||
// Wait until setActiveEngine completes AND the modular dock icon becomes
|
|
||||||
// visible — the dock icon is revealed from the tail end of setActiveEngine
|
|
||||||
// so this guarantees the initial modular-ui.refresh() restore pass has run.
|
|
||||||
// Without this we race: test code can fire before modularUI.show()
|
|
||||||
// reaches refresh()'s pendingRestore branch, which then wipes the test's
|
|
||||||
// subsequent sub-engine swap.
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.__nisps?.activeEngineId === 'modular',
|
|
||||||
null,
|
|
||||||
{ timeout: 20_000 }
|
|
||||||
);
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => !document.querySelector('.dock-icon[data-drawer="modular"]')?.classList.contains('hidden'),
|
|
||||||
null,
|
|
||||||
{ timeout: 20_000 }
|
|
||||||
);
|
|
||||||
// Also yield one extra microtask so any synchronous deferred work queued
|
|
||||||
// inside setActiveEngine settles before we start poking the engine.
|
|
||||||
await page.evaluate(() => new Promise(r => setTimeout(r, 0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Modular mode', () => {
|
|
||||||
|
|
||||||
test('switching to modular yields paramCount = 512', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
const count = await page.evaluate(() => window.__nisps.paramCount);
|
|
||||||
// 4 ADSR × (attack, decay, sustain, release) = 16
|
|
||||||
// 8 LFO × (rate, morph) = 16
|
|
||||||
// 48 × 10 matrix cells = 480
|
|
||||||
expect(count).toBe(512);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('matrix cells are in paramMeta so modular-ui updateLive can read them', async ({ page }) => {
|
|
||||||
// Regression guard: modular-ui.js `updateLive()` depends on every
|
|
||||||
// matrix cell being present in paramMeta so it can map MLP output
|
|
||||||
// indices to cell DOM. A previous fix that gated matrix cells on
|
|
||||||
// an opt-in flag broke the matrix UI's live visualisation when the
|
|
||||||
// joystick moved. If this test fails, inspect _rebuildParamMeta().
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
const info = await page.evaluate(() => {
|
|
||||||
const meta = window.__nisps.activeEngine.paramMeta;
|
|
||||||
const matrix = meta.filter(m => m.group && m.group.startsWith('Matrix/'));
|
|
||||||
const destBuckets = {};
|
|
||||||
for (const m of matrix) {
|
|
||||||
const d = m.group.split('/')[1];
|
|
||||||
destBuckets[d] = (destBuckets[d] || 0) + 1;
|
|
||||||
}
|
|
||||||
return { total: meta.length, matrixCount: matrix.length, destBuckets };
|
|
||||||
});
|
|
||||||
expect(info.total).toBe(512);
|
|
||||||
expect(info.matrixCount).toBe(480);
|
|
||||||
// 10 destinations × 48 sources each
|
|
||||||
expect(Object.keys(info.destBuckets).length).toBe(10);
|
|
||||||
for (const count of Object.values(info.destBuckets)) {
|
|
||||||
expect(count).toBe(48);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('debug probe exposes modular hooks', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
const hooks = await page.evaluate(() => ({
|
|
||||||
hasGet: typeof window.__nisps.getModularState === 'function',
|
|
||||||
hasSet: typeof window.__nisps.setModularState === 'function',
|
|
||||||
hasSwap: typeof window.__nisps.setModularSubEngine === 'function',
|
|
||||||
hasPreset: typeof window.__nisps.applyModularPreset === 'function',
|
|
||||||
hasCounts: typeof window.__nisps.setModularSourceCount === 'function',
|
|
||||||
presetList: window.__nisps.listModularPresets?.()?.length ?? 0,
|
|
||||||
}));
|
|
||||||
expect(hooks.hasGet).toBe(true);
|
|
||||||
expect(hooks.hasSet).toBe(true);
|
|
||||||
expect(hooks.hasSwap).toBe(true);
|
|
||||||
expect(hooks.hasPreset).toBe(true);
|
|
||||||
expect(hooks.hasCounts).toBe(true);
|
|
||||||
expect(hooks.presetList).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sub-engine swap keeps paramCount = 512', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
for (const sub of ['additive', 'fm', 'subtractive']) {
|
|
||||||
await page.evaluate(async (id) => {
|
|
||||||
await window.__nisps.setModularSubEngine(id);
|
|
||||||
}, sub);
|
|
||||||
const info = await page.evaluate(() => ({
|
|
||||||
paramCount: window.__nisps.paramCount,
|
|
||||||
subId: window.__nisps.activeEngine?.activeSubEngineId,
|
|
||||||
}));
|
|
||||||
expect(info.subId).toBe(sub);
|
|
||||||
expect(info.paramCount).toBe(512);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('destNames differ between sub-engines', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
const sub = await page.evaluate(() => window.__nisps.activeEngine?.destNames);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.__nisps.setModularSubEngine('fm');
|
|
||||||
});
|
|
||||||
const fm = await page.evaluate(() => window.__nisps.activeEngine?.destNames);
|
|
||||||
expect(sub).toBeTruthy();
|
|
||||||
expect(fm).toBeTruthy();
|
|
||||||
expect(sub).toEqual(['pitch','osc2_detune','osc3_detune','osc_mix_bal','noise_level','cutoff','resonance','filter_env_amt','amp','pan']);
|
|
||||||
expect(fm[1]).toBe('op1_level'); // fm-specific
|
|
||||||
expect(sub).not.toEqual(fm);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ADSR count change rebuilds paramMeta', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
const baseline = await page.evaluate(() => window.__nisps.paramCount);
|
|
||||||
expect(baseline).toBe(512);
|
|
||||||
|
|
||||||
await page.evaluate(() => window.__nisps.setModularSourceCount(6, 8));
|
|
||||||
const after = await page.evaluate(() => window.__nisps.paramCount);
|
|
||||||
// 6 ADSR × 4 + 8 LFO × 2 + 48 × 10 = 24 + 16 + 480 = 520
|
|
||||||
expect(after).toBe(520);
|
|
||||||
|
|
||||||
await page.evaluate(() => window.__nisps.setModularSourceCount(4, 8));
|
|
||||||
const reset = await page.evaluate(() => window.__nisps.paramCount);
|
|
||||||
expect(reset).toBe(512);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getState returns a snapshot with raw dsp values', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
const snap = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(snap).toBeTruthy();
|
|
||||||
expect(snap.version).toBe(1);
|
|
||||||
expect(snap.subEngine).toBe('subtractive');
|
|
||||||
expect(typeof snap.dsp).toBe('object');
|
|
||||||
// Default patch pre-arms ADSR1 but does not route it to amp — voice
|
|
||||||
// stays audible because base_amp defaults to 1.0.
|
|
||||||
expect(snap.dsp['MM_ADSR/00_adsr01_enable']).toBeCloseTo(1.0, 4);
|
|
||||||
expect(snap.dsp['4_Master/04_base_amp']).toBeCloseTo(1.0, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('matrix cell persistence across setState', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
// Pick a distinctive cell: ADSR2 (s=1) → cutoff (d=5) on subtractive.
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const engine = window.__nisps.activeEngine;
|
|
||||||
const idx = engine.paramMeta.findIndex(m =>
|
|
||||||
m.label === 'MM_Matrix/s01_d05_cutoff');
|
|
||||||
if (idx < 0) throw new Error('no s01_d05_cutoff cell in paramMeta');
|
|
||||||
// paramMeta min=-1 max=1; 0.9 in norm = 0.8 raw.
|
|
||||||
engine.setParam(idx, 0.9);
|
|
||||||
});
|
|
||||||
|
|
||||||
const snap = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(snap.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
|
|
||||||
|
|
||||||
// Mutate further, then restore.
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const engine = window.__nisps.activeEngine;
|
|
||||||
const idx = engine.paramMeta.findIndex(m =>
|
|
||||||
m.label === 'MM_Matrix/s01_d05_cutoff');
|
|
||||||
engine.setParam(idx, 0.1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const midSnap = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(midSnap.dsp['MM_Matrix/s01_d05_cutoff']).not.toBeCloseTo(0.8, 4);
|
|
||||||
|
|
||||||
await page.evaluate(async (s) => {
|
|
||||||
await window.__nisps.setModularState(s);
|
|
||||||
}, snap);
|
|
||||||
|
|
||||||
const restored = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(restored.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('modular DSP state survives a page reload', async ({ page }) => {
|
|
||||||
// NOTE: don't use loadApp() because it installs an addInitScript that
|
|
||||||
// clears nisps-a-immersive on every navigation — including our reload.
|
|
||||||
// Replicate loadApp's bootstrap inline, using a localStorage sentinel
|
|
||||||
// (NOT window.__x) so the "first nav only" guard survives subsequent
|
|
||||||
// navigations on the same origin.
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
if (!localStorage.getItem('__nisps-test-bootstrapped')) {
|
|
||||||
localStorage.setItem('__nisps-test-bootstrapped', '1');
|
|
||||||
localStorage.removeItem('nisps-a-immersive');
|
|
||||||
}
|
|
||||||
localStorage.setItem('nisps-help-seen', '1');
|
|
||||||
});
|
|
||||||
await page.goto('/a-immersive.html?debug=1');
|
|
||||||
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
|
|
||||||
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
// Set a distinctive value, save, then reload the page (localStorage
|
|
||||||
// is now preserved across the nav because __nispsTestBootstrapped is set).
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const engine = window.__nisps.activeEngine;
|
|
||||||
engine._setRawByLabel('3_Filter/01_resonance', 0.73);
|
|
||||||
});
|
|
||||||
await page.evaluate(() => window.__nisps.saveState());
|
|
||||||
|
|
||||||
await page.goto('/a-immersive.html?debug=1');
|
|
||||||
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
|
|
||||||
|
|
||||||
// Engine is deferred-constructed; clicking the modular card re-instantiates
|
|
||||||
// it and the pending DSP state should be applied before setActiveEngine.
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
const restored = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(restored).toBeTruthy();
|
|
||||||
expect(restored.dsp['3_Filter/01_resonance']).toBeCloseTo(0.73, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preset apply: plucky bass sets the expected matrix routes', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
const ok = await page.evaluate(async () => {
|
|
||||||
return await window.__nisps.applyModularPreset('modular-plucky-bass');
|
|
||||||
});
|
|
||||||
expect(ok).toBe(true);
|
|
||||||
|
|
||||||
const snap = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(snap.subEngine).toBe('subtractive');
|
|
||||||
// ADSR2 fast decay
|
|
||||||
expect(snap.dsp['MM_ADSR/01_adsr02_decay']).toBeCloseTo(0.15, 4);
|
|
||||||
// Matrix: ADSR2 (s01) → cutoff (d05) at raw 0.8
|
|
||||||
expect(snap.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
|
|
||||||
// Filter cutoff moved to 400 Hz
|
|
||||||
expect(snap.dsp['3_Filter/00_cutoff']).toBeCloseTo(400, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preset apply: DX bell swaps to fm sub-engine', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.__nisps.applyModularPreset('modular-dx-bell');
|
|
||||||
});
|
|
||||||
const snap = await page.evaluate(() => window.__nisps.getModularState());
|
|
||||||
expect(snap.subEngine).toBe('fm');
|
|
||||||
expect(snap.dsp['MM_Matrix/s02_d03_op3_level']).toBeCloseTo(1.0, 4);
|
|
||||||
|
|
||||||
// paramCount should still be 512 after the cross-engine swap.
|
|
||||||
const count = await page.evaluate(() => window.__nisps.paramCount);
|
|
||||||
expect(count).toBe(512);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initial outputs are in [0,1] after modular swap', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await switchToModular(page);
|
|
||||||
|
|
||||||
// Set inputs so the MLP runs a forward pass.
|
|
||||||
await page.evaluate(() => window.__nisps.setInputs(0.3, 0.7));
|
|
||||||
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
expect(outputs.length).toBe(512);
|
|
||||||
for (const v of outputs) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
/**
|
|
||||||
* State persistence tests — localStorage round-trip and URL param application.
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp, statusText } = require('./helpers');
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'nisps-a-immersive';
|
|
||||||
|
|
||||||
test.describe('State persistence', () => {
|
|
||||||
test('fresh load with no localStorage starts untrained', async ({ page }) => {
|
|
||||||
await loadApp(page); // helpers.js clears localStorage before load
|
|
||||||
const text = await statusText(page);
|
|
||||||
expect(text).toContain('0 examples');
|
|
||||||
expect(text).toContain('untrained');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('URL ?preset=beginner-1 applies preset on load', async ({ page }) => {
|
|
||||||
await loadApp(page, '&preset=beginner-1');
|
|
||||||
const selected = await page.locator('#synth-preset-select').inputValue();
|
|
||||||
expect(selected).toBe('beginner-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('URL ?preset=advanced-2 applies advanced preset', async ({ page }) => {
|
|
||||||
await loadApp(page, '&preset=advanced-2');
|
|
||||||
const selected = await page.locator('#synth-preset-select').inputValue();
|
|
||||||
expect(selected).toBe('advanced-2');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('URL ?spread=0 is accepted without crash', async ({ page }) => {
|
|
||||||
await loadApp(page, '&spread=0');
|
|
||||||
// App should be functional — probe must still exist
|
|
||||||
const probe = await page.evaluate(() => typeof window.__nisps);
|
|
||||||
expect(probe).toBe('object');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('URL ?spread=1 is accepted without crash', async ({ page }) => {
|
|
||||||
await loadApp(page, '&spread=1');
|
|
||||||
const probe = await page.evaluate(() => typeof window.__nisps);
|
|
||||||
expect(probe).toBe('object');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('examples + weights persist across reload via localStorage', async ({ page }) => {
|
|
||||||
// Navigate fresh — no init script so localStorage is untouched between loads.
|
|
||||||
// Suppress the help overlay by pre-setting nisps-help-seen via evaluate.
|
|
||||||
await page.goto(`/a-immersive.html?debug=1`);
|
|
||||||
await page.evaluate(() => localStorage.removeItem('nisps-a-immersive'));
|
|
||||||
await page.evaluate(() => localStorage.setItem('nisps-help-seen', '1'));
|
|
||||||
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
|
|
||||||
|
|
||||||
// Write 2 examples directly to localStorage in the app's save format.
|
|
||||||
await page.evaluate(([key]) => {
|
|
||||||
const state = {
|
|
||||||
features: [[0.1, 0.9], [0.9, 0.1]],
|
|
||||||
labels: [new Array(126).fill(0.2), new Array(126).fill(0.8)],
|
|
||||||
handFeatures: [],
|
|
||||||
handLabels: [],
|
|
||||||
noiseLevel: 0.05,
|
|
||||||
outputMode: 'visual',
|
|
||||||
inputMode: 'joystick',
|
|
||||||
joyX: 0.5, joyY: 0.5,
|
|
||||||
groupOverrides: null,
|
|
||||||
visualOverrides: null,
|
|
||||||
midiCCOverrides: null,
|
|
||||||
audioCanvasState: null,
|
|
||||||
synthPresetId: null,
|
|
||||||
};
|
|
||||||
localStorage.setItem(key, JSON.stringify(state));
|
|
||||||
}, [STORAGE_KEY]);
|
|
||||||
|
|
||||||
// Reload — no addInitScript registered, so localStorage is preserved.
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
|
|
||||||
|
|
||||||
// loadState() runs sync training; wait for status to show a loss value.
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => document.getElementById('status-text').textContent.includes('loss'),
|
|
||||||
{ timeout: 15_000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const count = await page.evaluate(() => window.__nisps.getExampleCount());
|
|
||||||
expect(count).toBe(2);
|
|
||||||
const text = await statusText(page);
|
|
||||||
expect(text).toContain('2 examples');
|
|
||||||
expect(text).toContain('loss');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('saveState probe writes valid JSON to localStorage', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(() => window.__nisps.saveState());
|
|
||||||
|
|
||||||
const raw = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
|
|
||||||
expect(raw).not.toBeNull();
|
|
||||||
|
|
||||||
const state = JSON.parse(raw);
|
|
||||||
expect(Array.isArray(state.features)).toBe(true);
|
|
||||||
expect(Array.isArray(state.labels)).toBe(true);
|
|
||||||
expect(typeof state.outputMode).toBe('string');
|
|
||||||
expect(typeof state.noiseLevel).toBe('number');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('localStorage state is overwritten on next saveState call', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(() => window.__nisps.saveState());
|
|
||||||
|
|
||||||
const before = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
|
|
||||||
|
|
||||||
// Add an example and save again
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.__nisps.iml.addExample([0.3, 0.7], new Array(126).fill(0.5));
|
|
||||||
window.__nisps.saveState();
|
|
||||||
});
|
|
||||||
|
|
||||||
const after = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
|
|
||||||
// State changed — the feature array should now include our example
|
|
||||||
const parsed = JSON.parse(after);
|
|
||||||
expect(parsed.features.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
/**
|
|
||||||
* UI state machine tests — drawers, mode switching, preset chips, keyboard shortcuts.
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp, statusText } = require('./helpers');
|
|
||||||
|
|
||||||
test.describe('UI interactions', () => {
|
|
||||||
test.describe('Dock drawers', () => {
|
|
||||||
test('Train dock button opens training drawer', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await expect(page.locator('#drawer-training')).toHaveClass(/hidden/);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Drawer close button hides the drawer', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
|
|
||||||
await page.click('#drawer-training .drawer-close');
|
|
||||||
await expect(page.locator('#drawer-training')).toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Mode dock button opens mode drawer', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await expect(page.locator('#drawer-mode')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Engine dock button opens engine drawer', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="params"]');
|
|
||||||
await expect(page.locator('#drawer-params')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multiple drawers can be open simultaneously', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
// The app doesn't auto-close drawers — both can be open
|
|
||||||
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
|
|
||||||
await expect(page.locator('#drawer-mode')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Output mode switching', () => {
|
|
||||||
test('default mode is visual — synth quick controls hidden', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await expect(page.locator('#synth-quick-controls')).toHaveClass(/hidden/);
|
|
||||||
await expect(page.locator('#midi-cc-quick-controls')).toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('switching to Synth shows synth quick controls', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await expect(page.locator('#synth-quick-controls')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('switching to MIDI CC shows midi-cc quick controls', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="midi-cc"]');
|
|
||||||
await expect(page.locator('#midi-cc-quick-controls')).not.toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('switching back to Visual hides synth controls', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
await page.click('[data-mode="visual"]');
|
|
||||||
await expect(page.locator('#synth-quick-controls')).toHaveClass(/hidden/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap shows 20 bars in visual mode', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const count = await page.locator('.heatmap-cell').count();
|
|
||||||
expect(count).toBe(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('heatmap shows 126 bars in synth mode', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="mode"]');
|
|
||||||
await page.click('[data-mode="synth"]');
|
|
||||||
const count = await page.locator('.heatmap-cell').count();
|
|
||||||
expect(count).toBe(126);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Status line', () => {
|
|
||||||
test('shows 0 examples · untrained on fresh load', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const text = await statusText(page);
|
|
||||||
expect(text).toContain('0 examples');
|
|
||||||
expect(text).toContain('untrained');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Training controls', () => {
|
|
||||||
test('Randomize button changes heatmap bar widths', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
|
|
||||||
const widthsBefore = await page.evaluate(() =>
|
|
||||||
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
|
|
||||||
);
|
|
||||||
await page.click('#btn-randomize');
|
|
||||||
const widthsAfter = await page.evaluate(() =>
|
|
||||||
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
|
|
||||||
);
|
|
||||||
|
|
||||||
const anyChanged = widthsBefore.some((w, i) => w !== widthsAfter[i]);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preset chip loads examples and trains', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await page.click('[data-preset="calm-to-chaotic"]');
|
|
||||||
|
|
||||||
// calm-to-chaotic has 3 examples; training is async
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => document.getElementById('status-text').textContent.includes('loss'),
|
|
||||||
{ timeout: 15_000 }
|
|
||||||
);
|
|
||||||
const text = await statusText(page);
|
|
||||||
expect(text).toContain('3 examples');
|
|
||||||
expect(text).toContain('loss');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Clear Ex resets example count to 0', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.click('[data-drawer="training"]');
|
|
||||||
await page.click('[data-preset="calm-to-chaotic"]');
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => document.getElementById('status-text').textContent.includes('loss'),
|
|
||||||
{ timeout: 15_000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.click('#btn-clear-examples');
|
|
||||||
const text = await statusText(page);
|
|
||||||
expect(text).toContain('0 examples');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Keyboard shortcuts', () => {
|
|
||||||
test('key 2 triggers thumbs-up (adds example)', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const before = await page.evaluate(() => window.__nisps.getExampleCount());
|
|
||||||
await page.keyboard.press('2');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
const after = await page.evaluate(() => window.__nisps.getExampleCount());
|
|
||||||
expect(after).toBe(before + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('key 1 triggers thumbs-down (changes outputs)', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const before = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
await page.keyboard.press('1');
|
|
||||||
const after = await page.evaluate(() => window.__nisps.getOutputs());
|
|
||||||
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.0001);
|
|
||||||
expect(anyChanged).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('key Z triggers undo after thumbs-down', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const before = await page.evaluate(() => window.__nisps.getWeights());
|
|
||||||
await page.keyboard.press('1'); // thumbs-down
|
|
||||||
await page.keyboard.press('z'); // undo
|
|
||||||
const after = await page.evaluate(() => window.__nisps.getWeights());
|
|
||||||
// Weights should be restored (approximately)
|
|
||||||
const maxDiff = before.reduce((m, v, i) => Math.max(m, Math.abs(v - after[i])), 0);
|
|
||||||
expect(maxDiff).toBeLessThan(1e-4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
/**
|
|
||||||
* WASM API tests — verify new WasmIML methods:
|
|
||||||
* - inferBatch: batch inference across multiple input points
|
|
||||||
* - evalLoss: MSE loss evaluation without training
|
|
||||||
* - getLayerStats: per-layer weight statistics
|
|
||||||
* - lossHistory: full per-iteration loss curve after training
|
|
||||||
* - moveWeights with pin mask: pinned outputs stay unchanged
|
|
||||||
*/
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
const { loadApp } = require('./helpers');
|
|
||||||
|
|
||||||
const EXAMPLE_LOW = { input: [0.1, 0.9], output: new Array(126).fill(0.1) };
|
|
||||||
const EXAMPLE_HIGH = { input: [0.9, 0.1], output: new Array(126).fill(0.9) };
|
|
||||||
|
|
||||||
test.describe('inferBatch', () => {
|
|
||||||
test('returns correct count of output arrays, each length 126', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const result = await page.evaluate(() => {
|
|
||||||
const outputs = window.__nisps.iml.inferBatch([[0.2, 0.8], [0.5, 0.5], [0.9, 0.1]]);
|
|
||||||
return outputs.map(o => Array.from(o));
|
|
||||||
});
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
for (const arr of result) {
|
|
||||||
expect(arr).toHaveLength(126);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('matches individual inference within 1e-5', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const points = [[0.1, 0.2], [0.3, 0.7], [0.5, 0.5], [0.8, 0.1], [0.0, 1.0]];
|
|
||||||
const { batchResults, individualResults } = await page.evaluate((pts) => {
|
|
||||||
const iml = window.__nisps.iml;
|
|
||||||
const batch = iml.inferBatch(pts).map(o => Array.from(o));
|
|
||||||
const individual = pts.map(([x, y]) => {
|
|
||||||
iml.setInput(0, x);
|
|
||||||
iml.setInput(1, y);
|
|
||||||
iml.process();
|
|
||||||
return Array.from(iml.getOutputs());
|
|
||||||
});
|
|
||||||
return { batchResults: batch, individualResults: individual };
|
|
||||||
}, points);
|
|
||||||
|
|
||||||
expect(batchResults).toHaveLength(5);
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
for (let j = 0; j < 126; j++) {
|
|
||||||
expect(Math.abs(batchResults[i][j] - individualResults[i][j])).toBeLessThan(1e-5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all batch outputs are in [0, 1]', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const corners = [
|
|
||||||
[0, 0], [0, 1], [1, 0], [1, 1],
|
|
||||||
[0.5, 0], [0.5, 1], [0, 0.5], [1, 0.5],
|
|
||||||
[0.25, 0.75], [0.75, 0.25],
|
|
||||||
];
|
|
||||||
const results = await page.evaluate((pts) => {
|
|
||||||
return window.__nisps.iml.inferBatch(pts).map(o => Array.from(o));
|
|
||||||
}, corners);
|
|
||||||
|
|
||||||
expect(results).toHaveLength(10);
|
|
||||||
for (const arr of results) {
|
|
||||||
for (const v of arr) {
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(v).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('evalLoss', () => {
|
|
||||||
test('returns null when no examples exist', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const loss = await page.evaluate(() => window.__nisps.iml.evalLoss());
|
|
||||||
expect(loss).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns finite non-negative value when examples exist', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const loss = await page.evaluate(() => window.__nisps.iml.evalLoss());
|
|
||||||
expect(typeof loss).toBe('number');
|
|
||||||
expect(isFinite(loss)).toBe(true);
|
|
||||||
expect(loss).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not change weights', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const { before, after } = await page.evaluate(() => {
|
|
||||||
const weightsBefore = window.__nisps.getWeights();
|
|
||||||
window.__nisps.iml.evalLoss();
|
|
||||||
const weightsAfter = window.__nisps.getWeights();
|
|
||||||
return { before: Array.from(weightsBefore), after: Array.from(weightsAfter) };
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(before).toEqual(after);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('getLayerStats', () => {
|
|
||||||
test('returns 4 layers for [3, 32, 48, 64, 126] architecture', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const stats = await page.evaluate(() => window.__nisps.iml.getLayerStats());
|
|
||||||
expect(stats).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('each layer has all 4 stat fields with valid ranges', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
const stats = await page.evaluate(() => window.__nisps.iml.getLayerStats());
|
|
||||||
for (const layer of stats) {
|
|
||||||
expect(typeof layer.meanAbs).toBe('number');
|
|
||||||
expect(typeof layer.maxAbs).toBe('number');
|
|
||||||
expect(typeof layer.deadFrac).toBe('number');
|
|
||||||
expect(typeof layer.satFrac).toBe('number');
|
|
||||||
|
|
||||||
expect(layer.meanAbs).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(layer.maxAbs).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(layer.deadFrac).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(layer.deadFrac).toBeLessThanOrEqual(1);
|
|
||||||
expect(layer.satFrac).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(layer.satFrac).toBeLessThanOrEqual(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('lossHistory', () => {
|
|
||||||
test('training populates lossHistory with multiple entries', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const histLen = await page.evaluate(() => {
|
|
||||||
window.__nisps.train();
|
|
||||||
return window.__nisps.iml.lossHistory.length;
|
|
||||||
});
|
|
||||||
expect(histLen).toBeGreaterThan(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all loss history entries are finite non-negative', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
await page.evaluate(([low, high]) => {
|
|
||||||
window.__nisps.iml.addExample(low.input, low.output);
|
|
||||||
window.__nisps.iml.addExample(high.input, high.output);
|
|
||||||
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
|
|
||||||
|
|
||||||
const history = await page.evaluate(() => {
|
|
||||||
window.__nisps.train();
|
|
||||||
return [...window.__nisps.iml.lossHistory];
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(history.length).toBeGreaterThan(0);
|
|
||||||
for (const v of history) {
|
|
||||||
expect(typeof v).toBe('number');
|
|
||||||
expect(isFinite(v)).toBe(true);
|
|
||||||
expect(v).toBeGreaterThanOrEqual(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('moveWeights with pin mask', () => {
|
|
||||||
test('pinned output-layer weights unchanged, unpinned weights changed', async ({ page }) => {
|
|
||||||
await loadApp(page);
|
|
||||||
// The pin mask protects output-layer weights for pinned nodes.
|
|
||||||
// Hidden layer weights still change (shared), so we compare flat weight
|
|
||||||
// vectors and check that the output-layer segment for pinned nodes is
|
|
||||||
// identical while unpinned weights differ.
|
|
||||||
const { pinnedWeightsMatch, anyUnpinnedWeightChanged } = await page.evaluate(() => {
|
|
||||||
const iml = window.__nisps.iml;
|
|
||||||
const weightsBefore = Array.from(window.__nisps.getWeights());
|
|
||||||
|
|
||||||
// Pin first 10 outputs
|
|
||||||
const pinMask = new Uint8Array(126);
|
|
||||||
for (let i = 0; i < 10; i++) pinMask[i] = 1;
|
|
||||||
|
|
||||||
// Apply noise (spread=0 means no decay, just additive noise)
|
|
||||||
iml.moveWeights(0.3, 0, pinMask);
|
|
||||||
|
|
||||||
const weightsAfter = Array.from(window.__nisps.getWeights());
|
|
||||||
|
|
||||||
// The architecture is [3, 32, 48, 64, 126].
|
|
||||||
// Output layer: 126 nodes, each with 64+1=65 weights (64 inputs + bias).
|
|
||||||
// The output layer weights are at the end of the flat array.
|
|
||||||
const outputLayerWeights = 126 * 65; // 8190
|
|
||||||
const outputLayerStart = weightsBefore.length - outputLayerWeights;
|
|
||||||
const weightsPerNode = 65;
|
|
||||||
|
|
||||||
// Check pinned nodes (first 10) have identical weights
|
|
||||||
let pinnedAllMatch = true;
|
|
||||||
for (let n = 0; n < 10; n++) {
|
|
||||||
const nodeStart = outputLayerStart + n * weightsPerNode;
|
|
||||||
for (let w = 0; w < weightsPerNode; w++) {
|
|
||||||
if (weightsBefore[nodeStart + w] !== weightsAfter[nodeStart + w]) {
|
|
||||||
pinnedAllMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!pinnedAllMatch) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check unpinned output nodes (10-125) have at least some changed weights
|
|
||||||
let unpinnedChanged = false;
|
|
||||||
for (let n = 10; n < 126; n++) {
|
|
||||||
const nodeStart = outputLayerStart + n * weightsPerNode;
|
|
||||||
for (let w = 0; w < weightsPerNode; w++) {
|
|
||||||
if (weightsBefore[nodeStart + w] !== weightsAfter[nodeStart + w]) {
|
|
||||||
unpinnedChanged = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unpinnedChanged) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { pinnedWeightsMatch: pinnedAllMatch, anyUnpinnedWeightChanged: unpinnedChanged };
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pinnedWeightsMatch).toBe(true);
|
|
||||||
expect(anyUnpinnedWeightChanged).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue