Replace the pitch*127 encoding hack with a dedicated midiNote field on pattern steps. IntervalLock now writes integer MIDI notes to step.midiNote while preserving the original [0,1] pitch for other consumers. The clock and sequencer bridge pass midiNote through the event pipeline. Also reworks mergePatterns to be trigger-aware: when only one generator fires at a step, use its values directly instead of averaging with the other generator's defaults.
243 lines
8.1 KiB
JavaScript
243 lines
8.1 KiB
JavaScript
/**
|
||
* Tests for the midiNote field on pattern step data structures.
|
||
*
|
||
* Covers: createStep, clonePattern, setStep, validatePattern,
|
||
* mergePatterns, and IntervalLock.process() behavior.
|
||
*/
|
||
|
||
import { describe, it } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
import {
|
||
createStep, createPattern, clonePattern,
|
||
mergePatterns, setStep, validatePattern,
|
||
} from '../pattern.js';
|
||
|
||
import { IntervalLock } from '../primitives.js';
|
||
import { createPRNG } from '../prng.js';
|
||
|
||
// ── createStep ──────────────────────────────────────────────────────
|
||
|
||
describe('createStep – midiNote', () => {
|
||
it('has midiNote: null by default', () => {
|
||
const step = createStep();
|
||
assert.strictEqual(step.midiNote, null);
|
||
});
|
||
});
|
||
|
||
// ── clonePattern ────────────────────────────────────────────────────
|
||
|
||
describe('clonePattern – midiNote', () => {
|
||
it('preserves midiNote across clone', () => {
|
||
const p = createPattern(2);
|
||
p.steps[0].midiNote = 60;
|
||
p.steps[1].midiNote = null;
|
||
|
||
const c = clonePattern(p);
|
||
assert.strictEqual(c.steps[0].midiNote, 60);
|
||
assert.strictEqual(c.steps[1].midiNote, null);
|
||
});
|
||
|
||
it('clone is independent (mutation does not propagate)', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 72;
|
||
const c = clonePattern(p);
|
||
c.steps[0].midiNote = 48;
|
||
assert.strictEqual(p.steps[0].midiNote, 72);
|
||
});
|
||
});
|
||
|
||
// ── setStep ─────────────────────────────────────────────────────────
|
||
|
||
describe('setStep – midiNote', () => {
|
||
it('can set midiNote to an integer', () => {
|
||
const p = createPattern(1);
|
||
setStep(p, 0, { midiNote: 64 });
|
||
assert.strictEqual(p.steps[0].midiNote, 64);
|
||
});
|
||
|
||
it('can set midiNote to null', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 60;
|
||
setStep(p, 0, { midiNote: null });
|
||
assert.strictEqual(p.steps[0].midiNote, null);
|
||
});
|
||
|
||
it('coerces midiNote to integer', () => {
|
||
const p = createPattern(1);
|
||
setStep(p, 0, { midiNote: 60.9 });
|
||
assert.strictEqual(p.steps[0].midiNote, 60);
|
||
});
|
||
|
||
it('does not touch midiNote when not in stepData', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 72;
|
||
setStep(p, 0, { pitch: 0.3 });
|
||
assert.strictEqual(p.steps[0].midiNote, 72);
|
||
});
|
||
});
|
||
|
||
// ── validatePattern ─────────────────────────────────────────────────
|
||
|
||
describe('validatePattern – midiNote', () => {
|
||
it('accepts midiNote: null', () => {
|
||
const p = createPattern(1);
|
||
assert.strictEqual(validatePattern(p), true);
|
||
});
|
||
|
||
it('accepts midiNote: 0', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 0;
|
||
assert.strictEqual(validatePattern(p), true);
|
||
});
|
||
|
||
it('accepts midiNote: 127', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 127;
|
||
assert.strictEqual(validatePattern(p), true);
|
||
});
|
||
|
||
it('accepts midiNote: 60 (middle C)', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 60;
|
||
assert.strictEqual(validatePattern(p), true);
|
||
});
|
||
|
||
it('rejects midiNote: float (60.5)', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 60.5;
|
||
assert.strictEqual(validatePattern(p), false);
|
||
});
|
||
|
||
it('rejects midiNote: -1', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = -1;
|
||
assert.strictEqual(validatePattern(p), false);
|
||
});
|
||
|
||
it('rejects midiNote: 128', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 128;
|
||
assert.strictEqual(validatePattern(p), false);
|
||
});
|
||
|
||
it('rejects midiNote: string', () => {
|
||
const p = createPattern(1);
|
||
p.steps[0].midiNote = 'C4';
|
||
assert.strictEqual(validatePattern(p), false);
|
||
});
|
||
});
|
||
|
||
// ── mergePatterns ───────────────────────────────────────────────────
|
||
|
||
describe('mergePatterns – midiNote', () => {
|
||
it('both triggered, both have midiNote → average rounded', () => {
|
||
const a = createPattern(1);
|
||
setStep(a, 0, { trigger: true, midiNote: 60 });
|
||
const b = createPattern(1);
|
||
setStep(b, 0, { trigger: true, midiNote: 65 });
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
// (60 + 65) / 2 = 62.5 → 63
|
||
assert.strictEqual(m.steps[0].midiNote, 63);
|
||
});
|
||
|
||
it('both triggered, only A has midiNote → use A', () => {
|
||
const a = createPattern(1);
|
||
setStep(a, 0, { trigger: true, midiNote: 72 });
|
||
const b = createPattern(1);
|
||
setStep(b, 0, { trigger: true });
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, 72);
|
||
});
|
||
|
||
it('both triggered, only B has midiNote → use B', () => {
|
||
const a = createPattern(1);
|
||
setStep(a, 0, { trigger: true });
|
||
const b = createPattern(1);
|
||
setStep(b, 0, { trigger: true, midiNote: 48 });
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, 48);
|
||
});
|
||
|
||
it('both triggered, neither has midiNote → null', () => {
|
||
const a = createPattern(1);
|
||
setStep(a, 0, { trigger: true });
|
||
const b = createPattern(1);
|
||
setStep(b, 0, { trigger: true });
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, null);
|
||
});
|
||
|
||
it('only A triggered → uses A midiNote', () => {
|
||
const a = createPattern(1);
|
||
setStep(a, 0, { trigger: true, midiNote: 55 });
|
||
const b = createPattern(1);
|
||
// b not triggered (default)
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, 55);
|
||
});
|
||
|
||
it('only B triggered → uses B midiNote', () => {
|
||
const a = createPattern(1);
|
||
// a not triggered
|
||
const b = createPattern(1);
|
||
setStep(b, 0, { trigger: true, midiNote: 80 });
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, 80);
|
||
});
|
||
|
||
it('neither triggered → null', () => {
|
||
const a = createPattern(1);
|
||
const b = createPattern(1);
|
||
|
||
const m = mergePatterns(a, b, 'additive');
|
||
assert.strictEqual(m.steps[0].midiNote, null);
|
||
});
|
||
});
|
||
|
||
// ── IntervalLock.process() ──────────────────────────────────────────
|
||
|
||
describe('IntervalLock – midiNote field', () => {
|
||
it('sets midiNote as integer and leaves pitch as [0,1]', () => {
|
||
const lock = new IntervalLock();
|
||
const rng = createPRNG(42);
|
||
|
||
// Create a pattern with a triggered step at pitch 0.5
|
||
const input = createPattern(1);
|
||
setStep(input, 0, { trigger: true, pitch: 0.5 });
|
||
|
||
// root=0 (C), mode=0.1 (major), octaveRange=0.25 (1 octave)
|
||
const result = lock.process([0.0, 0.1, 0.25], input, {}, rng);
|
||
const step = result.patternDesc.steps[0];
|
||
|
||
// midiNote should be an integer (a MIDI note from the C major scale)
|
||
assert.strictEqual(typeof step.midiNote, 'number');
|
||
assert.strictEqual(step.midiNote, step.midiNote | 0, 'midiNote should be integer');
|
||
assert.ok(step.midiNote >= 0 && step.midiNote <= 127, 'midiNote in MIDI range');
|
||
|
||
// pitch should remain a [0,1] float (the original pre-quantization value)
|
||
assert.strictEqual(step.pitch, 0.5, 'pitch should be unchanged from input');
|
||
});
|
||
|
||
it('produces different midiNote values for different pitch inputs', () => {
|
||
const lock = new IntervalLock();
|
||
const rng = createPRNG(42);
|
||
|
||
const input = createPattern(2);
|
||
setStep(input, 0, { trigger: true, pitch: 0.0 });
|
||
setStep(input, 1, { trigger: true, pitch: 1.0 });
|
||
|
||
const result = lock.process([0.0, 0.1, 0.5], input, {}, rng);
|
||
const note0 = result.patternDesc.steps[0].midiNote;
|
||
const note1 = result.patternDesc.steps[1].midiNote;
|
||
|
||
assert.notStrictEqual(note0, note1, 'different pitches should produce different MIDI notes');
|
||
assert.ok(note0 < note1, 'higher pitch should produce higher MIDI note');
|
||
});
|
||
});
|