memlnaut-nisps/playground/js/shapeseq/tests/polyrhythm-chain.test.js

211 lines
7.9 KiB
JavaScript
Raw Normal View History

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { Chain } from '../chain.js';
import { EuclideanRhythm, DensityMorph, VelocityShaper } from '../primitives.js';
import { lcm } from '../pattern.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Build a chain with two EuclideanRhythm generators.
* Returns { chain, gen0Index, gen1Index }.
*/
function twoGeneratorChain() {
const chain = new Chain();
chain.addPrimitive(new EuclideanRhythm()); // index 0
chain.addPrimitive(new EuclideanRhythm()); // index 1
return { chain, gen0Index: 0, gen1Index: 1 };
}
/**
* Count triggered steps in a pattern.
*/
function countTriggers(pattern) {
let count = 0;
for (let i = 0; i < pattern.stepCount; i++) {
if (pattern.steps[i].trigger) count++;
}
return count;
}
// ---------------------------------------------------------------------------
// Default behavior: all generators use global stepCount (backward compat)
// ---------------------------------------------------------------------------
describe('polyrhythm chain — backward compatibility', () => {
it('without per-generator step counts, all generators use global stepCount', () => {
const { chain } = twoGeneratorChain();
// Both generators: steps=1.0 (max), pulses=0.5, rotation=0
// With stepCount=8, steps param maps to 8, pulses maps to ~4
const params = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0]);
const pattern = chain.evaluate(params, 8, 42);
// Both use the same stepCount, so merged pattern should be 8 steps
assert.equal(pattern.stepCount, 8);
});
it('clearGeneratorStepCounts resets to default behavior', () => {
const { chain, gen0Index } = twoGeneratorChain();
// Set a custom step count
chain.setGeneratorStepCount(gen0Index, 3);
assert.equal(chain.getGeneratorStepCount(gen0Index), 3);
// Clear
chain.clearGeneratorStepCounts();
assert.equal(chain.getGeneratorStepCount(gen0Index), null);
// Evaluate — should use global stepCount for both
const params = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0]);
const pattern = chain.evaluate(params, 8, 42);
assert.equal(pattern.stepCount, 8);
});
});
// ---------------------------------------------------------------------------
// Per-generator step counts
// ---------------------------------------------------------------------------
describe('polyrhythm chain — per-generator step counts', () => {
it('two generators with different step counts produce LCM-length pattern', () => {
const { chain, gen0Index, gen1Index } = twoGeneratorChain();
chain.setGeneratorStepCount(gen0Index, 3);
chain.setGeneratorStepCount(gen1Index, 4);
// All pulses high so we get triggers
const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]);
const pattern = chain.evaluate(params, 8, 42);
// LCM(3, 4) = 12
assert.equal(pattern.stepCount, lcm(3, 4));
assert.equal(pattern.stepCount, 12);
});
it('triggers from both generators are present in merged output', () => {
const { chain, gen0Index, gen1Index } = twoGeneratorChain();
// Gen0: 3 steps with all triggers (steps=1.0 maps to max=3, pulses=1.0 fills all)
chain.setGeneratorStepCount(gen0Index, 3);
// Gen1: 4 steps with all triggers
chain.setGeneratorStepCount(gen1Index, 4);
const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]);
const pattern = chain.evaluate(params, 8, 42);
assert.equal(pattern.stepCount, 12);
// With all pulses, both generators trigger every step in their respective
// cycle lengths. Tiled to 12:
// Gen0 (3-step cycle, all on): every step has a trigger
// Gen1 (4-step cycle, all on): every step has a trigger
// Additive merge: every step should be triggered
for (let i = 0; i < 12; i++) {
assert.equal(pattern.steps[i].trigger, true, `step ${i} should be triggered`);
}
});
it('only one generator with custom step count, other uses global', () => {
const { chain, gen0Index } = twoGeneratorChain();
// Gen0 gets 3 steps, Gen1 uses global (4)
chain.setGeneratorStepCount(gen0Index, 3);
const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]);
const pattern = chain.evaluate(params, 4, 42);
// LCM(3, 4) = 12
assert.equal(pattern.stepCount, 12);
});
it('generatorStepCounts passed as evaluate() argument overrides instance map', () => {
const { chain, gen0Index, gen1Index } = twoGeneratorChain();
// Instance map says 3 and 4
chain.setGeneratorStepCount(gen0Index, 3);
chain.setGeneratorStepCount(gen1Index, 4);
// But explicit arg says 5 and 7
const explicitMap = new Map([[gen0Index, 5], [gen1Index, 7]]);
const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]);
const pattern = chain.evaluate(params, 8, 42, explicitMap);
// LCM(5, 7) = 35
assert.equal(pattern.stepCount, lcm(5, 7));
assert.equal(pattern.stepCount, 35);
});
it('getGeneratorStepCount returns null for unset generators', () => {
const { chain, gen1Index } = twoGeneratorChain();
assert.equal(chain.getGeneratorStepCount(gen1Index), null);
});
it('setGeneratorStepCount validates chain index', () => {
const { chain } = twoGeneratorChain();
assert.throws(() => chain.setGeneratorStepCount(99, 4), { name: 'RangeError' });
assert.throws(() => chain.setGeneratorStepCount(-1, 4), { name: 'RangeError' });
});
it('setGeneratorStepCount validates step count', () => {
const { chain } = twoGeneratorChain();
assert.throws(() => chain.setGeneratorStepCount(0, 0), { name: 'RangeError' });
assert.throws(() => chain.setGeneratorStepCount(0, -1), { name: 'RangeError' });
});
});
// ---------------------------------------------------------------------------
// Processors run on LCM-length pattern
// ---------------------------------------------------------------------------
describe('polyrhythm chain — processors on LCM-length pattern', () => {
it('processor receives and outputs the LCM-length pattern', () => {
const chain = new Chain();
chain.addPrimitive(new EuclideanRhythm()); // index 0 — generator
chain.addPrimitive(new EuclideanRhythm()); // index 1 — generator
chain.addPrimitive(new VelocityShaper()); // index 2 — processor
chain.setGeneratorStepCount(0, 3);
chain.setGeneratorStepCount(1, 4);
// EuclideanRhythm: 3 params each, VelocityShaper: 3 params
// Fill all with moderate values
const params = new Float32Array([
1.0, 1.0, 0.0, // gen0: all triggers
1.0, 1.0, 0.0, // gen1: all triggers
0.0, 0.5, 0.0, // velocity shaper: flat curve, half depth, no phase
]);
const pattern = chain.evaluate(params, 8, 42);
// LCM(3, 4) = 12 — processor should preserve that length
assert.equal(pattern.stepCount, 12);
// Velocity shaper should have modified velocities (not all default 0.7)
// With flat curve (idx 0) and depth 0.5, velocity = 0.7 * 0.5 + 1.0 * 0.5 = 0.85
const triggeredSteps = pattern.steps.filter(s => s.trigger);
assert.ok(triggeredSteps.length > 0, 'should have triggered steps');
});
});
// ---------------------------------------------------------------------------
// Single generator with per-generator step count
// ---------------------------------------------------------------------------
describe('polyrhythm chain — single generator', () => {
it('single generator uses its per-generator step count', () => {
const chain = new Chain();
chain.addPrimitive(new EuclideanRhythm()); // index 0
chain.setGeneratorStepCount(0, 5);
const params = new Float32Array([1.0, 1.0, 0.0]);
const pattern = chain.evaluate(params, 8, 42);
// Single generator with 5 steps — no merge, output is 5 steps
assert.equal(pattern.stepCount, 5);
});
});