feat(ableton): Improve drum rack handling and note mapping
This commit is contained in:
@@ -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`, {
|
||||
|
||||
Reference in New Issue
Block a user