2026-07-13 04:39:10 +02:00
|
|
|
import * as Sentry from '@sentry/react';
|
|
|
|
|
import omit from 'lodash/omit';
|
|
|
|
|
import {
|
|
|
|
|
ExporterParams,
|
2026-07-13 04:53:36 +02:00
|
|
|
FaderAutomationEvent,
|
2026-07-13 04:39:10 +02:00
|
|
|
FaderParam,
|
2026-07-14 09:37:36 +02:00
|
|
|
Group,
|
|
|
|
|
GroupPattern,
|
2026-07-14 06:40:46 +02:00
|
|
|
MidiPatternEvent,
|
2026-07-13 04:39:10 +02:00
|
|
|
Note,
|
|
|
|
|
Pad,
|
|
|
|
|
PadCode,
|
|
|
|
|
ProjectRawData,
|
2026-07-14 09:37:36 +02:00
|
|
|
Scene,
|
2026-07-13 04:39:10 +02:00
|
|
|
TimeSignature,
|
|
|
|
|
} from '../../types/types';
|
|
|
|
|
import { getSampleName } from '../exporters/utils';
|
|
|
|
|
import { findPad, findSoundByPad, findSoundIdByPad } from '../utils';
|
|
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const PROJECT_GROUPS: Group[] = ['a', 'b', 'c', 'd'];
|
|
|
|
|
|
2026-07-13 04:39:10 +02:00
|
|
|
export type AblData = {
|
|
|
|
|
tracks: AblTrack[];
|
|
|
|
|
scenes: AblScene[];
|
2026-07-13 04:57:31 +02:00
|
|
|
groupLanes: AblGroupLane[];
|
2026-07-14 09:37:36 +02:00
|
|
|
arrangementSections: AblArrangementSection[];
|
2026-07-13 04:39:10 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AblTrack = Omit<Pad, 'file' | 'rawData' | 'midiChannel'> & {
|
|
|
|
|
padCode: PadCode;
|
2026-07-14 09:37:36 +02:00
|
|
|
group: Group;
|
2026-07-13 04:39:10 +02:00
|
|
|
sampleName: string;
|
|
|
|
|
sampleChannels: number;
|
|
|
|
|
sampleRate: number;
|
|
|
|
|
sampleRootNote: number;
|
|
|
|
|
samplePitch: number;
|
|
|
|
|
bpm: number;
|
|
|
|
|
drumRack: boolean;
|
2026-07-14 07:55:54 +02:00
|
|
|
drumRackSendingNote?: number;
|
2026-07-13 04:39:10 +02:00
|
|
|
lane?: AblLane;
|
|
|
|
|
tracks: AblTrack[];
|
|
|
|
|
faderParams: { [K in FaderParam]: number };
|
|
|
|
|
timeSignature: TimeSignature;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AblLane = {
|
|
|
|
|
padCode: PadCode;
|
2026-07-14 09:37:36 +02:00
|
|
|
sessionClips: AblClip[];
|
|
|
|
|
arrangementClips: AblClip[];
|
2026-07-13 04:39:10 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AblNote = Note;
|
|
|
|
|
|
|
|
|
|
export type AblClip = {
|
|
|
|
|
notes: AblNote[];
|
2026-07-14 06:40:46 +02:00
|
|
|
midiEvents: AblMidiEvent[];
|
2026-07-13 04:39:10 +02:00
|
|
|
bars: number;
|
|
|
|
|
offset: number;
|
2026-07-14 09:37:36 +02:00
|
|
|
duration: number;
|
|
|
|
|
loopDuration: number;
|
|
|
|
|
sceneIndex?: number;
|
|
|
|
|
occurrenceIndex?: number;
|
2026-07-13 04:39:10 +02:00
|
|
|
sceneName: string;
|
|
|
|
|
timeSignature: TimeSignature;
|
|
|
|
|
faderParams: { [K in FaderParam]: number };
|
2026-07-13 04:57:31 +02:00
|
|
|
groupPattern?: AblGroupPatternOccurrence;
|
2026-07-13 04:53:36 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-14 06:40:46 +02:00
|
|
|
export type AblMidiEvent = Pick<
|
|
|
|
|
MidiPatternEvent,
|
|
|
|
|
'position' | 'source' | 'flags' | 'data' | 'rawData'
|
|
|
|
|
>;
|
|
|
|
|
|
2026-07-13 04:53:36 +02:00
|
|
|
export type AblFaderAutomationPoint = Pick<FaderAutomationEvent, 'position' | 'value' | 'flags'> & {
|
|
|
|
|
parameter: FaderParam;
|
2026-07-13 04:39:10 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-13 04:57:31 +02:00
|
|
|
export type AblGroupPatternOccurrence = {
|
2026-07-14 09:37:36 +02:00
|
|
|
group: Group;
|
|
|
|
|
patternNumber: number;
|
2026-07-13 04:57:31 +02:00
|
|
|
bars: number;
|
|
|
|
|
offset: number;
|
2026-07-14 09:37:36 +02:00
|
|
|
duration: number;
|
|
|
|
|
loopDuration: number;
|
|
|
|
|
sceneIndex?: number;
|
|
|
|
|
occurrenceIndex?: number;
|
|
|
|
|
sceneName: string;
|
2026-07-13 04:57:31 +02:00
|
|
|
timeSignature: TimeSignature;
|
|
|
|
|
faderParams: { [K in FaderParam]: number };
|
|
|
|
|
faderAutomation: AblFaderAutomationPoint[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AblGroupLane = {
|
2026-07-14 09:37:36 +02:00
|
|
|
group: Group;
|
|
|
|
|
sessionPatterns: AblGroupPatternOccurrence[];
|
|
|
|
|
arrangementPatterns: AblGroupPatternOccurrence[];
|
2026-07-13 04:57:31 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-13 04:39:10 +02:00
|
|
|
export type AblScene = {
|
|
|
|
|
name: string;
|
2026-07-14 09:37:36 +02:00
|
|
|
kind: 'scene' | 'pattern';
|
|
|
|
|
affectedGroups: Group[];
|
|
|
|
|
timeSignature?: TimeSignature;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AblArrangementSection = {
|
|
|
|
|
sceneNumber: number;
|
|
|
|
|
sceneName: string;
|
|
|
|
|
offset: number;
|
|
|
|
|
duration: number;
|
|
|
|
|
timeSignature: TimeSignature;
|
2026-07-13 04:39:10 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-14 07:55:54 +02:00
|
|
|
export function getDrumRackMidiNote(padCode: PadCode) {
|
|
|
|
|
const padNumber = parseInt(padCode.slice(1), 10);
|
|
|
|
|
const row = 3 - Math.floor(padNumber / 3);
|
|
|
|
|
const col = padNumber % 3;
|
2026-07-14 09:37:36 +02:00
|
|
|
return 36 + row * 4 + col;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQuarterNotesPerBar(timeSignature: TimeSignature) {
|
|
|
|
|
return timeSignature.numerator * (4 / timeSignature.denominator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPatternKey(group: Group, patternNumber: number) {
|
|
|
|
|
return `${group}${patternNumber}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPatternSources(pattern: GroupPattern) {
|
|
|
|
|
const sources = new Set(Object.keys(pattern.notes).map(Number));
|
|
|
|
|
pattern.events.forEach((event) => {
|
|
|
|
|
if (event.type === 'midi' && event.source >= 0 && event.source < 12) {
|
|
|
|
|
sources.add(event.source);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return [...sources].toSorted((a, b) => a - b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getScenePatterns(scene: Scene, patternLookup: Map<string, GroupPattern>) {
|
|
|
|
|
return PROJECT_GROUPS.map((group) =>
|
|
|
|
|
patternLookup.get(getPatternKey(group, scene.patternNumbers[group])),
|
|
|
|
|
).filter((pattern): pattern is GroupPattern => Boolean(pattern));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSceneDuration(scene: Scene, patternLookup: Map<string, GroupPattern>) {
|
2026-07-14 09:42:24 +02:00
|
|
|
const sceneBars = Math.max(
|
|
|
|
|
1,
|
|
|
|
|
...getScenePatterns(scene, patternLookup).map((pattern) => pattern.bars),
|
|
|
|
|
);
|
2026-07-14 09:37:36 +02:00
|
|
|
return sceneBars * getQuarterNotesPerBar(scene.timeSignature);
|
2026-07-14 07:55:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTrackNoteValues(track: AblTrack) {
|
2026-07-14 09:42:24 +02:00
|
|
|
const clips = [...(track.lane?.sessionClips || []), ...(track.lane?.arrangementClips || [])];
|
2026-07-14 09:37:36 +02:00
|
|
|
return [...new Set(clips.flatMap((clip) => clip.notes.map((note) => note.note)))];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasTrackMidiEvents(track: AblTrack) {
|
2026-07-14 09:42:24 +02:00
|
|
|
return [...(track.lane?.sessionClips || []), ...(track.lane?.arrangementClips || [])].some(
|
|
|
|
|
(clip) => clip.midiEvents.length > 0,
|
|
|
|
|
);
|
2026-07-14 09:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fallbackPad(group: Group, source: number): Pad {
|
|
|
|
|
return {
|
|
|
|
|
group,
|
|
|
|
|
volume: 100,
|
|
|
|
|
attack: 0,
|
|
|
|
|
release: 0,
|
|
|
|
|
trimLeft: 0,
|
|
|
|
|
trimRight: 0,
|
|
|
|
|
pad: source + 1,
|
|
|
|
|
playMode: 'oneshot',
|
|
|
|
|
pan: 0,
|
|
|
|
|
pitch: 0,
|
|
|
|
|
rootNote: 60,
|
|
|
|
|
timeStretch: 'off',
|
|
|
|
|
timeStretchBpm: 0,
|
|
|
|
|
timeStretchBars: 0,
|
|
|
|
|
soundId: 0,
|
|
|
|
|
name: '',
|
|
|
|
|
file: null,
|
|
|
|
|
rawData: null,
|
|
|
|
|
soundLength: 0,
|
|
|
|
|
inChokeGroup: false,
|
|
|
|
|
midiChannel: 0,
|
|
|
|
|
};
|
2026-07-14 07:55:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-13 04:39:10 +02:00
|
|
|
function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams) {
|
|
|
|
|
const customSceneNames = exporterParams.customSceneNames || {};
|
2026-07-14 09:37:36 +02:00
|
|
|
const patternLookup = new Map(
|
|
|
|
|
data.groupPatterns.map((pattern) => [
|
|
|
|
|
getPatternKey(pattern.group, pattern.patternNumber),
|
|
|
|
|
pattern,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const trackLookup = new Map<PadCode, AblTrack>();
|
|
|
|
|
const laneLookup = new Map<PadCode, AblLane>();
|
|
|
|
|
const groupLaneLookup = new Map<Group, AblGroupLane>(
|
2026-07-14 09:42:24 +02:00
|
|
|
PROJECT_GROUPS.map((group) => [group, { group, sessionPatterns: [], arrangementPatterns: [] }]),
|
2026-07-14 09:37:36 +02:00
|
|
|
);
|
2026-07-13 04:39:10 +02:00
|
|
|
const ablScenes: AblScene[] = [];
|
2026-07-14 09:37:36 +02:00
|
|
|
const arrangementSections: AblArrangementSection[] = [];
|
|
|
|
|
|
|
|
|
|
const getSceneName = (scene: Scene) => customSceneNames[scene.name] || scene.name;
|
|
|
|
|
|
|
|
|
|
const ensureTrack = (padCode: PadCode, group: Group, timeSignature: TimeSignature) => {
|
|
|
|
|
const existingTrack = trackLookup.get(padCode);
|
|
|
|
|
if (existingTrack) {
|
|
|
|
|
return existingTrack;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const source = Number(padCode.slice(1));
|
|
|
|
|
const pad = findPad(padCode, data.pads) || fallbackPad(group, source);
|
|
|
|
|
const soundId = findSoundIdByPad(padCode, data.pads) || 0;
|
|
|
|
|
const sound = findSoundByPad(padCode, data.pads, data.sounds);
|
|
|
|
|
const faderParams = data.settings.groupFaderParams[group];
|
|
|
|
|
const track: AblTrack = {
|
|
|
|
|
...omit(pad, ['file', 'rawData']),
|
|
|
|
|
soundId,
|
|
|
|
|
padCode,
|
|
|
|
|
group,
|
|
|
|
|
name: sound?.meta?.name || padCode,
|
|
|
|
|
volume: pad.volume / 200,
|
|
|
|
|
sampleName: getSampleName(sound?.meta?.name, soundId),
|
|
|
|
|
sampleChannels: sound?.meta?.channels || 0,
|
|
|
|
|
sampleRate: sound?.meta?.samplerate || 0,
|
|
|
|
|
sampleRootNote: pad.rootNote,
|
|
|
|
|
samplePitch: sound?.meta?.['sound.pitch'] ?? 0,
|
|
|
|
|
bpm: data.settings.bpm,
|
|
|
|
|
drumRack: false,
|
|
|
|
|
tracks: [],
|
|
|
|
|
faderParams,
|
|
|
|
|
timeSignature,
|
2026-07-13 04:39:10 +02:00
|
|
|
};
|
2026-07-14 09:37:36 +02:00
|
|
|
trackLookup.set(padCode, track);
|
|
|
|
|
return track;
|
|
|
|
|
};
|
2026-07-13 04:57:31 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const ensureLane = (track: AblTrack) => {
|
|
|
|
|
const existingLane = laneLookup.get(track.padCode);
|
|
|
|
|
if (existingLane) {
|
|
|
|
|
return existingLane;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lane: AblLane = {
|
|
|
|
|
padCode: track.padCode,
|
|
|
|
|
sessionClips: [],
|
|
|
|
|
arrangementClips: [],
|
|
|
|
|
};
|
|
|
|
|
laneLookup.set(track.padCode, lane);
|
|
|
|
|
track.lane = lane;
|
|
|
|
|
return lane;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createOccurrence = (
|
|
|
|
|
pattern: GroupPattern,
|
|
|
|
|
sceneName: string,
|
|
|
|
|
timeSignature: TimeSignature,
|
|
|
|
|
offset: number,
|
|
|
|
|
duration: number,
|
|
|
|
|
sceneIndex?: number,
|
|
|
|
|
occurrenceIndex?: number,
|
|
|
|
|
): AblGroupPatternOccurrence => {
|
|
|
|
|
const occurrence: AblGroupPatternOccurrence = {
|
|
|
|
|
group: pattern.group,
|
|
|
|
|
patternNumber: pattern.patternNumber,
|
|
|
|
|
bars: pattern.bars,
|
|
|
|
|
offset,
|
|
|
|
|
duration,
|
2026-07-14 09:38:26 +02:00
|
|
|
loopDuration: Math.max(1, pattern.bars) * getQuarterNotesPerBar(timeSignature),
|
2026-07-14 09:37:36 +02:00
|
|
|
sceneIndex,
|
|
|
|
|
occurrenceIndex,
|
|
|
|
|
sceneName,
|
|
|
|
|
timeSignature,
|
|
|
|
|
faderParams: data.settings.groupFaderParams[pattern.group],
|
|
|
|
|
faderAutomation: pattern.events
|
|
|
|
|
.filter((event): event is FaderAutomationEvent => event.type === 'fader')
|
|
|
|
|
.filter((event) => event.parameter >= FaderParam.LVL && event.parameter <= FaderParam.MOD)
|
|
|
|
|
.map((event) => ({
|
|
|
|
|
position: event.position,
|
|
|
|
|
parameter: event.parameter as FaderParam,
|
|
|
|
|
value: event.value,
|
|
|
|
|
flags: event.flags,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
const groupLane = groupLaneLookup.get(pattern.group);
|
|
|
|
|
if (groupLane) {
|
|
|
|
|
if (sceneIndex !== undefined) {
|
|
|
|
|
groupLane.sessionPatterns.push(occurrence);
|
|
|
|
|
}
|
|
|
|
|
if (occurrenceIndex !== undefined) {
|
|
|
|
|
groupLane.arrangementPatterns.push(occurrence);
|
2026-07-13 04:57:31 +02:00
|
|
|
}
|
2026-07-14 09:37:36 +02:00
|
|
|
}
|
|
|
|
|
return occurrence;
|
|
|
|
|
};
|
2026-07-13 04:57:31 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const addPatternClips = (
|
|
|
|
|
pattern: GroupPattern,
|
|
|
|
|
occurrence: AblGroupPatternOccurrence,
|
|
|
|
|
destination: 'session' | 'arrangement',
|
|
|
|
|
) => {
|
|
|
|
|
getPatternSources(pattern).forEach((source) => {
|
|
|
|
|
const padCode = `${pattern.group}${source}` as PadCode;
|
|
|
|
|
const track = ensureTrack(padCode, pattern.group, occurrence.timeSignature);
|
|
|
|
|
const lane = ensureLane(track);
|
|
|
|
|
const midiEvents = pattern.events.filter(
|
|
|
|
|
(event): event is MidiPatternEvent => event.type === 'midi' && event.source === source,
|
|
|
|
|
);
|
|
|
|
|
const clip: AblClip = {
|
|
|
|
|
notes: pattern.notes[source] || [],
|
|
|
|
|
midiEvents,
|
2026-07-13 04:57:31 +02:00
|
|
|
bars: pattern.bars,
|
2026-07-14 09:37:36 +02:00
|
|
|
offset: occurrence.offset,
|
|
|
|
|
duration: occurrence.duration,
|
|
|
|
|
loopDuration: occurrence.loopDuration,
|
|
|
|
|
sceneIndex: occurrence.sceneIndex,
|
|
|
|
|
occurrenceIndex: occurrence.occurrenceIndex,
|
|
|
|
|
sceneName: occurrence.sceneName,
|
|
|
|
|
timeSignature: occurrence.timeSignature,
|
|
|
|
|
faderParams: occurrence.faderParams,
|
|
|
|
|
groupPattern: occurrence,
|
2026-07-13 04:57:31 +02:00
|
|
|
};
|
2026-07-14 09:37:36 +02:00
|
|
|
if (destination === 'session') {
|
|
|
|
|
lane.sessionClips.push(clip);
|
|
|
|
|
} else {
|
|
|
|
|
lane.arrangementClips.push(clip);
|
2026-07-13 04:57:31 +02:00
|
|
|
}
|
|
|
|
|
});
|
2026-07-14 09:37:36 +02:00
|
|
|
};
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const selectedSceneNames = new Set(exporterParams.selectedScenes || []);
|
|
|
|
|
const sessionScenes = data.scenes.filter(
|
|
|
|
|
(scene) => exporterParams.allScenes !== false || selectedSceneNames.has(scene.name),
|
|
|
|
|
);
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
sessionScenes.forEach((scene) => {
|
|
|
|
|
const sceneIndex = ablScenes.length;
|
|
|
|
|
const sceneName = getSceneName(scene);
|
|
|
|
|
const duration = getSceneDuration(scene, patternLookup);
|
|
|
|
|
ablScenes.push({
|
|
|
|
|
name: sceneName,
|
|
|
|
|
kind: 'scene',
|
|
|
|
|
affectedGroups: [...PROJECT_GROUPS],
|
|
|
|
|
timeSignature: scene.timeSignature,
|
|
|
|
|
});
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
PROJECT_GROUPS.forEach((group) => {
|
|
|
|
|
const pattern = patternLookup.get(getPatternKey(group, scene.patternNumbers[group]));
|
|
|
|
|
if (!pattern) {
|
|
|
|
|
return;
|
2026-07-13 04:39:10 +02:00
|
|
|
}
|
2026-07-14 09:37:36 +02:00
|
|
|
const occurrence = createOccurrence(
|
|
|
|
|
pattern,
|
|
|
|
|
sceneName,
|
|
|
|
|
scene.timeSignature,
|
|
|
|
|
0,
|
|
|
|
|
duration,
|
|
|
|
|
sceneIndex,
|
|
|
|
|
);
|
|
|
|
|
addPatternClips(pattern, occurrence, 'session');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
if (exporterParams.keepUnusedPatterns !== false) {
|
|
|
|
|
const referencedPatterns = new Set<string>();
|
|
|
|
|
data.scenes.forEach((scene) => {
|
|
|
|
|
PROJECT_GROUPS.forEach((group) => {
|
|
|
|
|
const patternNumber = scene.patternNumbers[group];
|
|
|
|
|
if (patternNumber > 0) {
|
|
|
|
|
referencedPatterns.add(getPatternKey(group, patternNumber));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 10:32:22 +02:00
|
|
|
const unusedPatterns = data.groupPatterns
|
2026-07-14 09:37:36 +02:00
|
|
|
.filter(
|
|
|
|
|
(pattern) =>
|
2026-07-14 10:24:07 +02:00
|
|
|
getPatternSources(pattern).length > 0 &&
|
2026-07-14 09:37:36 +02:00
|
|
|
!referencedPatterns.has(getPatternKey(pattern.group, pattern.patternNumber)),
|
|
|
|
|
)
|
|
|
|
|
.toSorted((a, b) => {
|
2026-07-14 10:20:59 +02:00
|
|
|
const patternDifference = a.patternNumber - b.patternNumber;
|
2026-07-14 09:37:36 +02:00
|
|
|
const groupDifference = PROJECT_GROUPS.indexOf(a.group) - PROJECT_GROUPS.indexOf(b.group);
|
2026-07-14 10:20:59 +02:00
|
|
|
return patternDifference || groupDifference;
|
2026-07-14 10:32:22 +02:00
|
|
|
});
|
|
|
|
|
const patternRows = new Map<number, GroupPattern[]>();
|
|
|
|
|
unusedPatterns.forEach((pattern) => {
|
|
|
|
|
const row = patternRows.get(pattern.patternNumber) || [];
|
|
|
|
|
row.push(pattern);
|
|
|
|
|
patternRows.set(pattern.patternNumber, row);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
patternRows.forEach((patterns, patternNumber) => {
|
|
|
|
|
const sceneIndex = ablScenes.length;
|
|
|
|
|
const affectedGroups = patterns.map((pattern) => pattern.group);
|
|
|
|
|
const patternLabel = String(patternNumber).padStart(2, '0');
|
|
|
|
|
const sceneName =
|
|
|
|
|
patterns.length === 1
|
|
|
|
|
? `${patterns[0].group.toUpperCase()} PATTERN ${patternLabel}`
|
|
|
|
|
: `PATTERN ${patternLabel} (${affectedGroups.map((group) => group.toUpperCase()).join('/')})`;
|
|
|
|
|
const timeSignature = data.scenesSettings.timeSignature;
|
|
|
|
|
ablScenes.push({
|
|
|
|
|
name: sceneName,
|
|
|
|
|
kind: 'pattern',
|
|
|
|
|
affectedGroups,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
patterns.forEach((pattern) => {
|
2026-07-14 09:37:36 +02:00
|
|
|
const duration = Math.max(1, pattern.bars) * getQuarterNotesPerBar(timeSignature);
|
|
|
|
|
const occurrence = createOccurrence(
|
|
|
|
|
pattern,
|
|
|
|
|
sceneName,
|
|
|
|
|
timeSignature,
|
|
|
|
|
0,
|
|
|
|
|
duration,
|
|
|
|
|
sceneIndex,
|
|
|
|
|
);
|
|
|
|
|
addPatternClips(pattern, occurrence, 'session');
|
2026-07-13 04:39:10 +02:00
|
|
|
});
|
2026-07-14 10:32:22 +02:00
|
|
|
});
|
2026-07-14 09:37:36 +02:00
|
|
|
}
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
let arrangementOffset = 0;
|
|
|
|
|
data.songPositions.forEach((songPosition, occurrenceIndex) => {
|
|
|
|
|
const scene = data.scenes.find((candidate) => candidate.number === songPosition.sceneNumber);
|
|
|
|
|
if (!scene) {
|
|
|
|
|
console.warn(`Could not find scene ${songPosition.sceneNumber} from Song position`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const sceneName = getSceneName(scene);
|
|
|
|
|
const duration = getSceneDuration(scene, patternLookup);
|
|
|
|
|
arrangementSections.push({
|
|
|
|
|
sceneNumber: scene.number,
|
|
|
|
|
sceneName,
|
|
|
|
|
offset: arrangementOffset,
|
|
|
|
|
duration,
|
|
|
|
|
timeSignature: scene.timeSignature,
|
|
|
|
|
});
|
2026-07-13 04:39:10 +02:00
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
PROJECT_GROUPS.forEach((group) => {
|
|
|
|
|
const pattern = patternLookup.get(getPatternKey(group, scene.patternNumbers[group]));
|
|
|
|
|
if (!pattern) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const occurrence = createOccurrence(
|
|
|
|
|
pattern,
|
|
|
|
|
sceneName,
|
|
|
|
|
scene.timeSignature,
|
|
|
|
|
arrangementOffset,
|
|
|
|
|
duration,
|
|
|
|
|
undefined,
|
|
|
|
|
occurrenceIndex,
|
|
|
|
|
);
|
|
|
|
|
addPatternClips(pattern, occurrence, 'arrangement');
|
|
|
|
|
});
|
|
|
|
|
arrangementOffset += duration;
|
2026-07-13 04:39:10 +02:00
|
|
|
});
|
|
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
let tracks = [...trackLookup.values()].sort((a, b) =>
|
2026-07-13 04:39:10 +02:00
|
|
|
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (exporterParams.exportAllPadsWithSamples) {
|
2026-07-14 09:37:36 +02:00
|
|
|
PROJECT_GROUPS.forEach((group) => {
|
|
|
|
|
data.pads[group].forEach((pad, index) => {
|
2026-07-13 04:39:10 +02:00
|
|
|
if (pad.soundId <= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const padCode = `${group}${index}` as PadCode;
|
2026-07-14 09:37:36 +02:00
|
|
|
if (trackLookup.has(padCode)) {
|
2026-07-13 04:39:10 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-07-14 09:37:36 +02:00
|
|
|
const track = ensureTrack(padCode, group, data.scenesSettings.timeSignature);
|
|
|
|
|
tracks.push(track);
|
2026-07-13 04:39:10 +02:00
|
|
|
});
|
2026-07-14 09:37:36 +02:00
|
|
|
});
|
|
|
|
|
tracks = [...new Map(tracks.map((track) => [track.padCode, track])).values()].sort((a, b) =>
|
2026-07-13 04:39:10 +02:00
|
|
|
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const createDrumRackTrack = (group: Group): AblTrack | null => {
|
|
|
|
|
const groupTracks = tracks
|
2026-07-14 07:55:54 +02:00
|
|
|
.filter((track) => track.group === group && track.sampleName)
|
|
|
|
|
.filter((track) => getTrackNoteValues(track).length <= 1)
|
2026-07-14 09:37:36 +02:00
|
|
|
.filter((track) => !hasTrackMidiEvents(track))
|
2026-07-14 07:55:54 +02:00
|
|
|
.map((track) => ({
|
|
|
|
|
...track,
|
|
|
|
|
drumRackSendingNote: getTrackNoteValues(track)[0] ?? track.rootNote,
|
|
|
|
|
}));
|
2026-07-14 09:37:36 +02:00
|
|
|
if (groupTracks.length === 0) {
|
2026-07-13 04:39:10 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const mergeClips = (destination: 'session' | 'arrangement') => {
|
|
|
|
|
const clips = new Map<number, AblClip>();
|
|
|
|
|
groupTracks
|
|
|
|
|
.toSorted((a, b) =>
|
|
|
|
|
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
|
|
|
|
)
|
|
|
|
|
.forEach((track) => {
|
|
|
|
|
const drumPad = getDrumRackMidiNote(track.padCode);
|
|
|
|
|
const sourceClips =
|
|
|
|
|
destination === 'session'
|
|
|
|
|
? track.lane?.sessionClips || []
|
|
|
|
|
: track.lane?.arrangementClips || [];
|
|
|
|
|
sourceClips.forEach((clip) => {
|
|
|
|
|
const key = destination === 'session' ? clip.sceneIndex : clip.occurrenceIndex;
|
|
|
|
|
if (key === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!clips.has(key)) {
|
|
|
|
|
const mergedClip = structuredClone(clip);
|
|
|
|
|
mergedClip.notes = [];
|
|
|
|
|
mergedClip.midiEvents = [];
|
|
|
|
|
clips.set(key, mergedClip);
|
|
|
|
|
}
|
|
|
|
|
const mergedClip = clips.get(key);
|
|
|
|
|
if (!mergedClip) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mergedClip.notes.push(
|
|
|
|
|
...clip.notes.map((note) => ({
|
|
|
|
|
...note,
|
|
|
|
|
note: drumPad,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
mergedClip.midiEvents.push(...clip.midiEvents);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return [...clips.values()].sort((a, b) => {
|
|
|
|
|
const aIndex = destination === 'session' ? a.sceneIndex : a.occurrenceIndex;
|
|
|
|
|
const bIndex = destination === 'session' ? b.sceneIndex : b.occurrenceIndex;
|
|
|
|
|
return (aIndex ?? 0) - (bIndex ?? 0);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lane: AblLane = {
|
|
|
|
|
padCode: `${group}0` as PadCode,
|
|
|
|
|
sessionClips: mergeClips('session'),
|
|
|
|
|
arrangementClips: mergeClips('arrangement'),
|
|
|
|
|
};
|
2026-07-13 04:39:10 +02:00
|
|
|
const drumTrack: AblTrack = {
|
|
|
|
|
padCode: `${group}0` as PadCode,
|
|
|
|
|
group,
|
|
|
|
|
sampleName: '',
|
|
|
|
|
sampleChannels: 0,
|
|
|
|
|
sampleRate: 0,
|
|
|
|
|
sampleRootNote: 60,
|
|
|
|
|
samplePitch: 0,
|
|
|
|
|
bpm: data.settings.bpm,
|
|
|
|
|
drumRack: true,
|
2026-07-14 07:55:54 +02:00
|
|
|
drumRackSendingNote: undefined,
|
2026-07-13 04:39:10 +02:00
|
|
|
soundId: 0,
|
2026-07-14 07:55:54 +02:00
|
|
|
name: `${group.toUpperCase()} Pads`,
|
2026-07-14 09:37:36 +02:00
|
|
|
volume: 1,
|
2026-07-13 04:39:10 +02:00
|
|
|
attack: 0,
|
|
|
|
|
release: 0,
|
|
|
|
|
trimLeft: 0,
|
|
|
|
|
trimRight: 0,
|
|
|
|
|
pad: 0,
|
2026-07-14 09:42:24 +02:00
|
|
|
lane: lane.sessionClips.length > 0 || lane.arrangementClips.length > 0 ? lane : undefined,
|
2026-07-13 04:39:10 +02:00
|
|
|
playMode: 'oneshot',
|
|
|
|
|
pan: 0,
|
|
|
|
|
pitch: 0,
|
|
|
|
|
rootNote: 60,
|
|
|
|
|
timeStretch: 'off',
|
|
|
|
|
timeStretchBpm: 0,
|
|
|
|
|
timeStretchBars: 0,
|
|
|
|
|
soundLength: 0,
|
2026-07-14 09:37:36 +02:00
|
|
|
tracks: groupTracks,
|
2026-07-13 04:39:10 +02:00
|
|
|
inChokeGroup: false,
|
|
|
|
|
faderParams: data.settings.groupFaderParams[group],
|
|
|
|
|
timeSignature: data.scenesSettings.timeSignature,
|
|
|
|
|
};
|
|
|
|
|
return drumTrack;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const drumRackTracks: AblTrack[] = [];
|
2026-07-14 07:55:54 +02:00
|
|
|
if (exporterParams.includeArchivedSamples) {
|
2026-07-14 09:37:36 +02:00
|
|
|
PROJECT_GROUPS.forEach((group) => {
|
2026-07-13 04:39:10 +02:00
|
|
|
const drumTrack = createDrumRackTrack(group);
|
2026-07-14 09:37:36 +02:00
|
|
|
if (!drumTrack) {
|
|
|
|
|
return;
|
2026-07-13 04:39:10 +02:00
|
|
|
}
|
2026-07-14 09:37:36 +02:00
|
|
|
drumRackTracks.push(drumTrack);
|
|
|
|
|
const rackPadCodes = new Set(drumTrack.tracks.map((track) => track.padCode));
|
|
|
|
|
tracks = tracks.filter((track) => !rackPadCodes.has(track.padCode));
|
|
|
|
|
});
|
2026-07-13 04:39:10 +02:00
|
|
|
}
|
|
|
|
|
tracks.unshift(...drumRackTracks);
|
|
|
|
|
|
2026-07-14 09:37:36 +02:00
|
|
|
const groupLanes = PROJECT_GROUPS.map((group) => groupLaneLookup.get(group)).filter(
|
|
|
|
|
(lane): lane is AblGroupLane => Boolean(lane),
|
|
|
|
|
);
|
|
|
|
|
const result: AblData = {
|
2026-07-13 04:39:10 +02:00
|
|
|
tracks,
|
|
|
|
|
scenes: ablScenes,
|
2026-07-13 04:57:31 +02:00
|
|
|
groupLanes,
|
2026-07-14 09:37:36 +02:00
|
|
|
arrangementSections,
|
|
|
|
|
};
|
|
|
|
|
Sentry.setContext('abletonData', result);
|
|
|
|
|
return result;
|
2026-07-13 04:39:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default abletonTransformer;
|