From a370f001506ff82c3ee9acbabbe5fda790fbddb6 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Jul 2026 05:00:43 +0200 Subject: [PATCH] Improve group track and clip handling for Ableton export --- src/lib/exporters/ableton/builders.ts | 88 ++++++++++++++++++++------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index 62e08b4..9a7df93 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -515,6 +515,7 @@ async function buildDrumRackDevice(koTrack: AblTrack, abletonVersion: AbletonVer async function buildTrack( koTrack: AblTrack, + groupLane: AblGroupLane | undefined, maxScenes: number, trackGroupId: number, exporterParams: ExporterParams, @@ -532,19 +533,17 @@ async function buildTrack( midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip = []; midiTrack.TrackGroupId['@Value'] = trackGroupId; midiTrack.DeviceChain.DeviceChain.Devices['#'] = []; + midiTrack.DeviceChain.Mixer.Sends = {}; midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume; - if (koTrack.faderParams[FaderParam.PAN] !== -1) { + if (!exporterParams.groupTracks && koTrack.faderParams[FaderParam.PAN] !== -1) { midiTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = (koTrack.faderParams[FaderParam.PAN] - 0.5) * 2; } - if (koTrack.faderParams[FaderParam.LVL] !== -1) { - // setting volume to the minimum of track volume and fader param - midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = Math.min( - koTrack.volume, - koTrack.faderParams[FaderParam.LVL], - ); + if (!exporterParams.groupTracks && koTrack.faderParams[FaderParam.LVL] !== -1) { + midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = + koTrack.volume * koTrack.faderParams[FaderParam.LVL]; } if (koTrack.drumRack && exporterParams.includeArchivedSamples) { @@ -566,6 +565,28 @@ async function buildTrack( }); }); + if (exporterParams.groupTracks) { + midiTrack.DeviceChain.AudioOutputRouting.Target['@Value'] = 'AudioOut/GroupTrack'; + midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group'; + } + + if (exporterParams.sendEffects && (!exporterParams.groupTracks || exporterParams.clips)) { + const trackSendHolderTemplate = await loadTemplate( + 'trackSendHolder', + abletonVersion, + ); + const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder); + trackSendHolder['@Id'] = 0; + trackSendHolder.Send.Manual['@Value'] = koTrack.faderParams[FaderParam.FX]; + midiTrack.DeviceChain.Mixer.Sends.TrackSendHolder = trackSendHolder; + } + + fixIds(midiTrack); + const automationTargets = trackAutomationTargets( + midiTrack, + !exporterParams.groupTracks && !exporterParams.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 at least 8 slots to avoid Ableton crashes @@ -608,6 +629,8 @@ async function buildTrack( getColorValue(midiTrack), false, abletonVersion, + automationTargets, + koTrack, ); midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push( @@ -615,13 +638,39 @@ async function buildTrack( ); } } else { - for (const koClip of koTrack.lane.clips.values()) { + const sessionClips = [...koTrack.lane.clips]; + groupLane?.patterns.forEach((pattern) => { + const hasClip = sessionClips.some((clip) => clip.sceneIndex === pattern.sceneIndex); + const hasSupportedAutomation = pattern.faderAutomation.some( + (point) => + point.parameter !== FaderParam.TIM && + point.parameter !== FaderParam.VEL && + automationTargets[point.parameter]?.some((target) => target.session !== undefined), + ); + if (!hasClip && hasSupportedAutomation) { + sessionClips.push({ + notes: [], + bars: pattern.bars, + offset: pattern.offset, + sceneBars: pattern.sceneBars, + sceneIndex: pattern.sceneIndex, + sceneName: pattern.sceneName, + timeSignature: pattern.timeSignature, + faderParams: pattern.faderParams, + groupPattern: pattern, + }); + } + }); + + for (const koClip of sessionClips) { const midiClip = await buildMidiClip( koClip, 0, getColorValue(midiTrack), true, abletonVersion, + automationTargets, + koTrack, ); midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex] = { @@ -636,21 +685,16 @@ async function buildTrack( } } - if (exporterParams.groupTracks) { - midiTrack.DeviceChain.AudioOutputRouting.Target['@Value'] = 'AudioOut/GroupTrack'; - midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group'; - } - - if (exporterParams.sendEffects && !exporterParams.groupTracks) { - const trackSendHolderTemplate = await loadTemplate( - 'trackSendHolder', - abletonVersion, + if (!exporterParams.clips && groupLane) { + const envelopes = buildArrangementEnvelopes( + groupLane.patterns, + automationTargets, + koTrack, + false, ); - const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder); - - trackSendHolder.Send.Manual['@Value'] = koTrack.faderParams[FaderParam.FX]; - - midiTrack.DeviceChain.Mixer.Sends.TrackSendHolder = trackSendHolder; + midiTrack.AutomationEnvelopes.Envelopes = envelopes.length + ? ({ AutomationEnvelope: envelopes } as any) + : {}; } return { MidiTrack: midiTrack };