From a5b61b94430fafbeb97e4316896b8060b604be66 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Jul 2026 08:47:34 +0200 Subject: [PATCH] Improve fader value conversion and support target max value in Ableton exports --- src/lib/exporters/ableton/automation.ts | 68 +++++++++++++++++-------- src/lib/exporters/ableton/builders.ts | 26 ++++++++-- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/lib/exporters/ableton/automation.ts b/src/lib/exporters/ableton/automation.ts index 7390dfe..413fef0 100644 --- a/src/lib/exporters/ableton/automation.ts +++ b/src/lib/exporters/ableton/automation.ts @@ -5,6 +5,7 @@ import { koEnvRangeToSeconds } from './utils'; export type AbletonAutomationTarget = { arrangement?: number; session?: number; + maxValue?: number; }; export type AbletonAutomationTargets = Partial>; @@ -37,34 +38,52 @@ export function convertFaderValue( value: number, track: AblTrack, groupMixer: boolean, + target?: AbletonAutomationTarget, ) { + let convertedValue: number; switch (parameter) { case FaderParam.LVL: if (groupMixer) { - return value; + convertedValue = value; + } else { + convertedValue = track.volume * value; } - return track.volume * value; + break; case FaderParam.PTC: - return (value - 0.5) * 10; + convertedValue = (value - 0.5) * 10; + break; case FaderParam.LPF: - return 20 + value * 0.9 * 115; + convertedValue = 20 + value * 0.9 * 115; + break; case FaderParam.HPF: - return 20 + value * 0.7 * 115; + convertedValue = 20 + value * 0.7 * 115; + break; case FaderParam.FX: - return value; + convertedValue = value; + break; case FaderParam.ATK: - return koEnvRangeToSeconds(value * 255, track.soundLength) * 1000; + convertedValue = koEnvRangeToSeconds(value * 255, track.soundLength) * 1000; + break; case FaderParam.REL: - return koEnvRangeToSeconds(value * 255, track.soundLength) * 1000; + convertedValue = koEnvRangeToSeconds(value * 255, track.soundLength) * 1000; + break; case FaderParam.PAN: - return (value - 0.5) * 2; + convertedValue = (value - 0.5) * 2; + break; case FaderParam.TUNE: - return Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100; + convertedValue = Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100; + break; case FaderParam.MOD: - return value / 20; + convertedValue = value / 20; + break; default: - return value; + convertedValue = value; + break; } + + return target?.maxValue === undefined + ? convertedValue + : Math.min(target.maxValue, convertedValue); } export function getFaderValueAt( @@ -95,6 +114,7 @@ function getArrangementPoints( parameter: FaderParam, track: AblTrack, groupMixer: boolean, + target: AbletonAutomationTarget, ) { const points: AutomationPoint[] = []; let hasAutomation = false; @@ -103,6 +123,7 @@ function getArrangementPoints( patterns[0] ? getStaticValue(patterns[0], parameter) : defaultValue(parameter), track, groupMixer, + target, ); patterns.forEach((pattern) => { @@ -118,6 +139,7 @@ function getArrangementPoints( getStaticValue(pattern, parameter), track, groupMixer, + target, ); if (sourcePoints.length === 0) { @@ -139,7 +161,7 @@ function getArrangementPoints( if (loopOffset + point.position >= sceneTicks) { return; } - const value = convertFaderValue(parameter, point.value, track, groupMixer); + const value = convertFaderValue(parameter, point.value, track, groupMixer, target); points.push({ time: startTime + (loopOffset + point.position) / 96, value }); previousValue = value; }); @@ -158,6 +180,7 @@ function getClipPoints( parameter: FaderParam, track: AblTrack, groupMixer: boolean, + target: AbletonAutomationTarget, ) { const sourcePoints = pattern.faderAutomation .filter((point) => point.parameter === parameter) @@ -171,13 +194,14 @@ function getClipPoints( getStaticValue(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), + value: convertFaderValue(parameter, point.value, track, groupMixer, target), })), ]; } @@ -212,14 +236,14 @@ export function buildArrangementEnvelopes( if (parameter === FaderParam.TIM || parameter === FaderParam.VEL) { return; } - const points = getArrangementPoints(patterns, parameter, track, groupMixer); - if (points.length === 0) { - 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 } }, @@ -246,14 +270,14 @@ export function buildClipEnvelopes( if (parameter === FaderParam.TIM || parameter === FaderParam.VEL) { return; } - const points = getClipPoints(pattern, parameter, track, groupMixer); - if (points.length === 0) { - 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 } }, diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index 20a16f9..0dee6f6 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -104,7 +104,7 @@ function setLive10SampleReference(fileRef: any, sampleName: string) { fileRef.LivePackId = { '@Value': '' }; } -function targetFor(parameter: any): { arrangement?: number; session?: number } { +function targetFor(parameter: any, maxValue?: number) { const automationTarget = parameter?.AutomationTarget?.['@Id'] === undefined ? undefined @@ -112,6 +112,7 @@ function targetFor(parameter: any): { arrangement?: number; session?: number } { return { arrangement: automationTarget, session: automationTarget, + maxValue, }; } @@ -186,10 +187,29 @@ function trackAutomationTargets(midiTrack: any, includeArrangementMixer: boolean } findSimplers(midiTrack.DeviceChain.DeviceChain.Devices).forEach((simpler) => { + const oneshot = Number(simpler.Globals.PlaybackMode['@Value']) === 1; addTarget(targets, FaderParam.PTC, targetFor(simpler.Pitch.TransposeKey)); addTarget(targets, FaderParam.TUNE, targetFor(simpler.Pitch.TransposeFine)); - addTarget(targets, FaderParam.ATK, targetFor(simpler.VolumeAndPan.Envelope.AttackTime)); - addTarget(targets, FaderParam.REL, targetFor(simpler.VolumeAndPan.Envelope.ReleaseTime)); + addTarget( + targets, + FaderParam.ATK, + targetFor( + oneshot + ? simpler.VolumeAndPan.OneShotEnvelope.FadeInTime + : simpler.VolumeAndPan.Envelope.AttackTime, + oneshot ? 2000 : undefined, + ), + ); + addTarget( + targets, + FaderParam.REL, + targetFor( + oneshot + ? simpler.VolumeAndPan.OneShotEnvelope.FadeOutTime + : simpler.VolumeAndPan.Envelope.ReleaseTime, + oneshot ? 2000 : undefined, + ), + ); addTarget(targets, FaderParam.MOD, targetFor(simpler.Pitch.PitchLfoAmount)); });