import assert from 'node:assert/strict'; import test from 'node:test'; import { collectScenesAndPatterns, parsePatternEvents } from '../src/lib/parsers'; import { TarFile } from '../src/lib/untar'; function buildPattern(records: number[][], bars: number = 4, declaredCount = records.length) { const data = new Uint8Array(4 + records.length * 8); data[1] = bars; data[2] = declaredCount & 0xff; data[3] = declaredCount >> 8; records.forEach((record, index) => { data.set(record, 4 + index * 8); }); return data; } test('parses note records without changing the derived note shape', () => { const result = parsePatternEvents(buildPattern([[0x34, 0x12, 3 << 3, 64, 99, 0x78, 0x56, 7]])); assert.equal(result.bars, 4); assert.equal(result.recordCount, 1); assert.deepEqual(result.notes, { 3: [{ note: 64, position: 0x1234, duration: 0x5678, velocity: 99 }], }); assert.deepEqual(result.events[0], { type: 'note', kind: 0, position: 0x1234, source: 3, flags: 7, rawData: new Uint8Array([0x34, 0x12, 3 << 3, 64, 99, 0x78, 0x56, 7]), pad: 3, note: 64, velocity: 99, duration: 0x5678, }); }); test('parses and normalizes fader records', () => { const result = parsePatternEvents(buildPattern([[0x60, 0, 1, 7, 9, 0xff, 0x7f, 128]])); assert.deepEqual(result.notes, {}); assert.deepEqual(result.events[0], { type: 'fader', kind: 1, position: 96, source: 0, flags: 128, rawData: new Uint8Array([0x60, 0, 1, 7, 9, 0xff, 0x7f, 128]), parameter: 7, reserved: 9, rawValue: 32767, value: 1, }); }); test('preserves MIDI and unknown records', () => { const midi = [0x27, 0, (2 << 3) | 2, 0xd0, 0x62, 0xff, 0, 0x3d]; const unknown = [2, 0, (5 << 3) | 6, 10, 11, 12, 13, 14]; const result = parsePatternEvents(buildPattern([midi, unknown])); assert.deepEqual(result.events[0], { type: 'midi', kind: 2, position: 0x27, source: 2, flags: 0x3d, rawData: new Uint8Array(midi), data: new Uint8Array([0xd0, 0x62, 0xff, 0]), }); assert.deepEqual(result.events[1], { type: 'unknown', kind: 6, position: 2, source: 5, flags: 14, rawData: new Uint8Array(unknown), data: new Uint8Array([10, 11, 12, 13]), }); }); test('retains MIDI-only sources as empty-note scene patterns', () => { const patternData = buildPattern([[0x27, 0, (2 << 3) | 2, 0xd0, 0x62, 0xff, 0, 0x3d]]); const scenes = collectScenesAndPatterns( [new TarFile('patterns/a01', patternData.length, patternData)], 'TE032AS001', ); assert.equal(scenes.length, 1); assert.equal(scenes[0].patterns.length, 1); assert.equal(scenes[0].patterns[0].pad, 'a2'); assert.deepEqual(scenes[0].patterns[0].notes, []); assert.equal(scenes[0].patternEvents.a?.events.length, 1); }); test('uses the uint16 header count and ignores undeclared or partial records', () => { const records = Array.from({ length: 300 }, (_, index) => [ index & 0xff, index >> 8, 0, 60, 100, 1, 0, 0, ]); const data = buildPattern([...records, [0, 0, 1, 0, 0, 0, 0, 0]], 8, 300); const truncated = data.slice(0, data.length - 3); const result = parsePatternEvents(truncated); assert.equal(result.recordCount, 300); assert.equal(result.events.length, 300); assert.equal(result.events[299].position, 299); });