Update Ableton exporter builders to support versioning and improve color handling

This commit is contained in:
2026-07-13 04:48:19 +02:00
parent cf2f804a8c
commit 4a72485208

View File

@@ -43,14 +43,30 @@ let _localTrackColor = -1;
let _localGroupTrackColor = -1;
const MIN_CLIP_LAUNCHER_SCENES = 8;
type AbletonVersion = '10' | '11';
function getAbletonVersion(exporterParams: ExporterParams): AbletonVersion {
return exporterParams.abletonVersion === '10' ? '10' : '11';
}
function setColorValue(node: any, value: number) {
const color = node.Color || node.ColorIndex;
color['@Value'] = value;
}
function getColorValue(node: any): number {
const color = node.Color || node.ColorIndex;
return Number(color['@Value']);
}
async function buildMidiClip(
koClip: AblClip,
clipIdx: number,
color: number,
clipForLauncher: boolean = false,
abletonVersion: AbletonVersion = '11',
): Promise<ALSMidiClipContent> {
const midiClipTemplate = await loadTemplate<ALSMidiClip>('midiClip');
const midiClipTemplate = await loadTemplate<ALSMidiClip>('midiClip', abletonVersion);
const midiClip = structuredClone(midiClipTemplate.MidiClip);
const beats = getQuarterNotesPerBar(
@@ -70,7 +86,7 @@ async function buildMidiClip(
midiClip.Loop.LoopEnd['@Value'] = koClip.bars * beats;
midiClip.Loop.HiddenLoopStart['@Value'] = 0;
midiClip.Loop.HiddenLoopEnd['@Value'] = koClip.bars * beats;
midiClip.Color['@Value'] = color;
setColorValue(midiClip, color);
midiClip.Name['@Value'] = koClip.sceneName;
midiClip.TimeSignature.TimeSignatures.RemoteableTimeSignature.Numerator['@Value'] =
koClip.timeSignature.numerator;
@@ -137,8 +153,8 @@ async function buildMidiClip(
return midiClip;
}
async function buildSimplerDevice(koTrack: AblTrack) {
const simplerTemplate = await loadTemplate<ALSSimpler>('simpler');
async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVersion) {
const simplerTemplate = await loadTemplate<ALSSimpler>('simpler', abletonVersion);
const device = structuredClone(simplerTemplate.OriginalSimpler);
device.LastPresetRef.Value = {};
@@ -288,10 +304,10 @@ async function buildSimplerDevice(koTrack: AblTrack) {
};
}
async function buildDrumRackDevice(koTrack: AblTrack) {
async function buildDrumRackDevice(koTrack: AblTrack, abletonVersion: AbletonVersion) {
const [drumRackTemplate, drumBranchTemplate] = await Promise.all([
loadTemplate<ALSDrumRack>('drumRack'),
loadTemplate<ALSDrumBranch>('drumBranch'),
loadTemplate<ALSDrumRack>('drumRack', abletonVersion),
loadTemplate<ALSDrumBranch>('drumBranch', abletonVersion),
]);
const device = structuredClone(drumRackTemplate.DrumGroupDevice);
@@ -319,7 +335,10 @@ async function buildDrumRackDevice(koTrack: AblTrack) {
drumBranch.BranchInfo.ReceivingNote['@Value'] = note;
drumBranch.BranchInfo.SendingNote['@Value'] = 60; // not sure if this matters
drumBranch.BranchInfo.ChokeGroup['@Value'] = subtrack.inChokeGroup ? 1 : 0;
drumBranch.DeviceChain.MidiToAudioDeviceChain.Devices = await buildSimplerDevice(subtrack);
drumBranch.DeviceChain.MidiToAudioDeviceChain.Devices = await buildSimplerDevice(
subtrack,
abletonVersion,
);
device.Branches.DrumBranch.push(drumBranch);
}
@@ -335,7 +354,8 @@ async function buildTrack(
trackGroupId: number,
exporterParams: ExporterParams,
): Promise<ALSMidiTrack> {
const midiTrackTemplate = await loadTemplate<ALSMidiTrack>('midiTrack');
const abletonVersion = getAbletonVersion(exporterParams);
const midiTrackTemplate = await loadTemplate<ALSMidiTrack>('midiTrack', abletonVersion);
const midiTrack = structuredClone(midiTrackTemplate.MidiTrack);
midiTrack['@Id'] = _localId++;
@@ -343,7 +363,7 @@ async function buildTrack(
? `${String(koTrack.soundId).padStart(3, '0')} ${koTrack.name}`
: koTrack.name;
midiTrack.Name.UserName['@Value'] = midiTrack.Name.EffectiveName['@Value'];
midiTrack.Color['@Value'] = 8 + _localTrackColor++; // started with 8th color in the Ableton palette, why not
setColorValue(midiTrack, 8 + _localTrackColor++);
midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip = [];
midiTrack.TrackGroupId['@Value'] = trackGroupId;
midiTrack.DeviceChain.DeviceChain.Devices['#'] = [];
@@ -363,11 +383,15 @@ async function buildTrack(
}
if (koTrack.drumRack && exporterParams.includeArchivedSamples) {
midiTrack.DeviceChain.DeviceChain.Devices['#'].push(await buildDrumRackDevice(koTrack));
midiTrack.DeviceChain.DeviceChain.Devices['#'].push(
await buildDrumRackDevice(koTrack, abletonVersion),
);
}
if (!koTrack.drumRack && koTrack.sampleName && exporterParams.includeArchivedSamples) {
midiTrack.DeviceChain.DeviceChain.Devices['#'].push(await buildSimplerDevice(koTrack));
midiTrack.DeviceChain.DeviceChain.Devices['#'].push(
await buildSimplerDevice(koTrack, abletonVersion),
);
}
// make sure id's are sequential in device chain
@@ -380,6 +404,9 @@ async function buildTrack(
// make sure each tracks has the same empty slots for clips
// must be equeal to GroupTrackSlot if this track is in a group
// must be at least 8 slots to avoid Ableton crashes
midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot = [];
midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot = [];
for (let sc = 0; sc < Math.max(MIN_CLIP_LAUNCHER_SCENES, maxScenes); sc++) {
midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[sc] = {
'@Id': sc,
@@ -410,7 +437,13 @@ async function buildTrack(
if (koTrack.lane) {
if (!exporterParams.clips) {
for (const [clipIdx, koClip] of koTrack.lane.clips.entries()) {
const midiClip = await buildMidiClip(koClip, clipIdx, midiTrack.Color['@Value']);
const midiClip = await buildMidiClip(
koClip,
clipIdx,
getColorValue(midiTrack),
false,
abletonVersion,
);
midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push(
midiClip,
@@ -418,7 +451,13 @@ async function buildTrack(
}
} else {
for (const koClip of koTrack.lane.clips.values()) {
const midiClip = await buildMidiClip(koClip, 0, midiTrack.Color['@Value'], true);
const midiClip = await buildMidiClip(
koClip,
0,
getColorValue(midiTrack),
true,
abletonVersion,
);
midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex] = {
...midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex],
@@ -438,7 +477,10 @@ async function buildTrack(
}
if (exporterParams.sendEffects && !exporterParams.groupTracks) {
const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>('trackSendHolder');
const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>(
'trackSendHolder',
abletonVersion,
);
const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder);
trackSendHolder.Send.Manual['@Value'] = koTrack.faderParams[FaderParam.FX];
@@ -454,13 +496,14 @@ async function buildGroupTrack(
exporterParams: ExporterParams,
maxScenes: number,
) {
const groupTrackTemplate = await loadTemplate<ALSGroupTrack>('groupTrack');
const abletonVersion = getAbletonVersion(exporterParams);
const groupTrackTemplate = await loadTemplate<ALSGroupTrack>('groupTrack', abletonVersion);
const groupTrack = structuredClone(groupTrackTemplate.GroupTrack);
groupTrack['@Id'] = _localGroupId++;
groupTrack.Name.EffectiveName['@Value'] = koTrack.group.toLocaleUpperCase();
groupTrack.Name.UserName['@Value'] = koTrack.group.toLocaleUpperCase();
groupTrack.Color['@Value'] = 5 + _localGroupTrackColor++;
setColorValue(groupTrack, 5 + _localGroupTrackColor++);
groupTrack.Slots.GroupTrackSlot = [];
// adding empty slots for clips (or Ableton will crash)
@@ -475,7 +518,10 @@ async function buildGroupTrack(
}
if (exporterParams.sendEffects && koTrack.faderParams[FaderParam.FX] !== -1) {
const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>('trackSendHolder');
const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>(
'trackSendHolder',
abletonVersion,
);
const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder);
trackSendHolder.Send.Manual['@Value'] = koTrack.faderParams[FaderParam.FX];
@@ -486,8 +532,13 @@ async function buildGroupTrack(
return { GroupTrack: groupTrack };
}
async function buildScenes(scenes: AblScene[], settings: ProjectSettings, maxScenes: number) {
const sceneTemplate = await loadTemplate<ALSScene>('scene');
async function buildScenes(
scenes: AblScene[],
settings: ProjectSettings,
maxScenes: number,
abletonVersion: AbletonVersion,
) {
const sceneTemplate = await loadTemplate<ALSScene>('scene', abletonVersion);
const result: ALSSceneContent[] = [];
const sceneCount = Math.max(MIN_CLIP_LAUNCHER_SCENES, maxScenes);
@@ -496,8 +547,12 @@ async function buildScenes(scenes: AblScene[], settings: ProjectSettings, maxSce
const sceneContent = structuredClone(sceneTemplate.Scene);
sceneContent['@Id'] = _localId++;
sceneContent.Name['@Value'] = scene?.name || '';
sceneContent.Tempo['@Value'] = settings.bpm;
if (abletonVersion === '10') {
sceneContent['@Value'] = scene?.name || ` ${index + 1}`;
} else {
sceneContent.Name['@Value'] = scene?.name || '';
sceneContent.Tempo['@Value'] = settings.bpm;
}
result.push(sceneContent);
}
@@ -528,14 +583,18 @@ async function buildTracks(tracks: AblTrack[], maxScenes: number, exporterParams
return result;
}
async function buildReturnTrack(projectData: ProjectRawData, trackId: number = 0) {
const returnTrackTemplate = await loadTemplate<ALSReturnTrack>('returnTrack');
async function buildReturnTrack(
projectData: ProjectRawData,
trackId: number = 0,
abletonVersion: AbletonVersion = '11',
) {
const returnTrackTemplate = await loadTemplate<ALSReturnTrack>('returnTrack', abletonVersion);
const returnTrack = structuredClone(returnTrackTemplate.ReturnTrack);
returnTrack['@Id'] = trackId;
returnTrack.Name.EffectiveName['@Value'] = EFFECTS[projectData.effects.effectType] || '';
returnTrack.Name.UserName['@Value'] = EFFECTS[projectData.effects.effectType] || '';
returnTrack.Color['@Value'] = 28;
setColorValue(returnTrack, 28);
if (projectData.effects.effectType === EffectType.Reverb) {
const effectReverbTemplate = await loadTemplate<ALSReverb>('effectReverb');