feat(ableton): Improve drum rack handling and note mapping

This commit is contained in:
2026-07-14 07:55:54 +02:00
parent aa515202ca
commit 65578c51b2

View File

@@ -30,6 +30,7 @@ export type AblTrack = Omit<Pad, 'file' | 'rawData' | 'midiChannel'> & {
samplePitch: number; samplePitch: number;
bpm: number; bpm: number;
drumRack: boolean; drumRack: boolean;
drumRackSendingNote?: number;
lane?: AblLane; lane?: AblLane;
tracks: AblTrack[]; tracks: AblTrack[];
faderParams: { [K in FaderParam]: number }; faderParams: { [K in FaderParam]: number };
@@ -86,6 +87,19 @@ export type AblScene = {
name: string; name: string;
}; };
export function getDrumRackMidiNote(padCode: PadCode) {
const padNumber = parseInt(padCode.slice(1), 10);
const row = 3 - Math.floor(padNumber / 3);
const col = padNumber % 3;
return 36 + row * 4 + col;
}
function getTrackNoteValues(track: AblTrack) {
return [
...new Set(track.lane?.clips.flatMap((clip) => clip.notes.map((note) => note.note)) || []),
];
}
function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams) { function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams) {
const { pads, scenes } = data; const { pads, scenes } = data;
const customSceneNames = exporterParams.customSceneNames || {}; const customSceneNames = exporterParams.customSceneNames || {};
@@ -277,9 +291,14 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams
); );
} }
// Helper function to create a drum rack track for a specific group
const createDrumRackTrack = (group: 'a' | 'b' | 'c' | 'd'): AblTrack | null => { const createDrumRackTrack = (group: 'a' | 'b' | 'c' | 'd'): AblTrack | null => {
const groupTracksForDrumRack = tracks.filter((t) => t.group === group); const groupTracksForDrumRack = tracks
.filter((track) => track.group === group && track.sampleName)
.filter((track) => getTrackNoteValues(track).length <= 1)
.map((track) => ({
...track,
drumRackSendingNote: getTrackNoteValues(track)[0] ?? track.rootNote,
}));
if (groupTracksForDrumRack.length === 0) { if (groupTracksForDrumRack.length === 0) {
return null; return null;
} }
@@ -294,8 +313,9 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams
samplePitch: 0, samplePitch: 0,
bpm: data.settings.bpm, bpm: data.settings.bpm,
drumRack: true, drumRack: true,
drumRackSendingNote: undefined,
soundId: 0, soundId: 0,
name: `Drums ${group.toUpperCase()}`, name: `${group.toUpperCase()} Pads`,
volume: 1, // 1 means 0dB volume: 1, // 1 means 0dB
attack: 0, attack: 0,
release: 0, release: 0,
@@ -317,71 +337,59 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams
timeSignature: data.scenesSettings.timeSignature, timeSignature: data.scenesSettings.timeSignature,
}; };
// We need to merge notes from all tracks in the group into one track const newClips = new Map<number, AblClip>();
const newClips: Record<string, AblClip> = {};
drumTrack.tracks drumTrack.tracks
.toSorted((a, b) => .toSorted((a, b) =>
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }), a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
) )
.forEach((track) => { .forEach((track) => {
const padNumber = parseInt(track.padCode.slice(1), 10); const drumPad = getDrumRackMidiNote(track.padCode);
// EP133 is 3x4 (top→bottom), Ableton Drum Rack is 4x4 (bottom→top)
// flip vertically: row 3 → row 0, row 0 → row 3
const row = 3 - Math.floor(padNumber / 3);
const col = padNumber % 3;
const drumPad = 36 + row * 4 + col; // remaping notes starting from C1 (36)
track.lane?.clips.forEach((clip) => { track.lane?.clips.forEach((clip) => {
if (!newClips[clip.sceneName]) { if (!newClips.has(clip.sceneIndex)) {
newClips[clip.sceneName] = structuredClone(clip); const newClip = structuredClone(clip);
newClips[clip.sceneName].notes = []; newClip.notes = [];
newClips[clip.sceneName].midiEvents = []; newClip.midiEvents = [];
newClips.set(clip.sceneIndex, newClip);
} }
newClips[clip.sceneName].notes = [ const newClip = newClips.get(clip.sceneIndex);
...newClips[clip.sceneName].notes, if (!newClip) {
return;
}
newClip.notes = [
...newClip.notes,
...clip.notes.map((n) => ({ ...clip.notes.map((n) => ({
...n, ...n,
note: drumPad, note: drumPad,
})), })),
]; ];
newClips[clip.sceneName].midiEvents = [ newClip.midiEvents = [...newClip.midiEvents, ...clip.midiEvents];
...newClips[clip.sceneName].midiEvents,
...clip.midiEvents,
];
}); });
}); });
drumTrack.lane = { drumTrack.lane = {
padCode: `${group}0` as PadCode, padCode: `${group}0` as PadCode,
clips: Object.values(newClips), clips: [...newClips.values()].sort((a, b) => a.sceneIndex - b.sceneIndex),
}; };
return drumTrack; return drumTrack;
}; };
// Process drum racks for each group that has the option enabled
const drumRackTracks: AblTrack[] = []; const drumRackTracks: AblTrack[] = [];
const groupsToProcess: Array<{ group: 'a' | 'b' | 'c' | 'd'; enabled: boolean }> = [ if (exporterParams.includeArchivedSamples) {
{ group: 'a', enabled: exporterParams.drumRackGroupA || false }, for (const group of ['a', 'b', 'c', 'd'] as const) {
{ group: 'b', enabled: exporterParams.drumRackGroupB || false },
{ group: 'c', enabled: exporterParams.drumRackGroupC || false },
{ group: 'd', enabled: exporterParams.drumRackGroupD || false },
];
for (const { group, enabled } of groupsToProcess) {
if (enabled) {
const drumTrack = createDrumRackTrack(group); const drumTrack = createDrumRackTrack(group);
if (drumTrack) { if (drumTrack) {
drumRackTracks.push(drumTrack); drumRackTracks.push(drumTrack);
// Remove tracks from this group from the main tracks array const rackPadCodes = new Set(drumTrack.tracks.map((track) => track.padCode));
tracks = tracks.filter((t) => t.group !== group); tracks = tracks.filter((track) => !rackPadCodes.has(track.padCode));
} }
} }
} }
// Insert drum rack tracks at the beginning, maintaining group order (A, B, C, D)
tracks.unshift(...drumRackTracks); tracks.unshift(...drumRackTracks);
Sentry.setContext(`abletonData`, { Sentry.setContext(`abletonData`, {