Update Ableton MIDI exporter and add comprehensive test suite
This commit is contained in:
180
tests/ableton-midi.test.ts
Normal file
180
tests/ableton-midi.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
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 envelopes = buildMidiClipEnvelopes(
|
||||
[midiEvent(192, 2, 20), midiEvent(96, 2, 75), midiEvent(96, 2, 20)],
|
||||
{ 'ControllerTargets.129': { '@Id': 45123 } },
|
||||
);
|
||||
const envelope = envelopes[0];
|
||||
const events = envelope.Automation.Events.FloatEvent;
|
||||
|
||||
assert.equal(envelope.EnvelopeTarget.PointeeId['@Value'], 45123);
|
||||
assert.deepEqual(
|
||||
events.map((event: any) => [event['@Time'], event['@Value']]),
|
||||
[
|
||||
[-63072000, 0],
|
||||
[1, 0],
|
||||
[1, 75],
|
||||
[1, 75],
|
||||
[1, 20],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(envelope.LoopSlot, { Value: {} });
|
||||
});
|
||||
|
||||
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 project = {
|
||||
pads: {
|
||||
a: [pad('a', 0), pad('a', 1), pad('a', 2)],
|
||||
b: [],
|
||||
c: [],
|
||||
d: [],
|
||||
},
|
||||
scenes: [
|
||||
{
|
||||
name: '01',
|
||||
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: [source0, source1, source2].map((event) => ({
|
||||
type: 'midi' as const,
|
||||
kind: PatternEventKind.Midi,
|
||||
...event,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
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?.clips[0].notes.length,
|
||||
track.lane?.clips[0].midiEvents.map((event) => event.data[1]),
|
||||
]),
|
||||
[
|
||||
['a0', 0, [30]],
|
||||
['a1', 0, [60]],
|
||||
['a2', 0, [90]],
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user