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); }); });