import assert from 'node:assert/strict'; import test from 'node:test'; import { buildMidiClipEnvelopes, decodeChannelPressure, } from '../src/lib/exporters/ableton/midi'; import abletonTransformer, { AblMidiEvent } from '../src/lib/transformers/ableton'; import { EffectType, FaderParam, Group, GroupFaderParam, Pad, PatternEventKind, ProjectRawData, } from '../src/types/types'; function midiEvent(position: number, source: number, value: number): AblMidiEvent { const rawData = new Uint8Array([ position & 0xff, position >> 8, (source << 3) | PatternEventKind.Midi, 0xd0, value, 0xff, 0, 0x3d, ]); return { position, source, flags: rawData[7], rawData, data: rawData.slice(3, 7), }; } 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, }; } test('decodes only the captured EP Channel Pressure record shape', () => { assert.equal(decodeChannelPressure(midiEvent(39, 2, 98)), 98); assert.equal( decodeChannelPressure({ ...midiEvent(39, 2, 98), data: new Uint8Array([0xd0, 98, 3, 4]) }), undefined, ); assert.equal( decodeChannelPressure({ ...midiEvent(39, 2, 98), data: new Uint8Array([0xb0, 1, 98, 0]) }), undefined, ); }); test('builds native Live 10 Channel Pressure steps with raw values', () => { const controllerTargets = { 'ControllerTargets.1': { '@Id': 45123, LockEnvelope: { '@Value': 0 } }, }; const envelopes = buildMidiClipEnvelopes( [midiEvent(192, 2, 20), midiEvent(96, 2, 75), midiEvent(96, 2, 20)], controllerTargets, ); const envelope = envelopes[0]; const events = envelope.Automation.Events.FloatEvent; assert.equal(envelope.EnvelopeTarget.PointeeId['@Value'], 45123); assert.equal(controllerTargets['ControllerTargets.1'].LockEnvelope['@Value'], 1); assert.deepEqual( events.map((event: any) => [event['@Time'], event['@Value']]), [ [-63072000, 0], [1, 0], [1, 75], [1, 75], [1, 20], [2, 20], ], ); assert.deepEqual(envelope.LoopSlot, { Value: {} }); }); test('retains an unchanged zero-pressure event at its original time', () => { const envelopes = buildMidiClipEnvelopes([midiEvent(48, 2, 0)], { 'ControllerTargets.1': { '@Id': 45123 }, }); assert.deepEqual( envelopes[0].Automation.Events.FloatEvent.map((event: any) => [ event['@Time'], event['@Value'], ]), [ [-63072000, 0], [0.5, 0], ], ); }); test('routes each source curve only to its matching pad clip, including MIDI-only pads', () => { const source0 = midiEvent(24, 0, 30); const source1 = midiEvent(48, 1, 60); const source2 = midiEvent(72, 2, 90); const params = faderParams(); const events = [source0, source1, source2].map((event) => ({ type: 'midi' as const, kind: PatternEventKind.Midi, ...event, })); const project = { pads: { a: [pad('a', 0), pad('a', 1), pad('a', 2)], b: [], c: [], d: [], }, scenes: [ { number: 1, name: '01', patternNumbers: { a: 1, b: 0, c: 0, d: 0 }, timeSignature: { numerator: 4, denominator: 4 }, patterns: [ { pad: 'a0', group: 'a', notes: [], bars: 1 }, { pad: 'a1', group: 'a', notes: [], bars: 1 }, { pad: 'a2', group: 'a', notes: [], bars: 1 }, ], patternEvents: { a: { group: 'a', patternNumber: 1, bars: 1, recordCount: 3, events, }, }, }, ], groupPatterns: [ { group: 'a', patternNumber: 1, bars: 1, recordCount: 3, events, notes: { 0: [], 1: [], 2: [] }, }, ], songPositions: [], 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 } }, } satisfies ProjectRawData; const result = abletonTransformer(project, {}); assert.deepEqual( result.tracks.map((track) => [ track.padCode, track.lane?.sessionClips[0].notes.length, track.lane?.sessionClips[0].midiEvents.map((event) => event.data[1]), ]), [ ['a0', 0, [30]], ['a1', 0, [60]], ['a2', 0, [90]], ], ); });