218 lines
7.5 KiB
JavaScript
218 lines
7.5 KiB
JavaScript
|
|
import { describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import { FreezeManager } from '../freeze.js';
|
||
|
|
import { Chain } from '../chain.js';
|
||
|
|
import { EuclideanRhythm, ProbabilityGate, PitchWalker } from '../primitives.js';
|
||
|
|
import { createPattern, clonePattern } from '../pattern.js';
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Helpers
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function makeTestChain() {
|
||
|
|
const chain = new Chain();
|
||
|
|
chain.addPrimitive(new EuclideanRhythm());
|
||
|
|
chain.addPrimitive(new ProbabilityGate());
|
||
|
|
chain.addPrimitive(new PitchWalker());
|
||
|
|
return { chain, paramCount: chain.totalParamCount };
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeParams(count) {
|
||
|
|
const params = new Float32Array(count);
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
params[i] = (i + 1) / (count + 1);
|
||
|
|
}
|
||
|
|
return params;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a pattern with some non-default step data for testing.
|
||
|
|
*/
|
||
|
|
function makeTestPattern(stepCount) {
|
||
|
|
const pattern = createPattern(stepCount);
|
||
|
|
for (let i = 0; i < stepCount; i++) {
|
||
|
|
pattern.steps[i].trigger = i % 2 === 0;
|
||
|
|
pattern.steps[i].pitch = (i + 1) / (stepCount + 1);
|
||
|
|
pattern.steps[i].velocity = 0.5 + (i * 0.05);
|
||
|
|
pattern.steps[i].accent = i === 0;
|
||
|
|
pattern.steps[i].midiNote = 60 + i;
|
||
|
|
}
|
||
|
|
pattern.metadata.source = 'test';
|
||
|
|
return pattern;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// freeze with mode='pattern' captures the pattern
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
describe('FreezeManager freeze-as-pattern', () => {
|
||
|
|
it('freeze with mode=pattern captures the pattern', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
assert.strictEqual(fm.isFrozen, true);
|
||
|
|
const frozen = fm.getFrozenPattern();
|
||
|
|
assert.ok(frozen !== null, 'frozen pattern should not be null');
|
||
|
|
assert.strictEqual(frozen.stepCount, 8);
|
||
|
|
assert.strictEqual(frozen.steps.length, 8);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getFrozenPattern returns the captured pattern', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(4);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
const frozen = fm.getFrozenPattern();
|
||
|
|
// Verify step data matches
|
||
|
|
for (let i = 0; i < 4; i++) {
|
||
|
|
assert.strictEqual(frozen.steps[i].trigger, pattern.steps[i].trigger,
|
||
|
|
'step ' + i + ' trigger should match');
|
||
|
|
assert.strictEqual(frozen.steps[i].pitch, pattern.steps[i].pitch,
|
||
|
|
'step ' + i + ' pitch should match');
|
||
|
|
assert.strictEqual(frozen.steps[i].velocity, pattern.steps[i].velocity,
|
||
|
|
'step ' + i + ' velocity should match');
|
||
|
|
assert.strictEqual(frozen.steps[i].midiNote, pattern.steps[i].midiNote,
|
||
|
|
'step ' + i + ' midiNote should match');
|
||
|
|
}
|
||
|
|
assert.strictEqual(frozen.metadata.source, 'test');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('freezeMode is pattern', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
assert.strictEqual(fm.freezeMode, 'pattern');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getEffectiveParams returns null in pattern mode', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
const result = fm.getEffectiveParams(new Float32Array(8));
|
||
|
|
assert.strictEqual(result, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shouldSuppressReEval returns true in pattern mode', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
assert.strictEqual(fm.shouldSuppressReEval(), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('unfreeze clears frozen pattern', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
assert.strictEqual(fm.isFrozen, true);
|
||
|
|
assert.ok(fm.getFrozenPattern() !== null);
|
||
|
|
|
||
|
|
fm.unfreeze();
|
||
|
|
|
||
|
|
assert.strictEqual(fm.isFrozen, false);
|
||
|
|
assert.strictEqual(fm.freezeMode, null);
|
||
|
|
assert.strictEqual(fm.getFrozenPattern(), null);
|
||
|
|
assert.strictEqual(fm.getFrozenParams(), null);
|
||
|
|
assert.strictEqual(fm.getFrozenSeeds(), null);
|
||
|
|
assert.strictEqual(fm.getFrozenStates(), null);
|
||
|
|
assert.strictEqual(fm.getMasterSeed(), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('freeze with mode=algorithm still works (backward compat)', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const { chain, paramCount } = makeTestChain();
|
||
|
|
const params = makeParams(paramCount);
|
||
|
|
|
||
|
|
fm.freeze(chain, params, 42, 'algorithm');
|
||
|
|
|
||
|
|
assert.strictEqual(fm.isFrozen, true);
|
||
|
|
assert.strictEqual(fm.freezeMode, 'algorithm');
|
||
|
|
assert.ok(fm.getFrozenParams() instanceof Float32Array);
|
||
|
|
assert.strictEqual(fm.getFrozenParams().length, paramCount);
|
||
|
|
assert.ok(Array.isArray(fm.getFrozenSeeds()));
|
||
|
|
assert.ok(Array.isArray(fm.getFrozenStates()));
|
||
|
|
assert.strictEqual(fm.getMasterSeed(), 42);
|
||
|
|
assert.strictEqual(fm.getFrozenPattern(), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('freeze with default mode is algorithm (backward compat)', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const { chain, paramCount } = makeTestChain();
|
||
|
|
const params = makeParams(paramCount);
|
||
|
|
|
||
|
|
// Call without mode argument — should default to 'algorithm'
|
||
|
|
fm.freeze(chain, params, 42);
|
||
|
|
|
||
|
|
assert.strictEqual(fm.freezeMode, 'algorithm');
|
||
|
|
assert.ok(fm.getFrozenParams() instanceof Float32Array);
|
||
|
|
assert.strictEqual(fm.getFrozenPattern(), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('frozen pattern is a deep clone (mutating original does not affect frozen)', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(4);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
// Mutate the original
|
||
|
|
pattern.steps[0].trigger = !pattern.steps[0].trigger;
|
||
|
|
pattern.steps[0].pitch = 0.999;
|
||
|
|
pattern.steps[0].midiNote = 127;
|
||
|
|
pattern.metadata.source = 'mutated';
|
||
|
|
|
||
|
|
// Frozen should be unaffected
|
||
|
|
const frozen = fm.getFrozenPattern();
|
||
|
|
assert.strictEqual(frozen.steps[0].trigger, true, 'frozen trigger should be unchanged');
|
||
|
|
assert.notStrictEqual(frozen.steps[0].pitch, 0.999, 'frozen pitch should be unchanged');
|
||
|
|
assert.strictEqual(frozen.steps[0].midiNote, 60, 'frozen midiNote should be unchanged');
|
||
|
|
assert.strictEqual(frozen.metadata.source, 'test', 'frozen metadata should be unchanged');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('pattern mode throws if currentPattern is null', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
assert.throws(
|
||
|
|
() => fm.freeze(null, null, null, 'pattern', null),
|
||
|
|
/pattern mode requires a currentPattern/
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('freezeMode is null when not frozen', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
assert.strictEqual(fm.freezeMode, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getFrozenPattern returns null when not frozen', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
assert.strictEqual(fm.getFrozenPattern(), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getFrozenPattern returns null in algorithm mode', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const { chain, paramCount } = makeTestChain();
|
||
|
|
fm.freeze(chain, makeParams(paramCount), 42, 'algorithm');
|
||
|
|
assert.strictEqual(fm.getFrozenPattern(), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('algorithm-mode state is null in pattern mode', () => {
|
||
|
|
const fm = new FreezeManager();
|
||
|
|
const pattern = makeTestPattern(8);
|
||
|
|
|
||
|
|
fm.freeze(null, null, null, 'pattern', pattern);
|
||
|
|
|
||
|
|
assert.strictEqual(fm.getFrozenParams(), null);
|
||
|
|
assert.strictEqual(fm.getFrozenSeeds(), null);
|
||
|
|
assert.strictEqual(fm.getFrozenStates(), null);
|
||
|
|
assert.strictEqual(fm.getLiveFlags(), null);
|
||
|
|
assert.strictEqual(fm.getMasterSeed(), null);
|
||
|
|
});
|
||
|
|
});
|