From 0d7c5746771df6f15691efb58ab819d04502a6b1 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Jul 2026 11:24:34 +0200 Subject: [PATCH] Implement fader policies and pad sound state extraction for Ableton export --- src/lib/exporters/ableton/faderPolicies.ts | 126 +++++++++++++++++++++ src/lib/exporters/ableton/padSound.ts | 62 ++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/lib/exporters/ableton/faderPolicies.ts create mode 100644 src/lib/exporters/ableton/padSound.ts diff --git a/src/lib/exporters/ableton/faderPolicies.ts b/src/lib/exporters/ableton/faderPolicies.ts new file mode 100644 index 0000000..636bea0 --- /dev/null +++ b/src/lib/exporters/ableton/faderPolicies.ts @@ -0,0 +1,126 @@ +import { FaderParam } from '../../../types/types'; +import type { AblTrack } from '../../transformers/ableton'; + +export type FaderRenderStrategy = 'target' | 'note' | 'unsupported'; +export type FaderTargetScope = 'track' | 'pad'; +export type FaderSupportStatus = 'mapped' | 'research' | 'unsupported'; + +export type FaderConversionContext = { + track: AblTrack; + groupMixer?: boolean; + valueOffset?: number; +}; + +export type FaderPolicy = { + defaultValue: number; + renderStrategy: FaderRenderStrategy; + targetScope: FaderTargetScope; + status: FaderSupportStatus; + toLive: (value: number, context: FaderConversionContext) => number; +}; + +const identity = (value: number) => value; + +export const FADER_POLICIES = { + [FaderParam.LVL]: { + defaultValue: 1, + renderStrategy: 'target', + targetScope: 'track', + status: 'mapped', + toLive: (value, context) => + context.groupMixer ? value : context.track.volume * value, + }, + [FaderParam.PTC]: { + defaultValue: 0.5, + renderStrategy: 'target', + targetScope: 'pad', + status: 'research', + toLive: (value, context) => (context.valueOffset ?? 0) + (value - 0.5) * 10, + }, + [FaderParam.TIM]: { + defaultValue: 0.5, + renderStrategy: 'unsupported', + targetScope: 'pad', + status: 'unsupported', + toLive: identity, + }, + [FaderParam.LPF]: { + defaultValue: 1, + renderStrategy: 'target', + targetScope: 'track', + status: 'research', + toLive: (value) => 20 + value * 0.9 * 115, + }, + [FaderParam.HPF]: { + defaultValue: 0, + renderStrategy: 'target', + targetScope: 'track', + status: 'research', + toLive: (value) => 20 + value * 0.7 * 115, + }, + [FaderParam.FX]: { + defaultValue: 0, + renderStrategy: 'target', + targetScope: 'track', + status: 'mapped', + toLive: identity, + }, + [FaderParam.ATK]: { + defaultValue: 0, + renderStrategy: 'unsupported', + targetScope: 'pad', + status: 'research', + toLive: identity, + }, + [FaderParam.REL]: { + defaultValue: 0, + renderStrategy: 'unsupported', + targetScope: 'pad', + status: 'research', + toLive: identity, + }, + [FaderParam.PAN]: { + defaultValue: 0.5, + renderStrategy: 'target', + targetScope: 'track', + status: 'mapped', + toLive: (value) => (value - 0.5) * 2, + }, + [FaderParam.TUNE]: { + defaultValue: 0.5, + renderStrategy: 'target', + targetScope: 'pad', + status: 'research', + toLive: (value) => Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100, + }, + [FaderParam.VEL]: { + defaultValue: 0, + renderStrategy: 'note', + targetScope: 'track', + status: 'research', + toLive: identity, + }, + [FaderParam.MOD]: { + defaultValue: 0, + renderStrategy: 'target', + targetScope: 'pad', + status: 'research', + toLive: (value) => value / 20, + }, +} satisfies Record; + +export function getFaderPolicy(parameter: FaderParam) { + return FADER_POLICIES[parameter]; +} + +export function getFaderDefaultValue(parameter: FaderParam) { + return getFaderPolicy(parameter).defaultValue; +} + +export function convertGroupFaderValue( + parameter: FaderParam, + value: number, + context: FaderConversionContext, +) { + return getFaderPolicy(parameter).toLive(value, context); +} diff --git a/src/lib/exporters/ableton/padSound.ts b/src/lib/exporters/ableton/padSound.ts new file mode 100644 index 0000000..f279b5a --- /dev/null +++ b/src/lib/exporters/ableton/padSound.ts @@ -0,0 +1,62 @@ +import type { AblTrack } from '../../transformers/ableton'; +import { koEnvRangeToSeconds } from './utils'; + +export type PadSoundState = { + amp: number; + pitch: number; + playMode: AblTrack['playMode']; + pan: number; + trimStart: number; + trimEnd: number; + envelope: { + attack: number; + release: number; + }; + time: { + mode: AblTrack['timeStretch']; + bpm: number; + bars: number; + }; + rootNote: number; + soundLength: number; +}; + +export type PadEnvelopeState = { + classicAttackMs: number; + classicReleaseMs: number; + oneShotFadeInMs: number; + oneShotFadeOutMs: number; +}; + +export function getPadSoundState(track: AblTrack): PadSoundState { + return { + amp: track.volume, + pitch: track.pitch, + playMode: track.playMode, + pan: track.pan, + trimStart: track.trimLeft, + trimEnd: track.trimRight, + envelope: { + attack: track.attack, + release: track.release, + }, + time: { + mode: track.timeStretch, + bpm: track.timeStretchBpm, + bars: track.timeStretchBars, + }, + rootNote: track.rootNote, + soundLength: track.soundLength, + }; +} + +export function getPadEnvelopeState(sound: PadSoundState): PadEnvelopeState { + return { + classicAttackMs: + koEnvRangeToSeconds(sound.envelope.attack, sound.soundLength) * 1000, + classicReleaseMs: + koEnvRangeToSeconds(sound.envelope.release, sound.soundLength) * 1000, + oneShotFadeInMs: 0, + oneShotFadeOutMs: 5, + }; +}