Files
ep-133-project-export/src/lib/exporters/ableton/automation.ts

313 lines
8.9 KiB
TypeScript

import { FaderParam } from '../../../types/types';
import { AblGroupPatternOccurrence, AblTrack } from '../../transformers/ableton';
import { koEnvRangeToSeconds } from './utils';
export type AbletonAutomationTarget = {
arrangement?: number;
session?: number;
maxValue?: number;
soundLength?: number;
invertValue?: boolean;
staticValue?: number;
valueOffset?: number;
};
export type AbletonAutomationTargets = Partial<Record<FaderParam, AbletonAutomationTarget[]>>;
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,
target?: AbletonAutomationTarget,
) {
let convertedValue: number;
switch (parameter) {
case FaderParam.LVL:
if (groupMixer) {
convertedValue = value;
} else {
convertedValue = track.volume * value;
}
break;
case FaderParam.PTC:
convertedValue = (target?.valueOffset ?? 0) + (value - 0.5) * 10;
break;
case FaderParam.LPF:
convertedValue = 20 + value * 0.9 * 115;
break;
case FaderParam.HPF:
convertedValue = 20 + value * 0.7 * 115;
break;
case FaderParam.FX:
convertedValue = value;
break;
case FaderParam.ATK:
convertedValue =
koEnvRangeToSeconds(
(target?.invertValue ? 1 - value : value) * 255,
target?.soundLength ?? track.soundLength,
) * 1000;
break;
case FaderParam.REL:
convertedValue =
koEnvRangeToSeconds(
(target?.invertValue ? 1 - value : value) * 255,
target?.soundLength ?? track.soundLength,
) * 1000;
break;
case FaderParam.PAN:
convertedValue = (value - 0.5) * 2;
break;
case FaderParam.TUNE:
convertedValue = Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100;
break;
case FaderParam.MOD:
convertedValue = value / 20;
break;
default:
convertedValue = value;
break;
}
return target?.maxValue === undefined
? convertedValue
: Math.min(target.maxValue, convertedValue);
}
export function getFaderValueAt(
occurrence: AblGroupPatternOccurrence | undefined,
parameter: FaderParam,
position: number,
) {
const staticValue = occurrence?.faderParams[parameter];
let value = staticValue === undefined || staticValue === -1 ? undefined : staticValue;
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;
}
function getConvertedStaticValue(
pattern: AblGroupPatternOccurrence,
parameter: FaderParam,
track: AblTrack,
groupMixer: boolean,
target: AbletonAutomationTarget,
) {
if (pattern.faderParams[parameter] === -1 && target.staticValue !== undefined) {
return target.staticValue;
}
return convertFaderValue(
parameter,
getStaticValue(pattern, parameter),
track,
groupMixer,
target,
);
}
function getArrangementPoints(
patterns: AblGroupPatternOccurrence[],
parameter: FaderParam,
track: AblTrack,
groupMixer: boolean,
target: AbletonAutomationTarget,
) {
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 [];
}
const points: AutomationPoint[] = [];
let previousValue = orderedPatterns[0]
? getConvertedStaticValue(orderedPatterns[0], parameter, track, groupMixer, target)
: convertFaderValue(parameter, defaultValue(parameter), track, groupMixer, target);
orderedPatterns.forEach((pattern) => {
const patternTicks = Math.max(1, pattern.loopDuration * 96);
const occurrenceTicks = Math.max(1, pattern.duration * 96);
const startTime = pattern.offset;
const sourcePoints = pattern.faderAutomation
.filter((point) => point.parameter === parameter)
.sort((a, b) => a.position - b.position);
const staticValue = getConvertedStaticValue(pattern, parameter, track, groupMixer, target);
points.push({ time: Math.max(0, startTime - BOUNDARY_EPSILON), value: previousValue });
points.push({ time: startTime, value: staticValue });
previousValue = staticValue;
if (sourcePoints.length === 0) {
return;
}
for (let loopOffset = 0; loopOffset < occurrenceTicks; loopOffset += patternTicks) {
const loopStart = startTime + loopOffset / 96;
if (loopOffset > 0) {
points.push({ time: Math.max(0, loopStart - BOUNDARY_EPSILON), value: previousValue });
points.push({ time: loopStart, value: staticValue });
previousValue = staticValue;
}
sourcePoints.forEach((point) => {
if (loopOffset + point.position >= occurrenceTicks) {
return;
}
const value = convertFaderValue(parameter, point.value, track, groupMixer, target);
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,
target: AbletonAutomationTarget,
) {
const sourcePoints = pattern.faderAutomation
.filter((point) => point.parameter === parameter)
.sort((a, b) => a.position - b.position);
const staticValue = getConvertedStaticValue(pattern, parameter, track, groupMixer, target);
return [
{ time: SENTINEL_TIME, value: staticValue },
{ time: 0, value: staticValue },
...sourcePoints.map((point) => ({
time: point.position / 96,
value: convertFaderValue(parameter, point.value, track, groupMixer, target),
})),
];
}
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;
}
const points = getArrangementPoints(patterns, parameter, track, groupMixer, target);
if (points.length === 0) {
return;
}
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;
}
const points = getClipPoints(pattern, parameter, track, groupMixer, target);
if (points.length === 0) {
return;
}
envelopes.push({
'@Id': envelopes.length,
EnvelopeTarget: { PointeeId: { '@Value': target.session } },
Automation: automationContent(points),
LoopSlot: { Value: {} },
ScrollerTimePreserver: {
LeftTime: { '@Value': 0 },
RightTime: { '@Value': 0 },
},
});
});
});
return envelopes;
}