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:
|
||||
push:
|
||||
branches: [main, port-solidjs]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main, port-solidjs]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
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:
|
||||
push:
|
||||
branches: [main, feat/manifold-mission, feat/vcv-dist]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'vcv/**'
|
||||
- '.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