From 65578c51b22b4da2800db6bc42ad3834967fdf69 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Jul 2026 07:55:54 +0200 Subject: [PATCH] feat(ableton): Improve drum rack handling and note mapping --- src/lib/transformers/ableton.ts | 78 ++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/lib/transformers/ableton.ts b/src/lib/transformers/ableton.ts index 0182d51..8fee039 100644 --- a/src/lib/transformers/ableton.ts +++ b/src/lib/transformers/ableton.ts @@ -30,6 +30,7 @@ export type AblTrack = Omit & { 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 = {}; + const newClips = new Map(); 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`, {