From 18d7095dd2fe5a8a869b439d91ae1c456ee81c1a Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Jul 2026 09:40:28 +0200 Subject: [PATCH] Update Ableton exporter to use scenes array instead of maxScenes count --- src/lib/exporters/ableton/builders.ts | 221 +++++++++++--------------- 1 file changed, 93 insertions(+), 128 deletions(-) diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index 2a38904..aa98a40 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -671,7 +671,7 @@ async function buildDrumRackDevice(koTrack: AblTrack, abletonVersion: AbletonVer async function buildTrack( koTrack: AblTrack, groupLane: AblGroupLane | undefined, - maxScenes: number, + scenes: AblScene[], trackGroupId: number, exporterParams: ExporterParams, ): Promise { @@ -691,14 +691,12 @@ async function buildTrack( midiTrack.DeviceChain.Mixer.Sends = {} as any; midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume; - const useChildGroupMixer = !exporterParams.groupTracks || exporterParams.clips; - - if (useChildGroupMixer && koTrack.faderParams[FaderParam.PAN] !== -1) { + if (koTrack.faderParams[FaderParam.PAN] !== -1) { midiTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = (koTrack.faderParams[FaderParam.PAN] - 0.5) * 2; } - if (useChildGroupMixer && koTrack.faderParams[FaderParam.LVL] !== -1) { + if (koTrack.faderParams[FaderParam.LVL] !== -1) { midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume * koTrack.faderParams[FaderParam.LVL]; } @@ -719,7 +717,10 @@ async function buildTrack( for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) { const staticValue = koTrack.faderParams[parameter]; const defaultValue = parameter === FaderParam.LPF ? 1 : 0; - const hasAutomation = groupLane?.patterns.some((pattern) => + const hasAutomation = [ + ...(groupLane?.sessionPatterns || []), + ...(groupLane?.arrangementPatterns || []), + ].some((pattern) => pattern.faderAutomation.some((point) => point.parameter === parameter), ); if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) { @@ -742,7 +743,7 @@ async function buildTrack( midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group'; } - if (exporterParams.sendEffects && (!exporterParams.groupTracks || exporterParams.clips)) { + if (exporterParams.sendEffects) { const trackSendHolderTemplate = await loadTemplate( 'trackSendHolder', abletonVersion, @@ -754,11 +755,7 @@ async function buildTrack( } fixIds(midiTrack); - const automationTargets = trackAutomationTargets( - midiTrack, - !exporterParams.groupTracks && !exporterParams.clips, - koTrack, - ); + const automationTargets = trackAutomationTargets(midiTrack, true, koTrack); // make sure each tracks has the same empty slots for clips // must be equeal to GroupTrackSlot if this track is in a group @@ -766,7 +763,9 @@ async function buildTrack( 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, scenes.length); sc++) { + const scene = scenes[sc]; + const hasStop = !scene || scene.affectedGroups.includes(koTrack.group); midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[sc] = { '@Id': sc, LomId: { @@ -775,6 +774,9 @@ async function buildTrack( ClipSlot: { Value: {}, }, + HasStop: { + '@Value': hasStop ? 'true' : 'false', + }, }; midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot[sc] = { '@Id': sc, @@ -793,77 +795,69 @@ async function buildTrack( }; } - if (koTrack.lane) { - if (!exporterParams.clips) { - for (const [clipIdx, koClip] of koTrack.lane.clips.entries()) { - const midiClip = await buildMidiClip( - koClip, - clipIdx, - getColorValue(midiTrack), - false, - abletonVersion, - automationTargets, - midiTrack.DeviceChain.MainSequencer.MidiControllers, - koTrack, - ); - - midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push( - midiClip, - ); - } - } else { - 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: [], - midiEvents: [], - 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, - midiTrack.DeviceChain.MainSequencer.MidiControllers, - koTrack, - ); - - midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex] = { - ...midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex], - ClipSlot: { - Value: { - MidiClip: midiClip, - }, - }, - }; - } - } + for (const [clipIdx, koClip] of (koTrack.lane?.arrangementClips || []).entries()) { + const midiClip = await buildMidiClip( + koClip, + clipIdx, + getColorValue(midiTrack), + false, + abletonVersion, + automationTargets, + midiTrack.DeviceChain.MainSequencer.MidiControllers, + koTrack, + ); + midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push( + midiClip, + ); } - if (!exporterParams.clips && groupLane) { + const sessionClips = [...(koTrack.lane?.sessionClips || [])]; + groupLane?.sessionPatterns.forEach((pattern) => { + const hasClip = sessionClips.some((clip) => clip.sceneIndex === pattern.sceneIndex); + if (!hasClip && pattern.sceneIndex !== undefined) { + sessionClips.push({ + notes: [], + midiEvents: [], + bars: pattern.bars, + offset: 0, + duration: pattern.duration, + loopDuration: pattern.loopDuration, + sceneIndex: pattern.sceneIndex, + sceneName: pattern.sceneName, + timeSignature: pattern.timeSignature, + faderParams: pattern.faderParams, + groupPattern: pattern, + }); + } + }); + + for (const koClip of sessionClips) { + if (koClip.sceneIndex === undefined) { + continue; + } + const midiClip = await buildMidiClip( + koClip, + 0, + getColorValue(midiTrack), + true, + abletonVersion, + automationTargets, + midiTrack.DeviceChain.MainSequencer.MidiControllers, + koTrack, + ); + midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex] = { + ...midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex], + ClipSlot: { + Value: { + MidiClip: midiClip, + }, + }, + }; + } + + if (groupLane) { const envelopes = buildArrangementEnvelopes( - groupLane.patterns, + groupLane.arrangementPatterns, automationTargets, koTrack, false, @@ -878,7 +872,6 @@ async function buildTrack( async function buildGroupTrack( koTrack: AblTrack, - groupLane: AblGroupLane | undefined, exporterParams: ExporterParams, maxScenes: number, ) { @@ -892,19 +885,8 @@ async function buildGroupTrack( setColorValue(groupTrack, 5 + _localGroupTrackColor++); groupTrack.Slots.GroupTrackSlot = []; groupTrack.DeviceChain.Mixer.Sends = {} as any; - - if (!exporterParams.clips) { - if (koTrack.faderParams[FaderParam.LVL] !== -1) { - groupTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.faderParams[FaderParam.LVL]; - } - if (koTrack.faderParams[FaderParam.PAN] !== -1) { - groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = - (koTrack.faderParams[FaderParam.PAN] - 0.5) * 2; - } - } else { - groupTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = 1; - groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = 0; - } + groupTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = 1; + groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = 0; // adding empty slots for clips (or Ableton will crash) // must be at least 8 slots to avoid Ableton crashes @@ -917,36 +899,7 @@ async function buildGroupTrack( }); } - if ( - exporterParams.sendEffects && - !exporterParams.clips && - koTrack.faderParams[FaderParam.FX] !== -1 - ) { - const trackSendHolderTemplate = await loadTemplate( - 'trackSendHolder', - abletonVersion, - ); - const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder); - - trackSendHolder['@Id'] = 0; - trackSendHolder.Send.Manual['@Value'] = koTrack.faderParams[FaderParam.FX]; - - groupTrack.DeviceChain.Mixer.Sends.TrackSendHolder = trackSendHolder; - } - fixIds(groupTrack); - if (!exporterParams.clips && groupLane) { - const envelopes = buildArrangementEnvelopes( - groupLane.patterns, - groupAutomationTargets(groupTrack), - koTrack, - true, - ); - groupTrack.AutomationEnvelopes.Envelopes = envelopes.length - ? ({ AutomationEnvelope: envelopes } as any) - : {}; - } - return { GroupTrack: groupTrack }; } @@ -966,10 +919,22 @@ async function buildScenes( sceneContent['@Id'] = _localId++; if (abletonVersion === '10') { - (sceneContent as any)['@Value'] = scene?.name || ` ${index + 1}`; + const meter = scene?.timeSignature; + (sceneContent as any)['@Value'] = scene + ? `${scene.name}${meter ? ` ${meter.numerator}/${meter.denominator}` : ''}` + : ` ${index + 1}`; } else { sceneContent.Name['@Value'] = scene?.name || ''; sceneContent.Tempo['@Value'] = settings.bpm; + if (scene?.timeSignature) { + sceneContent.TimeSignatureId['@Value'] = + TIME_SIGNATURES[ + `${scene.timeSignature.numerator}/${scene.timeSignature.denominator}` + ] || TIME_SIGNATURES['4/4']; + sceneContent.IsTimeSignatureEnabled['@Value'] = 'true'; + } else { + sceneContent.IsTimeSignatureEnabled['@Value'] = 'false'; + } } result.push(sceneContent); @@ -981,7 +946,7 @@ async function buildScenes( async function buildTracks( tracks: AblTrack[], groupLanes: AblGroupLane[], - maxScenes: number, + scenes: AblScene[], exporterParams: ExporterParams, ) { const result = []; @@ -994,12 +959,12 @@ async function buildTracks( const groupLane = groupLanes.find((lane) => lane.group === koTrack.group); if (exporterParams.groupTracks && koTrack.group !== currentGroup[0]) { currentGroup = koTrack.group; - const groupTrack = await buildGroupTrack(koTrack, groupLane, exporterParams, maxScenes); + const groupTrack = await buildGroupTrack(koTrack, exporterParams, scenes.length); trackGroupId = groupTrack.GroupTrack['@Id']; result.push(groupTrack); } - const midiTrack = await buildTrack(koTrack, groupLane, maxScenes, trackGroupId, exporterParams); + const midiTrack = await buildTrack(koTrack, groupLane, scenes, trackGroupId, exporterParams); result.push(midiTrack); }