215 lines
5.2 KiB
TypeScript
215 lines
5.2 KiB
TypeScript
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]],
|
|
],
|
|
);
|
|
});
|