feat: Add comprehensive test suite for ableton song data structure
This commit is contained in:
222
tests/ableton-song.test.ts
Normal file
222
tests/ableton-song.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
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<Group, number>,
|
||||
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: [],
|
||||
d: [],
|
||||
},
|
||||
groupPatterns: [pattern('a', 1, 2, 0, 60), pattern('b', 2, 1, 0, 36), pattern('a', 3, 1, 1, 64)],
|
||||
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']],
|
||||
['A PATTERN 03', 'pattern', ['a']],
|
||||
],
|
||||
);
|
||||
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');
|
||||
|
||||
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), [2]);
|
||||
assert.deepEqual(a1?.lane?.arrangementClips, []);
|
||||
assert.deepEqual(b0?.lane?.sessionClips.map((clip) => clip.sceneIndex), [0]);
|
||||
assert.deepEqual(
|
||||
b0?.lane?.arrangementClips.map((clip) => [clip.occurrenceIndex, clip.offset]),
|
||||
[[1, 4]],
|
||||
);
|
||||
});
|
||||
|
||||
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],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user