Files
ep-133-project-export/tests/ableton-song.test.ts

278 lines
6.5 KiB
TypeScript

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 faderPattern(group: Group, patternNumber: number): GroupPattern {
return {
group,
patternNumber,
bars: 1,
recordCount: 1,
events: [
{
type: 'fader',
kind: PatternEventKind.Fader,
position: 0,
source: 0,
flags: 0,
rawData: new Uint8Array(8),
parameter: FaderParam.LPF,
reserved: 0,
rawValue: 0,
value: 0,
},
],
notes: {},
};
}
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: [pad('c', 0)],
d: [],
},
groupPatterns: [
pattern('a', 1, 2, 0, 60),
pattern('b', 2, 1, 0, 36),
pattern('a', 3, 1, 1, 64),
pattern('c', 3, 2, 0, 67),
pattern('b', 1, 1, 0, 38),
faderPattern('d', 2),
],
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']],
['PATTERN 01 (B)', 'pattern', ['b']],
['PATTERN 03 (A/C)', 'pattern', ['a', 'c']],
],
);
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');
const c0 = result.tracks.find((track) => track.padCode === 'c0');
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),
[3],
);
assert.deepEqual(a1?.lane?.arrangementClips, []);
assert.equal(a1?.lane?.sessionClips[0].loopDuration, 4);
assert.deepEqual(
b0?.lane?.sessionClips.map((clip) => clip.sceneIndex),
[0, 2],
);
assert.deepEqual(
b0?.lane?.arrangementClips.map((clip) => [clip.occurrenceIndex, clip.offset]),
[[1, 4]],
);
assert.deepEqual(
c0?.lane?.sessionClips.map((clip) => clip.sceneIndex),
[3],
);
assert.deepEqual(c0?.lane?.arrangementClips, []);
assert.equal(c0?.lane?.sessionClips[0].loopDuration, 8);
});
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],
);
});