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