Files
KGStudio/src/util/midiAutomationUtil.test.ts
T

128 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
bakeMidiAutomationPointsInWindow,
collectRegionMidiAutomationPoints,
normalizeMidiAutomationPoints,
resolveMidiAutomationValueAtBeat,
type MidiAutomationPoint,
} from './midiAutomationUtil';
const defaultOptions = {
maxIntervalMs: 10,
bpm: 120,
defaultValue: 8192,
};
describe('midiAutomationUtil', () => {
it('normalizes points and keeps the last duplicate beat', () => {
const points: MidiAutomationPoint[] = [
{ beat: 2, value: 1000 },
{ beat: 1, value: 2000 },
{ beat: 2, value: 3000 },
];
expect(normalizeMidiAutomationPoints(points)).toEqual([
{ beat: 1, value: 2000 },
{ beat: 2, value: 3000 },
]);
});
it('holds the default value before the first point', () => {
const points = [{ beat: 4, value: 0 }];
expect(resolveMidiAutomationValueAtBeat(points, 2, 8192)).toBe(8192);
});
it('interpolates linearly between two points', () => {
const points = [
{ beat: 0, value: 8192 },
{ beat: 4, value: 0 },
];
expect(resolveMidiAutomationValueAtBeat(points, 2, 8192)).toBe(4096);
});
it('holds the last value after the final point', () => {
const points = [{ beat: 1, value: 2048 }];
expect(resolveMidiAutomationValueAtBeat(points, 8, 8192)).toBe(2048);
});
it('collects region-local points into absolute beats', () => {
const collected = collectRegionMidiAutomationPoints([
{ startBeat: 4, points: [{ beat: 0.5, value: 7000 }] },
{ startBeat: 1, points: [{ beat: 0.25, value: 6000 }] },
]);
expect(collected).toEqual([
{ beat: 1.25, value: 6000 },
{ beat: 4.5, value: 7000 },
]);
});
it('bakes dense points according to the requested ms interval', () => {
const points = [
{ beat: 0, value: 8192 },
{ beat: 1, value: 0 },
];
expect(bakeMidiAutomationPointsInWindow(points, 0, 1, { ...defaultOptions, maxIntervalMs: 20 })).toHaveLength(25);
expect(bakeMidiAutomationPointsInWindow(points, 0, 1, { ...defaultOptions, maxIntervalMs: 10 })).toHaveLength(50);
expect(bakeMidiAutomationPointsInWindow(points, 0, 1, { ...defaultOptions, maxIntervalMs: 5 })).toHaveLength(100);
});
it('treats flat segments as holds without adding interior baked points', () => {
const points = [
{ beat: 0, value: 4096 },
{ beat: 4, value: 4096 },
];
expect(bakeMidiAutomationPointsInWindow(points, 0, 4, { ...defaultOptions, maxIntervalMs: 10 })).toEqual([
{ beat: 0, value: 4096 },
]);
});
it('collapses consecutive baked points with the same value', () => {
const points = [
{ beat: 0.5, value: 8192 },
{ beat: 1, value: 4096 },
{ beat: 2, value: 4096 },
{ beat: 3, value: 0 },
];
expect(bakeMidiAutomationPointsInWindow(points, 0, 4, { ...defaultOptions, maxIntervalMs: 500 })).toEqual([
{ beat: 0, value: 8192 },
{ beat: 1, value: 4096 },
{ beat: 3, value: 0 },
]);
});
it('keeps later changing segments after skipping a flat segment', () => {
const points = [
{ beat: 0, value: 4096 },
{ beat: 2, value: 4096 },
{ beat: 4, value: 0 },
];
const baked = bakeMidiAutomationPointsInWindow(points, 0, 4, { ...defaultOptions, maxIntervalMs: 500 });
expect(baked).toEqual([
{ beat: 0, value: 4096 },
{ beat: 3, value: 2048 },
]);
});
it('adds a window anchor and preserves the correct loop boundary value', () => {
const points = [
{ beat: 2, value: 0 },
{ beat: 6, value: 8192 },
];
const baked = bakeMidiAutomationPointsInWindow(points, 4, 8, { ...defaultOptions, maxIntervalMs: 500 });
expect(baked[0]).toEqual({ beat: 4, value: 4096 });
expect(baked.some(point => point.beat === 5 && point.value === 6144)).toBe(true);
expect(baked.some(point => point.beat === 6 && point.value === 8192)).toBe(true);
});
});