184 lines
7 KiB
JavaScript
184 lines
7 KiB
JavaScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|