From 4a724852081b61c9778b3402f176bd52a3b860ca Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Jul 2026 04:48:19 +0200 Subject: [PATCH] Update Ableton exporter builders to support versioning and improve color handling --- src/lib/exporters/ableton/builders.ts | 109 ++++++++++++++++++++------ 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index ea96969..74afb0e 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -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 { - const midiClipTemplate = await loadTemplate('midiClip'); + const midiClipTemplate = await loadTemplate('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('simpler'); +async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVersion) { + const simplerTemplate = await loadTemplate('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('drumRack'), - loadTemplate('drumBranch'), + loadTemplate('drumRack', abletonVersion), + loadTemplate('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 { - const midiTrackTemplate = await loadTemplate('midiTrack'); + const abletonVersion = getAbletonVersion(exporterParams); + const midiTrackTemplate = await loadTemplate('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('trackSendHolder'); + const trackSendHolderTemplate = await loadTemplate( + '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('groupTrack'); + const abletonVersion = getAbletonVersion(exporterParams); + const groupTrackTemplate = await loadTemplate('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('trackSendHolder'); + const trackSendHolderTemplate = await loadTemplate( + '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('scene'); +async function buildScenes( + scenes: AblScene[], + settings: ProjectSettings, + maxScenes: number, + abletonVersion: AbletonVersion, +) { + const sceneTemplate = await loadTemplate('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('returnTrack'); +async function buildReturnTrack( + projectData: ProjectRawData, + trackId: number = 0, + abletonVersion: AbletonVersion = '11', +) { + const returnTrackTemplate = await loadTemplate('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('effectReverb');