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;
bpm: number;
drumRack: boolean;
drumRackSendingNote?: number;
lane?: AblLane;
tracks: AblTrack[];
faderParams: { [K in FaderParam]: number };
@@ -86,6 +87,19 @@ export type AblScene = {
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) {
const { pads, scenes } = data;
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 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) {
return null;
}
@@ -294,8 +313,9 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams
samplePitch: 0,
bpm: data.settings.bpm,
drumRack: true,
drumRackSendingNote: undefined,
soundId: 0,
name: `Drums ${group.toUpperCase()}`,
name: `${group.toUpperCase()} Pads`,
volume: 1, // 1 means 0dB
attack: 0,
release: 0,
@@ -317,71 +337,59 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams
timeSignature: data.scenesSettings.timeSignature,
};
// We need to merge notes from all tracks in the group into one track
const newClips: Record<string, AblClip> = {};
const newClips = new Map<number, AblClip>();
drumTrack.tracks
.toSorted((a, b) =>
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
)
.forEach((track) => {
const padNumber = parseInt(track.padCode.slice(1), 10);
// 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)
const drumPad = getDrumRackMidiNote(track.padCode);
track.lane?.clips.forEach((clip) => {
if (!newClips[clip.sceneName]) {
newClips[clip.sceneName] = structuredClone(clip);
newClips[clip.sceneName].notes = [];
newClips[clip.sceneName].midiEvents = [];
if (!newClips.has(clip.sceneIndex)) {
const newClip = structuredClone(clip);
newClip.notes = [];
newClip.midiEvents = [];
newClips.set(clip.sceneIndex, newClip);
}
newClips[clip.sceneName].notes = [
...newClips[clip.sceneName].notes,
const newClip = newClips.get(clip.sceneIndex);
if (!newClip) {
return;
}
newClip.notes = [
...newClip.notes,
...clip.notes.map((n) => ({
...n,
note: drumPad,
})),
];
newClips[clip.sceneName].midiEvents = [
...newClips[clip.sceneName].midiEvents,
...clip.midiEvents,
];
newClip.midiEvents = [...newClip.midiEvents, ...clip.midiEvents];
});
});
drumTrack.lane = {
padCode: `${group}0` as PadCode,
clips: Object.values(newClips),
clips: [...newClips.values()].sort((a, b) => a.sceneIndex - b.sceneIndex),
};
return drumTrack;
};
// Process drum racks for each group that has the option enabled
const drumRackTracks: AblTrack[] = [];
const groupsToProcess: Array<{ group: 'a' | 'b' | 'c' | 'd'; enabled: boolean }> = [
{ group: 'a', enabled: exporterParams.drumRackGroupA || false },
{ 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) {
if (exporterParams.includeArchivedSamples) {
for (const group of ['a', 'b', 'c', 'd'] as const) {
const drumTrack = createDrumRackTrack(group);
if (drumTrack) {
drumRackTracks.push(drumTrack);
// Remove tracks from this group from the main tracks array
tracks = tracks.filter((t) => t.group !== group);
const rackPadCodes = new Set(drumTrack.tracks.map((track) => track.padCode));
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);
Sentry.setContext(`abletonData`, {