import assert from 'node:assert/strict'; import test from 'node:test'; import abletonTransformer from '../src/lib/transformers/ableton'; import { EffectType, FaderParam, Group, GroupFaderParam, GroupPattern, Note, Pad, PatternEventKind, ProjectRawData, Scene, } from '../src/types/types'; function faderParams(): GroupFaderParam { return Object.fromEntries( (['a', 'b', 'c', 'd'] as Group[]).map((group) => [ group, Object.fromEntries(Array.from({ length: 12 }, (_, parameter) => [parameter, -1])), ]), ) as GroupFaderParam; } function pad(group: Group, index: number): Pad { return { pad: index + 1, name: `${group}${index}`, group, file: null, rawData: null, soundId: 0, volume: 100, attack: 0, release: 0, trimLeft: 0, trimRight: 0, soundLength: 0, playMode: 'oneshot', pan: 0, pitch: 0, rootNote: 60, timeStretch: 'off', timeStretchBpm: 0, timeStretchBars: 0, inChokeGroup: false, midiChannel: 0, }; } function pattern( group: Group, patternNumber: number, bars: number, source: number, noteValue: number, ): GroupPattern { const note: Note = { note: noteValue, position: 0, duration: 48, velocity: 100 }; return { group, patternNumber, bars, recordCount: 1, events: [ { type: 'note', kind: PatternEventKind.Note, position: 0, source, flags: 0, rawData: new Uint8Array(8), pad: source, note: noteValue, velocity: 100, duration: 48, }, ], notes: { [source]: [note] }, }; } function scene( number: number, patternNumbers: Record, numerator: number, denominator: number, ): Scene { return { number, name: String(number).padStart(2, '0'), patternNumbers, timeSignature: { numerator, denominator }, patterns: [], patternEvents: {}, }; } function project(): ProjectRawData { const params = faderParams(); return { pads: { a: [pad('a', 0), pad('a', 1)], b: [pad('b', 0)], c: [pad('c', 0)], d: [], }, groupPatterns: [ pattern('a', 1, 2, 0, 60), pattern('b', 2, 1, 0, 36), pattern('a', 3, 1, 1, 64), pattern('c', 3, 1, 0, 67), pattern('b', 1, 1, 0, 38), ], scenes: [ scene(1, { a: 1, b: 2, c: 0, d: 0 }, 3, 4), scene(2, { a: 1, b: 0, c: 0, d: 0 }, 2, 4), ], songPositions: [ { position: 0, sceneNumber: 2 }, { position: 1, sceneNumber: 1 }, { position: 2, sceneNumber: 2 }, ], settings: { bpm: 120, scale: 0, rootNote: 0, groupFaderParams: params, faderAssignment: { a: FaderParam.LVL, b: FaderParam.LVL, c: FaderParam.LVL, d: FaderParam.LVL, }, rawData: new Uint8Array(), }, effects: { rawData: new Uint8Array(), effectType: EffectType.Off, param1: 0, param2: 0, }, sounds: [], projectFile: null as unknown as File, scenesSettings: { timeSignature: { numerator: 4, denominator: 4 } }, }; } test('combines genuine Session scenes, orphan patterns and the repeated Song Arrangement', () => { const result = abletonTransformer(project(), { allScenes: true, includeArchivedSamples: false, keepUnusedPatterns: true, }); assert.deepEqual( result.scenes.map((item) => [item.name, item.kind, item.affectedGroups]), [ ['01', 'scene', ['a', 'b', 'c', 'd']], ['02', 'scene', ['a', 'b', 'c', 'd']], ['B PATTERN 01', 'pattern', ['b']], ['A PATTERN 03', 'pattern', ['a']], ['C PATTERN 03', 'pattern', ['c']], ], ); assert.deepEqual( result.arrangementSections.map((section) => [ section.sceneNumber, section.offset, section.duration, `${section.timeSignature.numerator}/${section.timeSignature.denominator}`, ]), [ [2, 0, 4, '2/4'], [1, 4, 6, '3/4'], [2, 10, 4, '2/4'], ], ); const a0 = result.tracks.find((track) => track.padCode === 'a0'); const a1 = result.tracks.find((track) => track.padCode === 'a1'); const b0 = result.tracks.find((track) => track.padCode === 'b0'); const c0 = result.tracks.find((track) => track.padCode === 'c0'); assert.deepEqual( a0?.lane?.sessionClips.map((clip) => clip.sceneIndex), [0, 1], ); assert.deepEqual( a0?.lane?.arrangementClips.map((clip) => [clip.occurrenceIndex, clip.offset]), [ [0, 0], [1, 4], [2, 10], ], ); assert.deepEqual( a1?.lane?.sessionClips.map((clip) => clip.sceneIndex), [3], ); assert.deepEqual(a1?.lane?.arrangementClips, []); assert.deepEqual( b0?.lane?.sessionClips.map((clip) => clip.sceneIndex), [0, 2], ); assert.deepEqual( b0?.lane?.arrangementClips.map((clip) => [clip.occurrenceIndex, clip.offset]), [[1, 4]], ); assert.deepEqual( c0?.lane?.sessionClips.map((clip) => clip.sceneIndex), [4], ); assert.deepEqual(c0?.lane?.arrangementClips, []); }); test('keeps Session material and leaves Arrangement empty when no Song is present', () => { const data = project(); data.songPositions = []; const result = abletonTransformer(data, { allScenes: false, selectedScenes: ['02'], includeArchivedSamples: false, keepUnusedPatterns: false, }); assert.deepEqual( result.scenes.map((item) => item.name), ['02'], ); assert.deepEqual(result.arrangementSections, []); assert.ok(result.tracks.every((track) => (track.lane?.arrangementClips.length || 0) === 0)); }); test('keeps the complete Song Arrangement when Session scene selection is limited', () => { const result = abletonTransformer(project(), { allScenes: false, selectedScenes: ['02'], includeArchivedSamples: false, keepUnusedPatterns: false, }); assert.deepEqual( result.scenes.map((item) => item.name), ['02'], ); assert.deepEqual( result.arrangementSections.map((section) => section.sceneNumber), [2, 1, 2], ); });