230 lines
8 KiB
JavaScript
230 lines
8 KiB
JavaScript
|
|
import { describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import { createPattern, setStep, mergePatterns } from '../pattern.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper: build a 4-step pattern and configure specific steps.
|
||
|
|
* stepConfigs is an array of { index, ...stepData } objects.
|
||
|
|
*/
|
||
|
|
function buildPattern(stepConfigs) {
|
||
|
|
const p = createPattern(4);
|
||
|
|
for (const { index, ...data } of stepConfigs) {
|
||
|
|
setStep(p, index, data);
|
||
|
|
}
|
||
|
|
return p;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Additive mode
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
describe('mergePatterns — additive mode', () => {
|
||
|
|
it('both trigger: averages pitch, velocity, timeOffset; ORs accent; max subdivisions', () => {
|
||
|
|
const a = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.8, velocity: 0.6, accent: true, timeOffset: 0.1, subdivisions: 2 },
|
||
|
|
]);
|
||
|
|
const b = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.4, velocity: 1.0, accent: false, timeOffset: -0.1, subdivisions: 3 },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, true);
|
||
|
|
assert.ok(Math.abs(s.pitch - 0.6) < 1e-9, `pitch should be 0.6, got ${s.pitch}`);
|
||
|
|
assert.ok(Math.abs(s.velocity - 0.8) < 1e-9, `velocity should be 0.8, got ${s.velocity}`);
|
||
|
|
assert.equal(s.accent, true); // OR
|
||
|
|
assert.ok(Math.abs(s.timeOffset - 0.0) < 1e-9, `timeOffset should be 0.0, got ${s.timeOffset}`);
|
||
|
|
assert.equal(s.subdivisions, 3); // max
|
||
|
|
});
|
||
|
|
|
||
|
|
it('only A triggers: uses A values directly', () => {
|
||
|
|
const a = buildPattern([
|
||
|
|
{ index: 1, trigger: true, pitch: 0.9, velocity: 0.3, accent: true, timeOffset: 0.2, subdivisions: 2 },
|
||
|
|
]);
|
||
|
|
const b = buildPattern([]); // step 1 stays default (trigger: false)
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
const s = merged.steps[1];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, true);
|
||
|
|
assert.equal(s.pitch, 0.9);
|
||
|
|
assert.equal(s.velocity, 0.3);
|
||
|
|
assert.equal(s.accent, true);
|
||
|
|
assert.equal(s.timeOffset, 0.2);
|
||
|
|
assert.equal(s.subdivisions, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('only B triggers: uses B values directly', () => {
|
||
|
|
const a = buildPattern([]); // step 2 stays default (trigger: false)
|
||
|
|
const b = buildPattern([
|
||
|
|
{ index: 2, trigger: true, pitch: 0.1, velocity: 0.95, accent: false, timeOffset: -0.3, subdivisions: 4 },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
const s = merged.steps[2];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, true);
|
||
|
|
assert.equal(s.pitch, 0.1);
|
||
|
|
assert.equal(s.velocity, 0.95);
|
||
|
|
assert.equal(s.accent, false);
|
||
|
|
assert.equal(s.timeOffset, -0.3);
|
||
|
|
assert.equal(s.subdivisions, 4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('neither triggers: step stays untriggered with defaults', () => {
|
||
|
|
const a = buildPattern([]);
|
||
|
|
const b = buildPattern([]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, false);
|
||
|
|
assert.equal(s.pitch, 0.5); // DEFAULT_PITCH
|
||
|
|
assert.equal(s.velocity, 0.7); // DEFAULT_VELOCITY
|
||
|
|
assert.equal(s.accent, false); // DEFAULT_ACCENT
|
||
|
|
assert.equal(s.timeOffset, 0.0); // DEFAULT_TIME_OFFSET
|
||
|
|
assert.equal(s.subdivisions, 1); // DEFAULT_SUBDIVISIONS
|
||
|
|
});
|
||
|
|
|
||
|
|
it('timeOffset averaged when both trigger', () => {
|
||
|
|
const a = buildPattern([{ index: 0, trigger: true, timeOffset: 0.4 }]);
|
||
|
|
const b = buildPattern([{ index: 0, trigger: true, timeOffset: -0.2 }]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
assert.ok(Math.abs(merged.steps[0].timeOffset - 0.1) < 1e-9);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('subdivisions: max when both trigger, sole value when one triggers', () => {
|
||
|
|
const a = buildPattern([
|
||
|
|
{ index: 0, trigger: true, subdivisions: 1 },
|
||
|
|
{ index: 1, trigger: true, subdivisions: 3 },
|
||
|
|
]);
|
||
|
|
const b = buildPattern([
|
||
|
|
{ index: 0, trigger: true, subdivisions: 4 },
|
||
|
|
// step 1: not triggered
|
||
|
|
]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
assert.equal(merged.steps[0].subdivisions, 4); // max(1, 4)
|
||
|
|
assert.equal(merged.steps[1].subdivisions, 3); // only A triggered
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Multiplicative mode
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
describe('mergePatterns — multiplicative mode', () => {
|
||
|
|
it('both trigger: triggers, averages values, ANDs accent', () => {
|
||
|
|
const a = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.8, velocity: 0.6, accent: true, timeOffset: 0.1, subdivisions: 2 },
|
||
|
|
]);
|
||
|
|
const b = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.4, velocity: 1.0, accent: false, timeOffset: -0.1, subdivisions: 3 },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, true);
|
||
|
|
assert.ok(Math.abs(s.pitch - 0.6) < 1e-9);
|
||
|
|
assert.ok(Math.abs(s.velocity - 0.8) < 1e-9);
|
||
|
|
assert.equal(s.accent, false); // AND: true && false
|
||
|
|
assert.ok(Math.abs(s.timeOffset - 0.0) < 1e-9);
|
||
|
|
assert.equal(s.subdivisions, 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('only A triggers: no trigger in multiplicative (AND)', () => {
|
||
|
|
const a = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.9, velocity: 0.3, accent: true },
|
||
|
|
]);
|
||
|
|
const b = buildPattern([]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
// AND: true && false = false, so step not triggered
|
||
|
|
assert.equal(s.trigger, false);
|
||
|
|
// Since only A triggered (but result is untriggered due to AND),
|
||
|
|
// values come from the A-only branch
|
||
|
|
assert.equal(s.pitch, 0.9);
|
||
|
|
assert.equal(s.velocity, 0.3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('only B triggers: no trigger in multiplicative (AND)', () => {
|
||
|
|
const a = buildPattern([]);
|
||
|
|
const b = buildPattern([
|
||
|
|
{ index: 0, trigger: true, pitch: 0.1, velocity: 0.95, accent: false, timeOffset: -0.3, subdivisions: 4 },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, false);
|
||
|
|
// Values from the B-only branch
|
||
|
|
assert.equal(s.pitch, 0.1);
|
||
|
|
assert.equal(s.velocity, 0.95);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('neither triggers: untriggered with defaults', () => {
|
||
|
|
const a = buildPattern([]);
|
||
|
|
const b = buildPattern([]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
const s = merged.steps[0];
|
||
|
|
|
||
|
|
assert.equal(s.trigger, false);
|
||
|
|
assert.equal(s.pitch, 0.5);
|
||
|
|
assert.equal(s.velocity, 0.7);
|
||
|
|
assert.equal(s.accent, false);
|
||
|
|
assert.equal(s.timeOffset, 0.0);
|
||
|
|
assert.equal(s.subdivisions, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('timeOffset from sole triggering generator in multiplicative', () => {
|
||
|
|
const a = buildPattern([{ index: 0, trigger: true, timeOffset: 0.4 }]);
|
||
|
|
const b = buildPattern([]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
assert.equal(merged.steps[0].timeOffset, 0.4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('subdivisions from sole triggering generator in multiplicative', () => {
|
||
|
|
const a = buildPattern([]);
|
||
|
|
const b = buildPattern([{ index: 0, trigger: true, subdivisions: 3 }]);
|
||
|
|
|
||
|
|
const merged = mergePatterns(a, b, 'multiplicative');
|
||
|
|
assert.equal(merged.steps[0].subdivisions, 3);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Edge cases
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
describe('mergePatterns — edge cases', () => {
|
||
|
|
it('tiles mismatched step counts to LCM instead of rejecting', () => {
|
||
|
|
const a = createPattern(4);
|
||
|
|
const b = createPattern(8);
|
||
|
|
const merged = mergePatterns(a, b, 'additive');
|
||
|
|
assert.equal(merged.stepCount, 8); // lcm(4, 8) = 8
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects invalid mode', () => {
|
||
|
|
const a = createPattern(4);
|
||
|
|
const b = createPattern(4);
|
||
|
|
assert.throws(() => mergePatterns(a, b, 'xor'), TypeError);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not mutate input patterns', () => {
|
||
|
|
const a = buildPattern([{ index: 0, trigger: true, pitch: 0.2 }]);
|
||
|
|
const b = buildPattern([{ index: 0, trigger: true, pitch: 0.8 }]);
|
||
|
|
|
||
|
|
mergePatterns(a, b, 'additive');
|
||
|
|
|
||
|
|
assert.equal(a.steps[0].pitch, 0.2);
|
||
|
|
assert.equal(b.steps[0].pitch, 0.8);
|
||
|
|
});
|
||
|
|
});
|