feat(tests): Add comprehensive hybrid test suite for Ableton project data
This commit is contained in:
232
tests/ableton-hybrid.test.ts
Normal file
232
tests/ableton-hybrid.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import abletonTransformer, {
|
||||
AblTrack,
|
||||
getDrumRackMidiNote,
|
||||
} from '../src/lib/transformers/ableton';
|
||||
import {
|
||||
EffectType,
|
||||
FaderParam,
|
||||
Group,
|
||||
GroupFaderParam,
|
||||
Note,
|
||||
Pad,
|
||||
PadCode,
|
||||
ProjectRawData,
|
||||
} 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(index: number, overrides: Partial<Pad> = {}): Pad {
|
||||
return {
|
||||
pad: index + 1,
|
||||
name: `a${index}`,
|
||||
group: 'a',
|
||||
file: null,
|
||||
rawData: null,
|
||||
soundId: index + 1,
|
||||
volume: 100,
|
||||
attack: 0,
|
||||
release: 0,
|
||||
trimLeft: 0,
|
||||
trimRight: 46875,
|
||||
soundLength: 1,
|
||||
playMode: 'oneshot',
|
||||
pan: 0,
|
||||
pitch: 0,
|
||||
rootNote: 60,
|
||||
timeStretch: 'off',
|
||||
timeStretchBpm: 0,
|
||||
timeStretchBars: 0,
|
||||
inChokeGroup: false,
|
||||
midiChannel: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function note(noteValue: number, position: number, velocity = 100): Note {
|
||||
return {
|
||||
note: noteValue,
|
||||
position,
|
||||
duration: 48,
|
||||
velocity,
|
||||
};
|
||||
}
|
||||
|
||||
function project(): ProjectRawData {
|
||||
const params = faderParams();
|
||||
return {
|
||||
pads: {
|
||||
a: [
|
||||
pad(0, { rootNote: 48, volume: 150, inChokeGroup: true }),
|
||||
pad(1, { playMode: 'key', inChokeGroup: true }),
|
||||
pad(2, { rootNote: 55, volume: 80, inChokeGroup: true }),
|
||||
],
|
||||
b: [],
|
||||
c: [],
|
||||
d: [],
|
||||
},
|
||||
scenes: [
|
||||
{
|
||||
name: '01',
|
||||
patterns: [
|
||||
{ pad: 'a0', group: 'a', notes: [note(72, 0, 110)], bars: 1 },
|
||||
{ pad: 'a1', group: 'a', notes: [note(60, 0), note(64, 96)], bars: 1 },
|
||||
],
|
||||
patternEvents: {
|
||||
a: {
|
||||
group: 'a',
|
||||
patternNumber: 1,
|
||||
bars: 1,
|
||||
recordCount: 0,
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '02',
|
||||
patterns: [
|
||||
{ pad: 'a0', group: 'a', notes: [note(72, 24, 90)], bars: 1 },
|
||||
{ pad: 'a1', group: 'a', notes: [note(67, 48)], bars: 1 },
|
||||
],
|
||||
patternEvents: {
|
||||
a: {
|
||||
group: 'a',
|
||||
patternNumber: 2,
|
||||
bars: 1,
|
||||
recordCount: 0,
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
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 } },
|
||||
};
|
||||
}
|
||||
|
||||
function trackShape(tracks: AblTrack[]) {
|
||||
return tracks.map((track) => ({
|
||||
name: track.name,
|
||||
drumRack: track.drumRack,
|
||||
padCode: track.padCode,
|
||||
children: track.tracks.map((child) => child.padCode),
|
||||
clips: track.lane?.clips.map((clip) => ({
|
||||
sceneIndex: clip.sceneIndex,
|
||||
notes: clip.notes.map((item) => item.note),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
test('automatically builds a per-group rack without flattening chromatic pads', () => {
|
||||
const result = abletonTransformer(project(), {
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: true,
|
||||
customSceneNames: { '01': 'Same', '02': 'Same' },
|
||||
});
|
||||
const rack = result.tracks.find((track) => track.drumRack);
|
||||
const melodic = result.tracks.find((track) => track.padCode === 'a1' && !track.drumRack);
|
||||
|
||||
assert.ok(rack);
|
||||
assert.ok(melodic);
|
||||
assert.equal(rack.name, 'A Pads');
|
||||
assert.deepEqual(
|
||||
rack.tracks.map((track) => track.padCode),
|
||||
['a0', 'a2'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
rack.lane?.clips.map((clip) => [clip.sceneIndex, clip.sceneName, clip.notes[0].note]),
|
||||
[
|
||||
[0, 'Same', getDrumRackMidiNote('a0')],
|
||||
[1, 'Same', getDrumRackMidiNote('a0')],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
melodic.lane?.clips.map((clip) => clip.notes.map((item) => item.note)),
|
||||
[
|
||||
[60, 64],
|
||||
[67],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves rack playback pitch, unused-pad root note, AMP and choke membership', () => {
|
||||
const result = abletonTransformer(project(), {
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: true,
|
||||
});
|
||||
const rack = result.tracks.find((track) => track.drumRack);
|
||||
const a0 = rack?.tracks.find((track) => track.padCode === 'a0');
|
||||
const a2 = rack?.tracks.find((track) => track.padCode === 'a2');
|
||||
const melodic = result.tracks.find((track) => track.padCode === 'a1' && !track.drumRack);
|
||||
|
||||
assert.ok(a0);
|
||||
assert.ok(a2);
|
||||
assert.ok(melodic);
|
||||
assert.equal(a0.drumRackSendingNote, 72);
|
||||
assert.equal(a0.volume, 0.75);
|
||||
assert.equal(a0.inChokeGroup, true);
|
||||
assert.equal(a2.drumRackSendingNote, 55);
|
||||
assert.equal(a2.volume, 0.4);
|
||||
assert.equal(a2.inChokeGroup, true);
|
||||
assert.equal(melodic.inChokeGroup, true);
|
||||
});
|
||||
|
||||
test('uses one hybrid classification for Arrangement and Session exports', () => {
|
||||
const arrangement = abletonTransformer(project(), {
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: true,
|
||||
clips: false,
|
||||
});
|
||||
const session = abletonTransformer(project(), {
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: true,
|
||||
clips: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(trackShape(arrangement.tracks), trackShape(session.tracks));
|
||||
});
|
||||
|
||||
test('does not create selector-note racks when samples are excluded', () => {
|
||||
const result = abletonTransformer(project(), {
|
||||
includeArchivedSamples: false,
|
||||
exportAllPadsWithSamples: true,
|
||||
});
|
||||
|
||||
assert.equal(result.tracks.some((track) => track.drumRack), false);
|
||||
assert.deepEqual(
|
||||
result.tracks.map((track) => track.padCode),
|
||||
['a0', 'a1', 'a2'] as PadCode[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.tracks[0].lane?.clips.map((clip) => clip.notes.map((item) => item.note)),
|
||||
[[72], [72]],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user