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

184 lines
7 KiB
JavaScript
Raw Normal View History

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import {
createPattern, setStep, clonePattern, mergePatterns,
lcm, tilePattern, lcmOfPatterns,
} from '../pattern.js';
// ---------------------------------------------------------------------------
// lcm
// ---------------------------------------------------------------------------
describe('lcm', () => {
it('lcm(3, 4) = 12', () => assert.equal(lcm(3, 4), 12));
it('lcm(5, 8) = 40', () => assert.equal(lcm(5, 8), 40));
it('lcm(6, 6) = 6', () => assert.equal(lcm(6, 6), 6));
it('lcm(1, 7) = 7', () => assert.equal(lcm(1, 7), 7));
it('lcm(7, 1) = 7', () => assert.equal(lcm(7, 1), 7));
});
// ---------------------------------------------------------------------------
// tilePattern
// ---------------------------------------------------------------------------
describe('tilePattern', () => {
/** Build a 3-step pattern with distinct triggers on each step. */
function make3StepPattern() {
const p = createPattern(3);
setStep(p, 0, { trigger: true, pitch: 0.1, velocity: 0.2, accent: true, timeOffset: 0.05, subdivisions: 2, midiNote: 60 });
setStep(p, 1, { trigger: false, pitch: 0.3, velocity: 0.4, accent: false, timeOffset: -0.1, subdivisions: 1, midiNote: null });
setStep(p, 2, { trigger: true, pitch: 0.9, velocity: 0.8, accent: true, timeOffset: 0.0, subdivisions: 3, midiNote: 72 });
return p;
}
it('tiles a 3-step pattern to 12 steps by wrapping', () => {
const src = make3StepPattern();
const tiled = tilePattern(src, 12);
assert.equal(tiled.stepCount, 12);
assert.equal(tiled.steps.length, 12);
for (let i = 0; i < 12; i++) {
const srcStep = src.steps[i % 3];
const tiledStep = tiled.steps[i];
assert.equal(tiledStep.trigger, srcStep.trigger, `step ${i} trigger`);
assert.equal(tiledStep.pitch, srcStep.pitch, `step ${i} pitch`);
assert.equal(tiledStep.velocity, srcStep.velocity, `step ${i} velocity`);
assert.equal(tiledStep.accent, srcStep.accent, `step ${i} accent`);
assert.equal(tiledStep.timeOffset, srcStep.timeOffset, `step ${i} timeOffset`);
assert.equal(tiledStep.subdivisions, srcStep.subdivisions, `step ${i} subdivisions`);
assert.equal(tiledStep.midiNote, srcStep.midiNote, `step ${i} midiNote`);
}
});
it('same length returns a clone (no-op)', () => {
const src = make3StepPattern();
const tiled = tilePattern(src, 3);
assert.equal(tiled.stepCount, 3);
// Must be a separate object (clone), not the same reference
assert.notEqual(tiled, src);
assert.notEqual(tiled.steps, src.steps);
assert.notEqual(tiled.steps[0], src.steps[0]);
// But values match
assert.equal(tiled.steps[0].pitch, src.steps[0].pitch);
});
it('preserves all step fields (trigger, pitch, velocity, accent, timeOffset, subdivisions, midiNote)', () => {
const src = make3StepPattern();
const tiled = tilePattern(src, 6);
const step0 = tiled.steps[0];
assert.equal(step0.trigger, true);
assert.equal(step0.pitch, 0.1);
assert.equal(step0.velocity, 0.2);
assert.equal(step0.accent, true);
assert.equal(step0.timeOffset, 0.05);
assert.equal(step0.subdivisions, 2);
assert.equal(step0.midiNote, 60);
// Step 4 should be a copy of step 1 (4 % 3 = 1)
const step4 = tiled.steps[4];
assert.equal(step4.trigger, false);
assert.equal(step4.pitch, 0.3);
assert.equal(step4.velocity, 0.4);
assert.equal(step4.midiNote, null);
});
it('throws if targetStepCount < pattern.stepCount', () => {
const src = make3StepPattern();
assert.throws(
() => tilePattern(src, 2),
{ name: 'RangeError' }
);
});
});
// ---------------------------------------------------------------------------
// mergePatterns — backward compatibility (same step counts)
// ---------------------------------------------------------------------------
describe('mergePatterns — same step count (backward compat)', () => {
it('merges two 4-step patterns additively', () => {
const a = createPattern(4);
setStep(a, 0, { trigger: true, pitch: 0.8, velocity: 0.6 });
setStep(a, 2, { trigger: true, pitch: 0.3, velocity: 0.9 });
const b = createPattern(4);
setStep(b, 1, { trigger: true, pitch: 0.5, velocity: 0.7 });
setStep(b, 2, { trigger: true, pitch: 0.7, velocity: 0.5 });
const merged = mergePatterns(a, b, 'additive');
assert.equal(merged.stepCount, 4);
assert.equal(merged.steps[0].trigger, true); // A only
assert.equal(merged.steps[1].trigger, true); // B only
assert.equal(merged.steps[2].trigger, true); // both
assert.equal(merged.steps[3].trigger, false); // neither
});
});
// ---------------------------------------------------------------------------
// mergePatterns — different step counts (polyrhythm)
// ---------------------------------------------------------------------------
describe('mergePatterns — different step counts (polyrhythm)', () => {
it('3+4 step patterns produce LCM=12 length result', () => {
const a = createPattern(3);
setStep(a, 0, { trigger: true, pitch: 0.2, velocity: 0.8 });
// Steps 1, 2 are default (trigger: false)
const b = createPattern(4);
setStep(b, 0, { trigger: true, pitch: 0.6, velocity: 0.4 });
// Steps 1, 2, 3 are default (trigger: false)
const merged = mergePatterns(a, b, 'additive');
assert.equal(merged.stepCount, 12);
assert.equal(merged.steps.length, 12);
});
it('tiled pattern has correct trigger patterns from both inputs', () => {
// A: 3 steps, triggers on 0 and 2 → tiled to 12: triggers at 0,2,3,5,6,8,9,11
const a = createPattern(3);
setStep(a, 0, { trigger: true });
setStep(a, 2, { trigger: true });
// B: 4 steps, trigger on 1 → tiled to 12: triggers at 1,5,9
const b = createPattern(4);
setStep(b, 1, { trigger: true });
const merged = mergePatterns(a, b, 'additive');
assert.equal(merged.stepCount, 12);
// A triggers at positions where i%3 is 0 or 2: 0,2,3,5,6,8,9,11
// B triggers at positions where i%4 is 1: 1,5,9
// Combined (additive = OR): 0,1,2,3,5,6,8,9,11
const expectedTriggers = [true, true, true, true, false, true, true, false, true, true, false, true];
for (let i = 0; i < 12; i++) {
assert.equal(merged.steps[i].trigger, expectedTriggers[i], `step ${i} trigger`);
}
});
});
// ---------------------------------------------------------------------------
// lcmOfPatterns
// ---------------------------------------------------------------------------
describe('lcmOfPatterns', () => {
it('single pattern returns its stepCount', () => {
const p = createPattern(5);
assert.equal(lcmOfPatterns([p]), 5);
});
it('multiple patterns returns LCM of all step counts', () => {
const a = createPattern(3);
const b = createPattern(4);
const c = createPattern(6);
// lcm(3,4) = 12, lcm(12,6) = 12
assert.equal(lcmOfPatterns([a, b, c]), 12);
});
it('empty array returns 1', () => {
assert.equal(lcmOfPatterns([]), 1);
});
});