Add support for MIDI controller targets and clip envelopes in Ableton export

This commit is contained in:
2026-07-14 06:46:18 +02:00
parent 85c68268e8
commit 5233cae10b
2 changed files with 99 additions and 8 deletions

View File

@@ -29,6 +29,7 @@ import {
filterEffectValues,
reverbEffectValues,
} from './effects';
import { AbletonMidiControllerTargets, buildMidiClipEnvelopes } from './midi';
import { getSimplerPlaybackState } from './playback';
import { ALSDrumBranch } from './types/drumBranch';
import { ALSDrumRack } from './types/drumRack';
@@ -274,6 +275,7 @@ async function buildMidiClip(
clipForLauncher: boolean = false,
abletonVersion: AbletonVersion = '11',
automationTargets: AbletonAutomationTargets = {},
midiControllerTargets: AbletonMidiControllerTargets = {},
koTrack?: AblTrack,
): Promise<ALSMidiClipContent> {
const midiClipTemplate = await loadTemplate<ALSMidiClip>('midiClip', abletonVersion);
@@ -371,17 +373,21 @@ async function buildMidiClip(
midiClip.Notes.NoteIdGenerator.NextId['@Value'] = _noteId;
const clipEnvelopes =
abletonVersion === '10'
? buildMidiClipEnvelopes(koClip.midiEvents, midiControllerTargets)
: [];
if (clipForLauncher && koTrack) {
const clipEnvelopes = buildClipEnvelopes(
koClip.groupPattern,
automationTargets,
koTrack,
false,
clipEnvelopes.push(
...buildClipEnvelopes(koClip.groupPattern, automationTargets, koTrack, false),
);
}
clipEnvelopes.forEach((envelope, index) => {
envelope['@Id'] = index;
});
midiClip.Envelopes.Envelopes = clipEnvelopes.length
? ({ ClipEnvelope: clipEnvelopes } as any)
: {};
}
return midiClip;
}
@@ -780,6 +786,7 @@ async function buildTrack(
false,
abletonVersion,
automationTargets,
midiTrack.DeviceChain.MainSequencer.MidiControllers,
koTrack,
);
@@ -821,6 +828,7 @@ async function buildTrack(
true,
abletonVersion,
automationTargets,
midiTrack.DeviceChain.MainSequencer.MidiControllers,
koTrack,
);

View File

@@ -0,0 +1,83 @@
import type { AblMidiEvent } from '../../transformers/ableton';
export type AbletonMidiControllerTargets = Record<string, { '@Id': number }>;
type AutomationPoint = {
time: number;
value: number;
};
const CHANNEL_PRESSURE_TARGET = 129;
const SENTINEL_TIME = -63072000;
export function decodeChannelPressure(event: AblMidiEvent) {
if (
event.data.length !== 4 ||
event.data[0] !== 0xd0 ||
event.data[1] > 127 ||
event.data[2] !== 0xff ||
event.data[3] !== 0
) {
return undefined;
}
return event.data[1];
}
function floatEvents(points: AutomationPoint[]) {
return points.map((point, index) => ({
'@Id': index,
'@Time': point.time,
'@Value': point.value,
}));
}
export function buildMidiClipEnvelopes(
events: AblMidiEvent[],
controllerTargets: AbletonMidiControllerTargets,
) {
const target = controllerTargets[`ControllerTargets.${CHANNEL_PRESSURE_TARGET}`]?.['@Id'];
if (target === undefined) {
return [];
}
const points: AutomationPoint[] = [{ time: SENTINEL_TIME, value: 0 }];
let previousValue = 0;
let eventCount = 0;
events.forEach((event) => {
const value = decodeChannelPressure(event);
if (value === undefined) {
return;
}
const time = event.position / 96;
points.push({ time, value: previousValue });
points.push({ time, value });
previousValue = value;
eventCount++;
});
if (eventCount === 0) {
return [];
}
return [
{
'@Id': 0,
EnvelopeTarget: { PointeeId: { '@Value': target } },
Automation: {
Events: { FloatEvent: floatEvents(points) },
AutomationTransformViewState: {
IsTransformPending: { '@Value': 'false' },
TimeAndValueTransforms: {},
},
},
LoopSlot: { Value: {} },
ScrollerTimePreserver: {
LeftTime: { '@Value': 0 },
RightTime: { '@Value': 0 },
},
},
];
}