2026-07-13 04:58:33 +02:00
|
|
|
import { FaderParam } from '../../../types/types';
|
|
|
|
|
import { AblGroupPatternOccurrence, AblTrack } from '../../transformers/ableton';
|
2026-07-13 05:29:39 +02:00
|
|
|
import { koEnvRangeToSeconds } from './utils';
|
2026-07-13 04:58:33 +02:00
|
|
|
|
|
|
|
|
export type AbletonAutomationTarget = {
|
|
|
|
|
arrangement?: number;
|
|
|
|
|
session?: number;
|
2026-07-13 08:47:34 +02:00
|
|
|
maxValue?: number;
|
2026-07-13 08:47:53 +02:00
|
|
|
soundLength?: number;
|
2026-07-13 08:50:03 +02:00
|
|
|
invertValue?: boolean;
|
2026-07-13 08:54:03 +02:00
|
|
|
staticValue?: number;
|
2026-07-14 07:56:45 +02:00
|
|
|
valueOffset?: number;
|
2026-07-13 04:58:33 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-13 05:01:58 +02:00
|
|
|
export type AbletonAutomationTargets = Partial<Record<FaderParam, AbletonAutomationTarget[]>>;
|
2026-07-13 04:58:33 +02:00
|
|
|
|
|
|
|
|
type AutomationPoint = {
|
|
|
|
|
time: number;
|
|
|
|
|
value: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const SENTINEL_TIME = -63072000;
|
|
|
|
|
const BOUNDARY_EPSILON = 0.000001;
|
|
|
|
|
|
|
|
|
|
function defaultValue(parameter: FaderParam) {
|
|
|
|
|
switch (parameter) {
|
|
|
|
|
case FaderParam.LVL:
|
|
|
|
|
case FaderParam.LPF:
|
|
|
|
|
return 1;
|
|
|
|
|
case FaderParam.PTC:
|
|
|
|
|
case FaderParam.TIM:
|
|
|
|
|
case FaderParam.PAN:
|
|
|
|
|
case FaderParam.TUNE:
|
|
|
|
|
return 0.5;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertFaderValue(
|
|
|
|
|
parameter: FaderParam,
|
|
|
|
|
value: number,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
2026-07-13 08:47:34 +02:00
|
|
|
target?: AbletonAutomationTarget,
|
2026-07-13 04:58:33 +02:00
|
|
|
) {
|
2026-07-13 08:47:34 +02:00
|
|
|
let convertedValue: number;
|
2026-07-13 04:58:33 +02:00
|
|
|
switch (parameter) {
|
|
|
|
|
case FaderParam.LVL:
|
2026-07-13 05:12:11 +02:00
|
|
|
if (groupMixer) {
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = value;
|
|
|
|
|
} else {
|
|
|
|
|
convertedValue = track.volume * value;
|
2026-07-13 04:58:33 +02:00
|
|
|
}
|
2026-07-13 08:47:34 +02:00
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.PTC:
|
2026-07-14 07:56:45 +02:00
|
|
|
convertedValue = (target?.valueOffset ?? 0) + (value - 0.5) * 10;
|
2026-07-13 08:47:34 +02:00
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.LPF:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = 20 + value * 0.9 * 115;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.HPF:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = 20 + value * 0.7 * 115;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.FX:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = value;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.ATK:
|
2026-07-13 08:47:53 +02:00
|
|
|
convertedValue =
|
2026-07-13 08:50:03 +02:00
|
|
|
koEnvRangeToSeconds(
|
|
|
|
|
(target?.invertValue ? 1 - value : value) * 255,
|
|
|
|
|
target?.soundLength ?? track.soundLength,
|
|
|
|
|
) * 1000;
|
2026-07-13 08:47:34 +02:00
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.REL:
|
2026-07-13 08:47:53 +02:00
|
|
|
convertedValue =
|
2026-07-13 08:50:03 +02:00
|
|
|
koEnvRangeToSeconds(
|
|
|
|
|
(target?.invertValue ? 1 - value : value) * 255,
|
|
|
|
|
target?.soundLength ?? track.soundLength,
|
|
|
|
|
) * 1000;
|
2026-07-13 08:47:34 +02:00
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.PAN:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = (value - 0.5) * 2;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.TUNE:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
case FaderParam.MOD:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = value / 20;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
default:
|
2026-07-13 08:47:34 +02:00
|
|
|
convertedValue = value;
|
|
|
|
|
break;
|
2026-07-13 04:58:33 +02:00
|
|
|
}
|
2026-07-13 08:47:34 +02:00
|
|
|
|
|
|
|
|
return target?.maxValue === undefined
|
|
|
|
|
? convertedValue
|
|
|
|
|
: Math.min(target.maxValue, convertedValue);
|
2026-07-13 04:58:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getFaderValueAt(
|
|
|
|
|
occurrence: AblGroupPatternOccurrence | undefined,
|
|
|
|
|
parameter: FaderParam,
|
|
|
|
|
position: number,
|
|
|
|
|
) {
|
|
|
|
|
const staticValue = occurrence?.faderParams[parameter];
|
2026-07-13 05:28:18 +02:00
|
|
|
let value = staticValue === undefined || staticValue === -1 ? undefined : staticValue;
|
2026-07-13 04:58:33 +02:00
|
|
|
const points = (occurrence?.faderAutomation || [])
|
|
|
|
|
.filter((point) => point.parameter === parameter && point.position <= position)
|
|
|
|
|
.sort((a, b) => a.position - b.position);
|
|
|
|
|
|
|
|
|
|
if (points.length > 0) {
|
|
|
|
|
value = points[points.length - 1].value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getStaticValue(pattern: AblGroupPatternOccurrence, parameter: FaderParam) {
|
|
|
|
|
const value = pattern.faderParams[parameter];
|
|
|
|
|
return value === -1 ? defaultValue(parameter) : value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 08:54:03 +02:00
|
|
|
function getConvertedStaticValue(
|
|
|
|
|
pattern: AblGroupPatternOccurrence,
|
|
|
|
|
parameter: FaderParam,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
|
|
|
|
target: AbletonAutomationTarget,
|
|
|
|
|
) {
|
|
|
|
|
if (pattern.faderParams[parameter] === -1 && target.staticValue !== undefined) {
|
|
|
|
|
return target.staticValue;
|
|
|
|
|
}
|
2026-07-13 08:55:51 +02:00
|
|
|
return convertFaderValue(
|
|
|
|
|
parameter,
|
|
|
|
|
getStaticValue(pattern, parameter),
|
|
|
|
|
track,
|
|
|
|
|
groupMixer,
|
|
|
|
|
target,
|
|
|
|
|
);
|
2026-07-13 08:54:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-13 04:58:33 +02:00
|
|
|
function getArrangementPoints(
|
|
|
|
|
patterns: AblGroupPatternOccurrence[],
|
|
|
|
|
parameter: FaderParam,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
2026-07-13 08:47:34 +02:00
|
|
|
target: AbletonAutomationTarget,
|
2026-07-13 04:58:33 +02:00
|
|
|
) {
|
2026-07-14 09:38:22 +02:00
|
|
|
const orderedPatterns = patterns.toSorted((a, b) => {
|
|
|
|
|
const offsetDifference = a.offset - b.offset;
|
|
|
|
|
return offsetDifference || (a.occurrenceIndex ?? 0) - (b.occurrenceIndex ?? 0);
|
|
|
|
|
});
|
|
|
|
|
const hasAutomation = orderedPatterns.some((pattern) =>
|
|
|
|
|
pattern.faderAutomation.some((point) => point.parameter === parameter),
|
|
|
|
|
);
|
|
|
|
|
if (!hasAutomation) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 04:58:33 +02:00
|
|
|
const points: AutomationPoint[] = [];
|
2026-07-14 09:38:22 +02:00
|
|
|
let previousValue = orderedPatterns[0]
|
|
|
|
|
? getConvertedStaticValue(orderedPatterns[0], parameter, track, groupMixer, target)
|
2026-07-13 08:54:03 +02:00
|
|
|
: convertFaderValue(parameter, defaultValue(parameter), track, groupMixer, target);
|
2026-07-13 04:58:33 +02:00
|
|
|
|
2026-07-14 09:38:22 +02:00
|
|
|
orderedPatterns.forEach((pattern) => {
|
|
|
|
|
const patternTicks = Math.max(1, pattern.loopDuration * 96);
|
|
|
|
|
const occurrenceTicks = Math.max(1, pattern.duration * 96);
|
|
|
|
|
const startTime = pattern.offset;
|
2026-07-13 04:58:33 +02:00
|
|
|
const sourcePoints = pattern.faderAutomation
|
|
|
|
|
.filter((point) => point.parameter === parameter)
|
|
|
|
|
.sort((a, b) => a.position - b.position);
|
2026-07-13 08:54:03 +02:00
|
|
|
const staticValue = getConvertedStaticValue(pattern, parameter, track, groupMixer, target);
|
2026-07-13 04:58:33 +02:00
|
|
|
|
2026-07-14 09:38:22 +02:00
|
|
|
points.push({ time: Math.max(0, startTime - BOUNDARY_EPSILON), value: previousValue });
|
|
|
|
|
points.push({ time: startTime, value: staticValue });
|
|
|
|
|
previousValue = staticValue;
|
|
|
|
|
|
2026-07-13 04:58:33 +02:00
|
|
|
if (sourcePoints.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 09:38:22 +02:00
|
|
|
for (let loopOffset = 0; loopOffset < occurrenceTicks; loopOffset += patternTicks) {
|
2026-07-13 04:58:33 +02:00
|
|
|
const loopStart = startTime + loopOffset / 96;
|
2026-07-14 09:38:22 +02:00
|
|
|
if (loopOffset > 0) {
|
|
|
|
|
points.push({ time: Math.max(0, loopStart - BOUNDARY_EPSILON), value: previousValue });
|
|
|
|
|
points.push({ time: loopStart, value: staticValue });
|
|
|
|
|
previousValue = staticValue;
|
|
|
|
|
}
|
2026-07-13 04:58:33 +02:00
|
|
|
|
|
|
|
|
sourcePoints.forEach((point) => {
|
2026-07-14 09:38:22 +02:00
|
|
|
if (loopOffset + point.position >= occurrenceTicks) {
|
2026-07-13 04:58:33 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 08:47:34 +02:00
|
|
|
const value = convertFaderValue(parameter, point.value, track, groupMixer, target);
|
2026-07-13 04:58:33 +02:00
|
|
|
points.push({ time: startTime + (loopOffset + point.position) / 96, value });
|
|
|
|
|
previousValue = value;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return [{ time: SENTINEL_TIME, value: points[0]?.value ?? previousValue }, ...points];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getClipPoints(
|
|
|
|
|
pattern: AblGroupPatternOccurrence,
|
|
|
|
|
parameter: FaderParam,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
2026-07-13 08:47:34 +02:00
|
|
|
target: AbletonAutomationTarget,
|
2026-07-13 04:58:33 +02:00
|
|
|
) {
|
|
|
|
|
const sourcePoints = pattern.faderAutomation
|
|
|
|
|
.filter((point) => point.parameter === parameter)
|
|
|
|
|
.sort((a, b) => a.position - b.position);
|
|
|
|
|
|
2026-07-13 08:54:03 +02:00
|
|
|
const staticValue = getConvertedStaticValue(pattern, parameter, track, groupMixer, target);
|
2026-07-13 04:58:33 +02:00
|
|
|
return [
|
|
|
|
|
{ time: SENTINEL_TIME, value: staticValue },
|
|
|
|
|
{ time: 0, value: staticValue },
|
|
|
|
|
...sourcePoints.map((point) => ({
|
|
|
|
|
time: point.position / 96,
|
2026-07-13 08:47:34 +02:00
|
|
|
value: convertFaderValue(parameter, point.value, track, groupMixer, target),
|
2026-07-13 04:58:33 +02:00
|
|
|
})),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function floatEvents(points: AutomationPoint[]) {
|
|
|
|
|
return points.map((point, index) => ({
|
|
|
|
|
'@Id': index,
|
|
|
|
|
'@Time': point.time,
|
|
|
|
|
'@Value': point.value,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function automationContent(points: AutomationPoint[]) {
|
|
|
|
|
return {
|
|
|
|
|
Events: { FloatEvent: floatEvents(points) },
|
|
|
|
|
AutomationTransformViewState: {
|
|
|
|
|
IsTransformPending: { '@Value': 'false' },
|
|
|
|
|
TimeAndValueTransforms: {},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildArrangementEnvelopes(
|
|
|
|
|
patterns: AblGroupPatternOccurrence[],
|
|
|
|
|
targets: AbletonAutomationTargets,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
|
|
|
|
) {
|
|
|
|
|
const envelopes: any[] = [];
|
|
|
|
|
Object.entries(targets).forEach(([parameterString, targetList]) => {
|
|
|
|
|
const parameter = Number(parameterString) as FaderParam;
|
|
|
|
|
if (parameter === FaderParam.TIM || parameter === FaderParam.VEL) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
targetList?.forEach((target) => {
|
|
|
|
|
if (target.arrangement === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 08:47:34 +02:00
|
|
|
const points = getArrangementPoints(patterns, parameter, track, groupMixer, target);
|
|
|
|
|
if (points.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 04:58:33 +02:00
|
|
|
envelopes.push({
|
|
|
|
|
'@Id': envelopes.length,
|
|
|
|
|
EnvelopeTarget: { PointeeId: { '@Value': target.arrangement } },
|
|
|
|
|
Automation: automationContent(points),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return envelopes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildClipEnvelopes(
|
|
|
|
|
pattern: AblGroupPatternOccurrence | undefined,
|
|
|
|
|
targets: AbletonAutomationTargets,
|
|
|
|
|
track: AblTrack,
|
|
|
|
|
groupMixer: boolean,
|
|
|
|
|
) {
|
|
|
|
|
if (!pattern) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const envelopes: any[] = [];
|
|
|
|
|
Object.entries(targets).forEach(([parameterString, targetList]) => {
|
|
|
|
|
const parameter = Number(parameterString) as FaderParam;
|
|
|
|
|
if (parameter === FaderParam.TIM || parameter === FaderParam.VEL) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
targetList?.forEach((target) => {
|
|
|
|
|
if (target.session === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 08:47:34 +02:00
|
|
|
const points = getClipPoints(pattern, parameter, track, groupMixer, target);
|
|
|
|
|
if (points.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-13 04:58:33 +02:00
|
|
|
envelopes.push({
|
|
|
|
|
'@Id': envelopes.length,
|
|
|
|
|
EnvelopeTarget: { PointeeId: { '@Value': target.session } },
|
|
|
|
|
Automation: automationContent(points),
|
|
|
|
|
LoopSlot: { Value: {} },
|
|
|
|
|
ScrollerTimePreserver: {
|
|
|
|
|
LeftTime: { '@Value': 0 },
|
|
|
|
|
RightTime: { '@Value': 0 },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return envelopes;
|
|
|
|
|
}
|