Update Ableton exporter to use scenes array instead of maxScenes count

This commit is contained in:
2026-07-14 09:40:28 +02:00
parent b82b23a8ee
commit 18d7095dd2

View File

@@ -671,7 +671,7 @@ async function buildDrumRackDevice(koTrack: AblTrack, abletonVersion: AbletonVer
async function buildTrack( async function buildTrack(
koTrack: AblTrack, koTrack: AblTrack,
groupLane: AblGroupLane | undefined, groupLane: AblGroupLane | undefined,
maxScenes: number, scenes: AblScene[],
trackGroupId: number, trackGroupId: number,
exporterParams: ExporterParams, exporterParams: ExporterParams,
): Promise<ALSMidiTrack> { ): Promise<ALSMidiTrack> {
@@ -691,14 +691,12 @@ async function buildTrack(
midiTrack.DeviceChain.Mixer.Sends = {} as any; midiTrack.DeviceChain.Mixer.Sends = {} as any;
midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume; midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume;
const useChildGroupMixer = !exporterParams.groupTracks || exporterParams.clips; if (koTrack.faderParams[FaderParam.PAN] !== -1) {
if (useChildGroupMixer && koTrack.faderParams[FaderParam.PAN] !== -1) {
midiTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = midiTrack.DeviceChain.Mixer.Pan.Manual['@Value'] =
(koTrack.faderParams[FaderParam.PAN] - 0.5) * 2; (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'] = midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] =
koTrack.volume * koTrack.faderParams[FaderParam.LVL]; koTrack.volume * koTrack.faderParams[FaderParam.LVL];
} }
@@ -719,7 +717,10 @@ async function buildTrack(
for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) { for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) {
const staticValue = koTrack.faderParams[parameter]; const staticValue = koTrack.faderParams[parameter];
const defaultValue = parameter === FaderParam.LPF ? 1 : 0; 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), pattern.faderAutomation.some((point) => point.parameter === parameter),
); );
if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) { if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) {
@@ -742,7 +743,7 @@ async function buildTrack(
midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group'; midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group';
} }
if (exporterParams.sendEffects && (!exporterParams.groupTracks || exporterParams.clips)) { if (exporterParams.sendEffects) {
const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>( const trackSendHolderTemplate = await loadTemplate<ALSTrackSendHolder>(
'trackSendHolder', 'trackSendHolder',
abletonVersion, abletonVersion,
@@ -754,11 +755,7 @@ async function buildTrack(
} }
fixIds(midiTrack); fixIds(midiTrack);
const automationTargets = trackAutomationTargets( const automationTargets = trackAutomationTargets(midiTrack, true, koTrack);
midiTrack,
!exporterParams.groupTracks && !exporterParams.clips,
koTrack,
);
// 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
@@ -766,7 +763,9 @@ async function buildTrack(
midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot = []; midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot = [];
midiTrack.DeviceChain.FreezeSequencer.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] = { midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[sc] = {
'@Id': sc, '@Id': sc,
LomId: { LomId: {
@@ -775,6 +774,9 @@ async function buildTrack(
ClipSlot: { ClipSlot: {
Value: {}, Value: {},
}, },
HasStop: {
'@Value': hasStop ? 'true' : 'false',
},
}; };
midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot[sc] = { midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot[sc] = {
'@Id': sc, '@Id': sc,
@@ -793,9 +795,7 @@ async function buildTrack(
}; };
} }
if (koTrack.lane) { for (const [clipIdx, koClip] of (koTrack.lane?.arrangementClips || []).entries()) {
if (!exporterParams.clips) {
for (const [clipIdx, koClip] of koTrack.lane.clips.entries()) {
const midiClip = await buildMidiClip( const midiClip = await buildMidiClip(
koClip, koClip,
clipIdx, clipIdx,
@@ -806,28 +806,22 @@ async function buildTrack(
midiTrack.DeviceChain.MainSequencer.MidiControllers, midiTrack.DeviceChain.MainSequencer.MidiControllers,
koTrack, koTrack,
); );
midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push( midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push(
midiClip, midiClip,
); );
} }
} else {
const sessionClips = [...koTrack.lane.clips]; const sessionClips = [...(koTrack.lane?.sessionClips || [])];
groupLane?.patterns.forEach((pattern) => { groupLane?.sessionPatterns.forEach((pattern) => {
const hasClip = sessionClips.some((clip) => clip.sceneIndex === pattern.sceneIndex); const hasClip = sessionClips.some((clip) => clip.sceneIndex === pattern.sceneIndex);
const hasSupportedAutomation = pattern.faderAutomation.some( if (!hasClip && pattern.sceneIndex !== undefined) {
(point) =>
point.parameter !== FaderParam.TIM &&
point.parameter !== FaderParam.VEL &&
automationTargets[point.parameter]?.some((target) => target.session !== undefined),
);
if (!hasClip && hasSupportedAutomation) {
sessionClips.push({ sessionClips.push({
notes: [], notes: [],
midiEvents: [], midiEvents: [],
bars: pattern.bars, bars: pattern.bars,
offset: pattern.offset, offset: 0,
sceneBars: pattern.sceneBars, duration: pattern.duration,
loopDuration: pattern.loopDuration,
sceneIndex: pattern.sceneIndex, sceneIndex: pattern.sceneIndex,
sceneName: pattern.sceneName, sceneName: pattern.sceneName,
timeSignature: pattern.timeSignature, timeSignature: pattern.timeSignature,
@@ -838,6 +832,9 @@ async function buildTrack(
}); });
for (const koClip of sessionClips) { for (const koClip of sessionClips) {
if (koClip.sceneIndex === undefined) {
continue;
}
const midiClip = await buildMidiClip( const midiClip = await buildMidiClip(
koClip, koClip,
0, 0,
@@ -848,7 +845,6 @@ async function buildTrack(
midiTrack.DeviceChain.MainSequencer.MidiControllers, midiTrack.DeviceChain.MainSequencer.MidiControllers,
koTrack, koTrack,
); );
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],
ClipSlot: { ClipSlot: {
@@ -858,12 +854,10 @@ async function buildTrack(
}, },
}; };
} }
}
}
if (!exporterParams.clips && groupLane) { if (groupLane) {
const envelopes = buildArrangementEnvelopes( const envelopes = buildArrangementEnvelopes(
groupLane.patterns, groupLane.arrangementPatterns,
automationTargets, automationTargets,
koTrack, koTrack,
false, false,
@@ -878,7 +872,6 @@ async function buildTrack(
async function buildGroupTrack( async function buildGroupTrack(
koTrack: AblTrack, koTrack: AblTrack,
groupLane: AblGroupLane | undefined,
exporterParams: ExporterParams, exporterParams: ExporterParams,
maxScenes: number, maxScenes: number,
) { ) {
@@ -892,19 +885,8 @@ async function buildGroupTrack(
setColorValue(groupTrack, 5 + _localGroupTrackColor++); setColorValue(groupTrack, 5 + _localGroupTrackColor++);
groupTrack.Slots.GroupTrackSlot = []; groupTrack.Slots.GroupTrackSlot = [];
groupTrack.DeviceChain.Mixer.Sends = {} as any; 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.Volume.Manual['@Value'] = 1;
groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = 0; groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = 0;
}
// adding empty slots for clips (or Ableton will crash) // adding empty slots for clips (or Ableton will crash)
// must be at least 8 slots to avoid Ableton crashes // 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<ALSTrackSendHolder>(
'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); 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 }; return { GroupTrack: groupTrack };
} }
@@ -966,10 +919,22 @@ async function buildScenes(
sceneContent['@Id'] = _localId++; sceneContent['@Id'] = _localId++;
if (abletonVersion === '10') { 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 { } else {
sceneContent.Name['@Value'] = scene?.name || ''; sceneContent.Name['@Value'] = scene?.name || '';
sceneContent.Tempo['@Value'] = settings.bpm; 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); result.push(sceneContent);
@@ -981,7 +946,7 @@ async function buildScenes(
async function buildTracks( async function buildTracks(
tracks: AblTrack[], tracks: AblTrack[],
groupLanes: AblGroupLane[], groupLanes: AblGroupLane[],
maxScenes: number, scenes: AblScene[],
exporterParams: ExporterParams, exporterParams: ExporterParams,
) { ) {
const result = []; const result = [];
@@ -994,12 +959,12 @@ async function buildTracks(
const groupLane = groupLanes.find((lane) => lane.group === koTrack.group); const groupLane = groupLanes.find((lane) => lane.group === koTrack.group);
if (exporterParams.groupTracks && koTrack.group !== currentGroup[0]) { if (exporterParams.groupTracks && koTrack.group !== currentGroup[0]) {
currentGroup = koTrack.group; currentGroup = koTrack.group;
const groupTrack = await buildGroupTrack(koTrack, groupLane, exporterParams, maxScenes); const groupTrack = await buildGroupTrack(koTrack, exporterParams, scenes.length);
trackGroupId = groupTrack.GroupTrack['@Id']; trackGroupId = groupTrack.GroupTrack['@Id'];
result.push(groupTrack); result.push(groupTrack);
} }
const midiTrack = await buildTrack(koTrack, groupLane, maxScenes, trackGroupId, exporterParams); const midiTrack = await buildTrack(koTrack, groupLane, scenes, trackGroupId, exporterParams);
result.push(midiTrack); result.push(midiTrack);
} }