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