import { create } from 'xmlbuilder2'; import { EffectType, ExporterParams, FaderParam, ProjectRawData, ProjectSettings, } from '../../../types/types'; import { EFFECTS } from '../../constants'; import abletonTransformer, { AblArrangementSection, AblClip, AblGroupLane, AblScene, AblTrack, getDrumRackMidiNote, } from '../../transformers/ableton'; import { AbletonAutomationTarget, AbletonAutomationTargets, buildArrangementEnvelopes, buildClipEnvelopes, getFaderValueAt, } from './automation'; import { chorusEffectValues, compressorEffectValues, delayEffectValues, distortionEffectValues, filterEffectValues, reverbEffectValues, } from './effects'; import { convertGroupFaderValue, getFaderDefaultValue } from './faderPolicies'; import { AbletonMidiControllerTargets, buildMidiClipEnvelopes } from './midi'; import { getPadEnvelopeState, getPadSoundState } from './padSound'; import { getSimplerPlaybackState } from './playback'; import { ALSDrumBranch } from './types/drumBranch'; import { ALSDrumRack } from './types/drumRack'; import { ALSChorus } from './types/effectChorus'; import { ALSCompressor } from './types/effectCompressor'; import { ALSDelay } from './types/effectDelay'; import { ALSDistortion } from './types/effectDistortion'; import { ALSFilter } from './types/effectFilter'; import { ALSReverb } from './types/effectReverb'; import { ALSGroupTrack } from './types/groupTrack'; import { ALSMidiClip, ALSMidiClipContent } from './types/midiClip'; import { ALSMidiTrack } from './types/midiTrack'; import { ALSProject } from './types/project'; import { ALSReturnTrack } from './types/returnTrack'; import { ALSScene, ALSSceneContent } from './types/scene'; import { ALSSimpler } from './types/simpler'; import { ALSTrackSendHolder } from './types/trackSendHolder'; import { fixIds, getId, gzipString, loadTemplate, resetIds, TIME_SIGNATURES, toNativePath, } from './utils'; let _localId = -1; let _localGroupId = -1; let _localTrackColor = -1; let _localGroupTrackColor = -1; const simplerPadTracks = new WeakMap(); const MIN_CLIP_LAUNCHER_SCENES = 8; type AbletonVersion = '10' | '11'; function getAbletonVersion(exporterParams: ExporterParams): AbletonVersion { return exporterParams.abletonVersion === '10' ? '10' : '11'; } function setColorValue(node: any, value: number) { const color = node.Color || node.ColorIndex; color['@Value'] = value; } function getColorValue(node: any): number { const color = node.Color || node.ColorIndex; return Number(color['@Value']); } function setLive10SampleReference(fileRef: any, sampleName: string) { const pathElements = ['Samples', 'Imported'].map((dir, id) => ({ '@Id': id, '@Dir': dir, })); fileRef.HasRelativePath = { '@Value': 'true' }; fileRef.RelativePathType = { '@Value': 3 }; fileRef.RelativePath = { RelativePathElement: pathElements }; fileRef.Name = { '@Value': sampleName }; fileRef.Type = { '@Value': 2 }; fileRef.RefersToFolder = { '@Value': 'false' }; fileRef.SearchHint = { PathHint: { RelativePathElement: pathElements }, FileSize: { '@Value': 0 }, Crc: { '@Value': 0 }, MaxCrcSize: { '@Value': 16384 }, HasExtendedInfo: { '@Value': 'false' }, }; fileRef.LivePackName = { '@Value': '' }; fileRef.LivePackId = { '@Value': '' }; } export function assignDeviceListIds(devices: Record[]) { devices.forEach((device, deviceIdx) => { Object.values(device).forEach((devContent: any) => { devContent['@Id'] = deviceIdx; }); }); } function targetFor(parameter: any, padTrack?: AblTrack): AbletonAutomationTarget { const automationTarget = parameter?.AutomationTarget?.['@Id'] === undefined ? undefined : Number(parameter.AutomationTarget['@Id']); return { arrangement: automationTarget, session: automationTarget, staticValue: parameter?.Manual?.['@Value'] === undefined ? undefined : Number(parameter.Manual['@Value']), padTrack, }; } function findSimplers(node: any, result: any[] = []): any[] { if (Array.isArray(node)) { node.forEach((item) => { findSimplers(item, result); }); return result; } if (!node || typeof node !== 'object') { return result; } if (node.OriginalSimpler) { result.push(node.OriginalSimpler); } Object.values(node).forEach((value) => { findSimplers(value, result); }); return result; } function findAutoFilters(node: any, result: any[] = []): any[] { if (Array.isArray(node)) { node.forEach((item) => { findAutoFilters(item, result); }); return result; } if (!node || typeof node !== 'object') { return result; } if (node.AutoFilter) { result.push(node.AutoFilter); } Object.values(node).forEach((value) => { findAutoFilters(value, result); }); return result; } function addTarget( targets: AbletonAutomationTargets, parameter: FaderParam, target: AbletonAutomationTarget, ) { if (target.arrangement === undefined && target.session === undefined) { return; } targets[parameter] = [...(targets[parameter] || []), target]; } function trackAutomationTargets( midiTrack: any, includeArrangementMixer: boolean, koTrack: AblTrack, ) { const targets: AbletonAutomationTargets = {}; const mixer = midiTrack.DeviceChain.Mixer; const volumeTarget = targetFor(mixer.Volume); const panTarget = targetFor(mixer.Pan); if (!includeArrangementMixer) { volumeTarget.arrangement = undefined; panTarget.arrangement = undefined; } addTarget(targets, FaderParam.LVL, volumeTarget); addTarget(targets, FaderParam.PAN, panTarget); const send = mixer.Sends?.TrackSendHolder?.Send; if (send) { const target = targetFor(send); if (!includeArrangementMixer) { target.arrangement = undefined; } addTarget(targets, FaderParam.FX, target); } findSimplers(midiTrack.DeviceChain.DeviceChain.Devices).forEach((simpler) => { const padTrack = simplerPadTracks.get(simpler) ?? koTrack; const pitchTarget = targetFor(simpler.Pitch.TransposeKey, padTrack); const staticPitchValue = padTrack.faderParams[FaderParam.PTC]; const staticPitchOffset = staticPitchValue === -1 ? 0 : Math.round((staticPitchValue - 0.5) * 10); pitchTarget.valueOffset = Number(simpler.Pitch.TransposeKey.Manual['@Value']) - staticPitchOffset; addTarget(targets, FaderParam.PTC, pitchTarget); addTarget(targets, FaderParam.TUNE, targetFor(simpler.Pitch.TransposeFine, padTrack)); addTarget(targets, FaderParam.MOD, targetFor(simpler.Pitch.PitchLfoAmount, padTrack)); }); findAutoFilters(midiTrack.DeviceChain.DeviceChain.Devices).forEach((filter) => { const parameter = Number(filter.FilterType.Manual['@Value']) === 1 ? FaderParam.HPF : FaderParam.LPF; addTarget(targets, parameter, targetFor(filter.Cutoff)); }); return targets; } async function buildMidiClip( koClip: AblClip, clipIdx: number, color: number, clipForLauncher: boolean = false, abletonVersion: AbletonVersion = '11', automationTargets: AbletonAutomationTargets = {}, midiControllerTargets: AbletonMidiControllerTargets = {}, koTrack?: AblTrack, ): Promise { const midiClipTemplate = await loadTemplate('midiClip', abletonVersion); const midiClip = structuredClone(midiClipTemplate.MidiClip); const time = koClip.offset; const start = time; const end = time + koClip.duration; midiClip['@Id'] = clipIdx; midiClip['@Time'] = time; midiClip.CurrentStart['@Value'] = start; midiClip.CurrentEnd['@Value'] = end; midiClip.Loop.LoopOn['@Value'] = 'true'; midiClip.Loop.LoopStart['@Value'] = 0; midiClip.Loop.LoopEnd['@Value'] = koClip.loopDuration; if (midiClip.Loop.OutMarker) { midiClip.Loop.OutMarker['@Value'] = koClip.loopDuration; } midiClip.Loop.HiddenLoopStart['@Value'] = 0; midiClip.Loop.HiddenLoopEnd['@Value'] = koClip.loopDuration; setColorValue(midiClip, color); midiClip.Name['@Value'] = koClip.sceneName; midiClip.TimeSignature.TimeSignatures.RemoteableTimeSignature.Numerator['@Value'] = koClip.timeSignature.numerator; midiClip.TimeSignature.TimeSignatures.RemoteableTimeSignature.Denominator['@Value'] = koClip.timeSignature.denominator; if (clipForLauncher) { midiClip['@Time'] = 0; midiClip.CurrentStart['@Value'] = 0; midiClip.CurrentEnd['@Value'] = koClip.loopDuration; } // ableton group notes const grouppedNotes: Record = koClip.notes.reduce( (acc, note) => { const group = note.note.toString(); acc[group] = acc[group] || []; acc[group].push(note); return acc; }, {} as Record, ); midiClip.Notes.KeyTracks.KeyTrack = []; let _noteId = 0; Object.entries(grouppedNotes).forEach(([noteValueStr, notes], groupIndex) => { const noteValue = parseInt(noteValueStr, 10); midiClip.Notes.KeyTracks.KeyTrack.push({ '@Id': groupIndex, Notes: { MidiNoteEvent: notes.map((noteData, noteIndex) => { let dur = noteData.duration / 96; const nextNote = notes[noteIndex + 1]; // making sure same notes are not overlapping if (nextNote && noteData.position / 96 + dur > nextNote.position / 96) { dur = nextNote.position / 96 - noteData.position / 96; } const automatedVelocity = koClip.groupPattern ? getFaderValueAt(koClip.groupPattern, FaderParam.VEL, noteData.position) : undefined; const velocity = Math.max( 1, Math.min( 127, automatedVelocity === undefined ? noteData.velocity : automatedVelocity * 127, ), ); return { '@Time': noteData.position / 96, '@Duration': dur, '@Velocity': velocity, '@OffVelocity': 64, '@NoteId': _noteId++, '@IsEnabled': 'true', }; }), }, MidiKey: { '@Value': noteValue, }, }); }); midiClip.Notes.NoteIdGenerator.NextId['@Value'] = _noteId; const clipEnvelopes = abletonVersion === '10' ? buildMidiClipEnvelopes(koClip.midiEvents, midiControllerTargets) : []; if (clipForLauncher && koTrack) { clipEnvelopes.push( ...buildClipEnvelopes(koClip.groupPattern, automationTargets, koTrack, false), ); } clipEnvelopes.forEach((envelope, index) => { envelope['@Id'] = index; }); midiClip.Envelopes.Envelopes = clipEnvelopes.length ? ({ ClipEnvelope: clipEnvelopes } as any) : {}; return midiClip; } async function buildSimplerDevice( koTrack: AblTrack, abletonVersion: AbletonVersion, automatedParameters: ReadonlySet = new Set(), ) { const simplerTemplate = await loadTemplate('simpler', abletonVersion); const device = structuredClone(simplerTemplate.OriginalSimpler); simplerPadTracks.set(device, koTrack); const padSound = getPadSoundState(koTrack); const padEnvelope = getPadEnvelopeState(padSound); const playbackState = getSimplerPlaybackState(padSound.playMode); const samplePart = device.Player.MultiSampleMap.SampleParts.MultiSamplePart; const loopModulators = device.Player.LoopModulators; const simplerFilter = device.Filter.Slot.Value.SimplerFilter; device.LastPresetRef.Value = {} as any; device.ShouldShowPresetName['@Value'] = 'false'; device.UserName['@Value'] = ''; loopModulators.SampleStart.Manual['@Value'] = 0; loopModulators.SampleLength.Manual['@Value'] = 1; loopModulators.LoopOn.Manual['@Value'] = 'false'; loopModulators.LoopLength.Manual['@Value'] = 1; loopModulators.LoopFade.Manual['@Value'] = 0; device.Filter.IsOn.Manual['@Value'] = 'false'; simplerFilter.Freq.Manual['@Value'] = 22000; simplerFilter.Res.Manual['@Value'] = 0; simplerFilter.Envelope.IsOn.Manual['@Value'] = 'false'; simplerFilter.Envelope.Amount.Manual['@Value'] = 0; simplerFilter.ModByLfo.Manual['@Value'] = 0; device.Lfo.IsOn.Manual['@Value'] = 'false'; device.Pitch.PitchLfoAmount.Manual['@Value'] = 0; device.VolumeAndPan.OneShotEnvelope.FadeInTime.Manual['@Value'] = padEnvelope.oneShotFadeInMs; device.VolumeAndPan.OneShotEnvelope.FadeOutTime.Manual['@Value'] = padEnvelope.oneShotFadeOutMs; device.VolumeAndPan.OneShotEnvelope.SustainMode.Manual['@Value'] = 0; device.VolumeAndPan.Envelope.AttackTime.Manual['@Value'] = padEnvelope.classicAttackMs; device.VolumeAndPan.Envelope.ReleaseTime.Manual['@Value'] = padEnvelope.classicReleaseMs; device.Globals.PlaybackMode['@Value'] = playbackState.playbackMode; device.Globals.NumVoices['@Value'] = playbackState.voices; device.Globals.RetriggerMode['@Value'] = playbackState.retrigger; device.Globals.PortamentoMode.Manual['@Value'] = playbackState.portamentoMode; device.Globals.PortamentoTime.Manual['@Value'] = playbackState.portamentoMode === 2 ? 0.1 : 50; const sampleRef = device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleRef; if (abletonVersion === '10') { setLive10SampleReference(sampleRef.FileRef, koTrack.sampleName); } else { sampleRef.FileRef.RelativePath['@Value'] = toNativePath( `Samples/Imported/${koTrack.sampleName}`, ); } if (sampleRef.LastModDate) { sampleRef.LastModDate['@Value'] = 0; } if (sampleRef.DefaultDuration) { sampleRef.DefaultDuration['@Value'] = Math.round(padSound.soundLength * koTrack.sampleRate); } if (sampleRef.DefaultSampleRate) { sampleRef.DefaultSampleRate['@Value'] = koTrack.sampleRate; } samplePart.Name['@Value'] = koTrack.name; samplePart.RootKey['@Value'] = padSound.rootNote; samplePart.SampleStart['@Value'] = padSound.trimStart; samplePart.SampleEnd['@Value'] = padSound.trimEnd; samplePart.SustainLoop.Start['@Value'] = padSound.trimStart; samplePart.SustainLoop.End['@Value'] = padSound.trimEnd; samplePart.SustainLoop.Mode['@Value'] = 0; samplePart.SustainLoop.Crossfade['@Value'] = 0; samplePart.ReleaseLoop.Start['@Value'] = 0; samplePart.ReleaseLoop.End['@Value'] = 0; samplePart.SampleWarpProperties.TimeSignature.TimeSignatures.RemoteableTimeSignature.Numerator[ '@Value' ] = koTrack.timeSignature.numerator; samplePart.SampleWarpProperties.TimeSignature.TimeSignatures.RemoteableTimeSignature.Denominator[ '@Value' ] = koTrack.timeSignature.denominator; // setting velocity scaling to 20% to match KO behavior device.VolumeAndPan.VolumeVelScale.Manual['@Value'] = 0.2; device.VolumeAndPan.Panorama.Manual['@Value'] = padSound.pan; let pitch = padSound.pitch || 0; // adding PITCH fader param if defined if (koTrack.faderParams[FaderParam.PTC] !== -1) { pitch += Math.round( convertGroupFaderValue(FaderParam.PTC, koTrack.faderParams[FaderParam.PTC], { track: koTrack, }), ); } if (koTrack.faderParams[FaderParam.TUNE] !== -1) { device.Pitch.TransposeFine.Manual['@Value'] = convertGroupFaderValue( FaderParam.TUNE, koTrack.faderParams[FaderParam.TUNE], { track: koTrack }, ); } device.Pitch.TransposeKey.Manual['@Value'] = pitch; // adding vibrato from MOD fader param if defined if (koTrack.faderParams[FaderParam.MOD] !== -1 || automatedParameters.has(FaderParam.MOD)) { device.Lfo.IsOn.Manual['@Value'] = 'true'; if (!device.Lfo.Slot.Value.SimplerLfo && abletonVersion === '10') { const simplerLfoTemplate = await loadTemplate('simplerLfo', abletonVersion); device.Lfo.Slot.Value.SimplerLfo = structuredClone(simplerLfoTemplate.SimplerLfo); } // KO's MOD wheel on 100% is roughly equal to 5% LFO pitch modulation device.Pitch.PitchLfoAmount.Manual['@Value'] = convertGroupFaderValue( FaderParam.MOD, Math.max(0, koTrack.faderParams[FaderParam.MOD]), { track: koTrack }, ); device.Lfo.Slot.Value.SimplerLfo.RateType.Manual['@Value'] = 1; // tempo synced 1/16 } // TODO: add bandpass filter if (padSound.time.mode === 'bars') { device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.IsWarped[ '@Value' ] = 'true'; device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.WarpMode[ '@Value' ] = 0; // warp in 'Beats' mode device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.WarpMarkers.WarpMarker = [ { '@Id': 0, '@SecTime': 0, '@BeatTime': 0, }, { '@Id': 1, '@SecTime': padSound.soundLength, '@BeatTime': padSound.time.bars * koTrack.timeSignature.numerator * (4 / koTrack.timeSignature.denominator), // convert bars to beats }, ]; } if (padSound.time.mode === 'bpm') { // calculating beat length using koTrack.timeStretchBpm const beatTime = padSound.soundLength / (60 / padSound.time.bpm); device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.IsWarped[ '@Value' ] = 'true'; device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.WarpMode[ '@Value' ] = 0; // warp in 'Beats' mode device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleWarpProperties.WarpMarkers.WarpMarker = [ { '@Id': 0, '@SecTime': 0, '@BeatTime': 0, }, { '@Id': 1, '@SecTime': padSound.soundLength, '@BeatTime': beatTime, }, ]; } if (padSound.time.mode === 'rev') { device.Player.Reverse.Manual['@Value'] = 'true'; } return { OriginalSimpler: device, }; } async function buildFaderFilterDevice( parameter: FaderParam.LPF | FaderParam.HPF, value: number, track: AblTrack, abletonVersion: AbletonVersion, ) { const filterTemplate = await loadTemplate('effectFilter', abletonVersion); const filter = structuredClone(filterTemplate.AutoFilter); const highPass = parameter === FaderParam.HPF; filter.UserName['@Value'] = highPass ? 'EP HPF' : 'EP LPF'; filter.FilterType.Manual['@Value'] = highPass ? 1 : 0; filter.LegacyFilterType.Manual['@Value'] = highPass ? 1 : 0; filter.Slope.Manual['@Value'] = highPass ? 'false' : 'true'; filter.Cutoff.Manual['@Value'] = convertGroupFaderValue(parameter, value, { track }); return { AutoFilter: filter }; } async function buildDrumRackDevice( koTrack: AblTrack, abletonVersion: AbletonVersion, automatedParameters: ReadonlySet, ) { const [drumRackTemplate, drumBranchTemplate] = await Promise.all([ loadTemplate('drumRack', abletonVersion), loadTemplate('drumBranch', abletonVersion), ]); const device = structuredClone(drumRackTemplate.DrumGroupDevice); device.Branches.DrumBranch = []; device.ReturnBranches = {}; device.LockId['@Value'] = 0; device.LockSeal['@Value'] = 0; for (const [idx, subtrack] of koTrack.tracks.entries()) { if (!subtrack.sampleName) { continue; } // EP133 is 3x4 (top→bottom), Ableton Drum Rack is 4x4 (bottom→top) // flip vertically: row 3 → row 0, row 0 → row 3 const drumPad = getDrumRackMidiNote(subtrack.padCode) - 36; const drumBranch = structuredClone(drumBranchTemplate.DrumBranch); const note = 92 - drumPad; // 92 is the number of slot for C1 in the drum rack and it goes down drumBranch['@Id'] = idx; drumBranch.IsSelected['@Value'] = device.Branches.DrumBranch.length === 0 ? 'true' : 'false'; drumBranch.Name.EffectiveName['@Value'] = subtrack.name; drumBranch.Name.UserName['@Value'] = subtrack.name; drumBranch.BranchInfo.ReceivingNote['@Value'] = note; drumBranch.BranchInfo.SendingNote['@Value'] = subtrack.drumRackSendingNote ?? subtrack.rootNote; // not sure if this matters drumBranch.BranchInfo.ChokeGroup['@Value'] = subtrack.inChokeGroup ? 1 : 0; drumBranch.MixerDevice.Volume.Manual['@Value'] = subtrack.volume; drumBranch.MixerDevice.SendInfos = {}; const branchDevices = await buildSimplerDevice(subtrack, abletonVersion, automatedParameters); assignDeviceListIds([branchDevices]); drumBranch.DeviceChain.MidiToAudioDeviceChain.Devices = branchDevices; device.Branches.DrumBranch.push(drumBranch); } return { DrumGroupDevice: device, }; } async function buildTrack( koTrack: AblTrack, groupLane: AblGroupLane | undefined, scenes: AblScene[], trackGroupId: number, exporterParams: ExporterParams, ): Promise { const abletonVersion = getAbletonVersion(exporterParams); const midiTrackTemplate = await loadTemplate('midiTrack', abletonVersion); const midiTrack = structuredClone(midiTrackTemplate.MidiTrack); const automatedParameters = new Set( [...(groupLane?.sessionPatterns || []), ...(groupLane?.arrangementPatterns || [])].flatMap( (pattern) => pattern.faderAutomation.map((point) => point.parameter), ), ); midiTrack['@Id'] = _localId++; midiTrack.Name.EffectiveName['@Value'] = koTrack.soundId ? `${String(koTrack.soundId).padStart(3, '0')} ${koTrack.name}` : koTrack.name; midiTrack.Name.UserName['@Value'] = midiTrack.Name.EffectiveName['@Value']; setColorValue(midiTrack, 8 + _localTrackColor++); midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip = []; midiTrack.TrackGroupId['@Value'] = trackGroupId; midiTrack.DeviceChain.DeviceChain.Devices['#'] = []; midiTrack.DeviceChain.Mixer.Sends = {} as any; midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = koTrack.volume; if (koTrack.faderParams[FaderParam.PAN] !== -1) { midiTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = convertGroupFaderValue( FaderParam.PAN, koTrack.faderParams[FaderParam.PAN], { track: koTrack }, ); } if (koTrack.faderParams[FaderParam.LVL] !== -1) { midiTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = convertGroupFaderValue( FaderParam.LVL, koTrack.faderParams[FaderParam.LVL], { track: koTrack }, ); } if (koTrack.drumRack && exporterParams.includeArchivedSamples) { midiTrack.DeviceChain.DeviceChain.Devices['#'].push( await buildDrumRackDevice(koTrack, abletonVersion, automatedParameters), ); } if (!koTrack.drumRack && koTrack.sampleName && exporterParams.includeArchivedSamples) { midiTrack.DeviceChain.DeviceChain.Devices['#'].push( await buildSimplerDevice(koTrack, abletonVersion, automatedParameters), ); } if (exporterParams.includeArchivedSamples) { for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) { const staticValue = koTrack.faderParams[parameter]; const defaultValue = getFaderDefaultValue(parameter); const hasAutomation = automatedParameters.has(parameter); if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) { midiTrack.DeviceChain.DeviceChain.Devices['#'].push( await buildFaderFilterDevice( parameter, staticValue === -1 ? defaultValue : staticValue, koTrack, abletonVersion, ), ); } } } // make sure id's are sequential in device chain assignDeviceListIds(midiTrack.DeviceChain.DeviceChain.Devices['#']); if (exporterParams.groupTracks) { midiTrack.DeviceChain.AudioOutputRouting.Target['@Value'] = 'AudioOut/GroupTrack'; midiTrack.DeviceChain.AudioOutputRouting.UpperDisplayString['@Value'] = 'Group'; } if (exporterParams.sendEffects) { const trackSendHolderTemplate = await loadTemplate( 'trackSendHolder', abletonVersion, ); const trackSendHolder = structuredClone(trackSendHolderTemplate.TrackSendHolder); trackSendHolder['@Id'] = 0; trackSendHolder.Send.Manual['@Value'] = convertGroupFaderValue( FaderParam.FX, Math.max(getFaderDefaultValue(FaderParam.FX), koTrack.faderParams[FaderParam.FX]), { track: koTrack }, ); midiTrack.DeviceChain.Mixer.Sends.TrackSendHolder = trackSendHolder; } fixIds(midiTrack); const automationTargets = trackAutomationTargets(midiTrack, true, koTrack); // 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 at least 8 slots to avoid Ableton crashes midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot = []; midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot = []; 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] = { '@Id': sc, LomId: { '@Value': 0, }, ClipSlot: { Value: {}, }, HasStop: { '@Value': hasStop ? 'true' : 'false', }, }; midiTrack.DeviceChain.FreezeSequencer.ClipSlotList.ClipSlot[sc] = { '@Id': sc, LomId: { '@Value': 0, }, ClipSlot: { Value: {}, }, HasStop: { '@Value': 'true', }, NeedRefreeze: { '@Value': 'true', }, }; } for (const [clipIdx, koClip] of (koTrack.lane?.arrangementClips || []).entries()) { const midiClip = await buildMidiClip( koClip, clipIdx, getColorValue(midiTrack), false, abletonVersion, automationTargets, midiTrack.DeviceChain.MainSequencer.MidiControllers, koTrack, ); midiTrack.DeviceChain.MainSequencer.ClipTimeable.ArrangerAutomation.Events.MidiClip.push( midiClip, ); } for (const koClip of koTrack.lane?.sessionClips || []) { if (koClip.sceneIndex === undefined) { continue; } const midiClip = await buildMidiClip( koClip, 0, getColorValue(midiTrack), true, abletonVersion, automationTargets, midiTrack.DeviceChain.MainSequencer.MidiControllers, koTrack, ); midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex] = { ...midiTrack.DeviceChain.MainSequencer.ClipSlotList.ClipSlot[koClip.sceneIndex], ClipSlot: { Value: { MidiClip: midiClip, }, }, }; } if (groupLane) { const envelopes = buildArrangementEnvelopes( groupLane.arrangementPatterns, automationTargets, koTrack, false, ); midiTrack.AutomationEnvelopes.Envelopes = envelopes.length ? ({ AutomationEnvelope: envelopes } as any) : {}; } return { MidiTrack: midiTrack }; } async function buildGroupTrack( koTrack: AblTrack, exporterParams: ExporterParams, maxScenes: number, ) { const abletonVersion = getAbletonVersion(exporterParams); const groupTrackTemplate = await loadTemplate('groupTrack', abletonVersion); const groupTrack = structuredClone(groupTrackTemplate.GroupTrack); groupTrack['@Id'] = _localGroupId++; groupTrack.Name.EffectiveName['@Value'] = koTrack.group.toLocaleUpperCase(); groupTrack.Name.UserName['@Value'] = koTrack.group.toLocaleUpperCase(); setColorValue(groupTrack, 5 + _localGroupTrackColor++); groupTrack.Slots.GroupTrackSlot = []; groupTrack.DeviceChain.Mixer.Sends = {} as any; groupTrack.DeviceChain.Mixer.Volume.Manual['@Value'] = 1; groupTrack.DeviceChain.Mixer.Pan.Manual['@Value'] = 0; // adding empty slots for clips (or Ableton will crash) // must be at least 8 slots to avoid Ableton crashes for (let sc = 0; sc < Math.max(MIN_CLIP_LAUNCHER_SCENES, maxScenes); sc++) { groupTrack.Slots.GroupTrackSlot.push({ '@Id': sc, LomId: { '@Value': 0, }, }); } fixIds(groupTrack); return { GroupTrack: groupTrack }; } async function buildScenes( scenes: AblScene[], settings: ProjectSettings, maxScenes: number, abletonVersion: AbletonVersion, ) { const sceneTemplate = await loadTemplate('scene', abletonVersion); const result: ALSSceneContent[] = []; const sceneCount = Math.max(MIN_CLIP_LAUNCHER_SCENES, maxScenes); for (let index = 0; index < sceneCount; index++) { const scene = scenes[index]; const sceneContent = structuredClone(sceneTemplate.Scene); sceneContent['@Id'] = _localId++; if (abletonVersion === '10') { const meter = scene?.timeSignature; (sceneContent as any)['@Value'] = scene ? `${scene.name}${meter ? ` ${meter.numerator}/${meter.denominator}` : ''}` : ` ${index + 1}`; } else { sceneContent.Name['@Value'] = scene?.name || ''; 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); } return result; } async function buildTracks( tracks: AblTrack[], groupLanes: AblGroupLane[], scenes: AblScene[], exporterParams: ExporterParams, ) { const result = []; let trackGroupId = -1; let currentGroup = ''; // if the tracks are grouped, we need to create a group track for each group BEFORE the children midi tracks // this took me around 3 hours to figure out for (const koTrack of tracks.sort((a, b) => a.group.localeCompare(b.group))) { const groupLane = groupLanes.find((lane) => lane.group === koTrack.group); if (exporterParams.groupTracks && koTrack.group !== currentGroup[0]) { currentGroup = koTrack.group; const groupTrack = await buildGroupTrack(koTrack, exporterParams, scenes.length); trackGroupId = groupTrack.GroupTrack['@Id']; result.push(groupTrack); } const midiTrack = await buildTrack(koTrack, groupLane, scenes, trackGroupId, exporterParams); result.push(midiTrack); } return result; } async function buildReturnTrack( projectData: ProjectRawData, trackId: number = 0, abletonVersion: AbletonVersion = '11', ) { const returnTrackTemplate = await loadTemplate('returnTrack', abletonVersion); const returnTrack = structuredClone(returnTrackTemplate.ReturnTrack); returnTrack['@Id'] = trackId; returnTrack.Name.EffectiveName['@Value'] = EFFECTS[projectData.effects.effectType] || ''; returnTrack.Name.UserName['@Value'] = EFFECTS[projectData.effects.effectType] || ''; setColorValue(returnTrack, 28); returnTrack.DeviceChain.Mixer.Sends = {} as any; returnTrack.DeviceChain.DeviceChain.Devices = {} as any; if (projectData.effects.effectType === EffectType.Reverb) { const effectReverbTemplate = await loadTemplate('effectReverb', abletonVersion); const effectReverb = structuredClone(effectReverbTemplate.Reverb); const values = reverbEffectValues(projectData.effects.param1, projectData.effects.param2); // these params are very rough approximation of the KO's reverb // it's not exact, but close enough effectReverb.PreDelay.Manual['@Value'] = 0.5; effectReverb.RoomSize.Manual['@Value'] = 300; effectReverb.ShelfHiFreq.Manual['@Value'] = 1000; effectReverb.ShelfLoFreq.Manual['@Value'] = 1000; effectReverb.DecayTime.Manual['@Value'] = values.decayTime; effectReverb.ShelfHighOn.Manual['@Value'] = values.highShelfOn ? 'true' : 'false'; effectReverb.ShelfHiGain.Manual['@Value'] = values.highShelfGain; effectReverb.ShelfLowOn.Manual['@Value'] = values.lowShelfOn ? 'true' : 'false'; effectReverb.ShelfLoGain.Manual['@Value'] = values.lowShelfGain; effectReverb.MixReflect.Manual['@Value'] = 1; effectReverb.MixDiffuse.Manual['@Value'] = 1; effectReverb.MixDirect.Manual['@Value'] = 0; returnTrack.DeviceChain.DeviceChain.Devices = { Reverb: effectReverb }; } if (projectData.effects.effectType === EffectType.Delay) { const effectDelayTemplate = await loadTemplate('effectDelay', abletonVersion); const effectDelay = structuredClone(effectDelayTemplate.Delay); const values = delayEffectValues(projectData.effects.param1, projectData.effects.param2); effectDelay.DelayLine_SyncL.Manual['@Value'] = 'false'; effectDelay.DelayLine_SyncR.Manual['@Value'] = 'false'; // don't even ask how I came up with this formula effectDelay.DelayLine_TimeL.Manual['@Value'] = values.time; effectDelay.DelayLine_TimeR.Manual['@Value'] = values.time; effectDelay.Feedback.Manual['@Value'] = values.feedback; effectDelay.Filter_On.Manual['@Value'] = 'false'; effectDelay.DryWet.Manual['@Value'] = 1; returnTrack.DeviceChain.DeviceChain.Devices = { Delay: effectDelay }; } if (projectData.effects.effectType === EffectType.Distortion) { const effectDistortionTemplate = await loadTemplate( 'effectDistortion', abletonVersion, ); const effectDistortion = structuredClone(effectDistortionTemplate.Overdrive); const values = distortionEffectValues(projectData.effects.param1, projectData.effects.param2); effectDistortion.Drive.Manual['@Value'] = values.drive; effectDistortion.Tone.Manual['@Value'] = values.tone; effectDistortion.DryWet.Manual['@Value'] = 100; returnTrack.DeviceChain.DeviceChain.Devices = { Overdrive: effectDistortion }; } if (projectData.effects.effectType === EffectType.Chorus) { const effectChorusTemplate = await loadTemplate('effectChorus', abletonVersion); const effectChorus = structuredClone( abletonVersion === '10' ? (effectChorusTemplate as any).Chorus : effectChorusTemplate.Chorus2, ); const values = chorusEffectValues(projectData.effects.param1, projectData.effects.param2); if (abletonVersion === '10') { effectChorus.ModFreq.Manual['@Value'] = values.rate; effectChorus.LfoMultiplier.Manual['@Value'] = 'false'; } else { effectChorus.Rate.Manual['@Value'] = values.rate; } effectChorus.Feedback.Manual['@Value'] = values.feedback; effectChorus.DryWet.Manual['@Value'] = 1; returnTrack.DeviceChain.DeviceChain.Devices = ( abletonVersion === '10' ? { Chorus: effectChorus } : { Chorus2: effectChorus } ) as any; } // putting a filter in return track is absolutely pointless, but hey, why not if (projectData.effects.effectType === EffectType.Filter) { const effectFilterTemplate = await loadTemplate('effectFilter', abletonVersion); const effectFilter = structuredClone(effectFilterTemplate.AutoFilter); const values = filterEffectValues(projectData.effects.param1, projectData.effects.param2); effectFilter.FilterType.Manual['@Value'] = values.filterType; effectFilter.LegacyFilterType.Manual['@Value'] = values.filterType; effectFilter.Cutoff.Manual['@Value'] = values.cutoff; effectFilter.Resonance.Manual['@Value'] = values.resonance; returnTrack.DeviceChain.DeviceChain.Devices = { AutoFilter: effectFilter }; } if (projectData.effects.effectType === EffectType.Compressor) { const effectCompressorTemplate = await loadTemplate( 'effectCompressor', abletonVersion, ); const effectCompressor = structuredClone(effectCompressorTemplate.Compressor2); const values = compressorEffectValues(projectData.effects.param1, projectData.effects.param2); effectCompressor.Threshold.Manual['@Value'] = values.threshold; effectCompressor.Ratio.Manual['@Value'] = values.ratio; effectCompressor.Attack.Manual['@Value'] = values.attack; effectCompressor.Release.Manual['@Value'] = values.release; effectCompressor.AutoReleaseControlOnOff.Manual['@Value'] = 'false'; effectCompressor.Gain.Manual['@Value'] = 0; effectCompressor.GainCompensation.Manual['@Value'] = 'true'; effectCompressor.DryWet.Manual['@Value'] = 1; effectCompressor.Model.Manual['@Value'] = 0; effectCompressor.Knee.Manual['@Value'] = 6; effectCompressor.LookAhead.Manual['@Value'] = 0; returnTrack.DeviceChain.DeviceChain.Devices = { Compressor2: effectCompressor }; } Object.values(returnTrack.DeviceChain.DeviceChain.Devices).forEach((device: any) => { device['@Id'] = 0; }); return { ReturnTrack: returnTrack }; } function getTimeSignatureId(timeSignature: { numerator: number; denominator: number }) { return ( TIME_SIGNATURES[`${timeSignature.numerator}/${timeSignature.denominator}`] || TIME_SIGNATURES['4/4'] ); } function getArrangementTimeSignatureId(timeSignature: { numerator: number; denominator: number }) { const signature = `${timeSignature.numerator}/${timeSignature.denominator}`; const timeSignatureId = TIME_SIGNATURES[signature]; if (timeSignatureId === undefined) { throw new Error( `Arrangement export has no verified Ableton Live time-signature ID for ${signature}`, ); } return timeSignatureId; } function buildArrangementTimeSignatureEvents( sections: AblArrangementSection[], fallbackTimeSignature: { numerator: number; denominator: number }, ) { const initialTimeSignature = sections[0]?.timeSignature || fallbackTimeSignature; const initialTimeSignatureId = sections.length > 0 ? getArrangementTimeSignatureId(initialTimeSignature) : getTimeSignatureId(initialTimeSignature); const events = [ { '@Id': 0, '@Time': -63072000, '@Value': initialTimeSignatureId, }, ]; let previousTimeSignatureId = initialTimeSignatureId; sections.forEach((section, index) => { const timeSignatureId = getArrangementTimeSignatureId(section.timeSignature); if (index === 0 || timeSignatureId === previousTimeSignatureId) { previousTimeSignatureId = timeSignatureId; return; } events.push({ '@Id': events.length, '@Time': section.offset, '@Value': timeSignatureId, }); previousTimeSignatureId = timeSignatureId; }); return events; } export async function buildProject(projectData: ProjectRawData, exporterParams: ExporterParams) { resetIds(); const abletonVersion = getAbletonVersion(exporterParams); const transformedData = abletonTransformer(projectData, exporterParams); if (import.meta.env.DEV) { console.log('projectData', projectData); console.log('transformedData', transformedData); } // reset local ids _localId = 1; _localGroupId = 50; _localTrackColor = 1; _localGroupTrackColor = 1; const projectTemplate = await loadTemplate('project', abletonVersion); const project = structuredClone(projectTemplate); // setting up tempo project.Ableton.LiveSet.MasterTrack.DeviceChain.Mixer.Tempo.Manual['@Value'] = projectData.settings.bpm; project.Ableton.LiveSet.MasterTrack.AutomationEnvelopes.Envelopes.AutomationEnvelope[1].Automation.Events.FloatEvent[ '@Value' ] = projectData.settings.bpm; const initialTimeSignature = transformedData.arrangementSections[0]?.timeSignature || projectData.scenesSettings.timeSignature; (project.Ableton.LiveSet.MasterTrack.DeviceChain.Mixer as any).TimeSignature.Manual['@Value'] = transformedData.arrangementSections.length > 0 ? getArrangementTimeSignatureId(initialTimeSignature) : getTimeSignatureId(initialTimeSignature); project.Ableton.LiveSet.MasterTrack.AutomationEnvelopes.Envelopes.AutomationEnvelope[0].Automation.Events.EnumEvent = buildArrangementTimeSignatureEvents( transformedData.arrangementSections, projectData.scenesSettings.timeSignature, ) as any; const tracks = await buildTracks( transformedData.tracks, transformedData.groupLanes, transformedData.scenes, exporterParams, ); project.Ableton.LiveSet.Tracks = { '#': tracks } as any; if (exporterParams.sendEffects) { const returnTrack = await buildReturnTrack( projectData, project.Ableton.LiveSet.Tracks['#'].length + 1, abletonVersion, ); project.Ableton.LiveSet.Tracks['#'].push(returnTrack); } const scenes = await buildScenes( transformedData.scenes, projectData.settings, transformedData.scenes.length, abletonVersion, ); if (abletonVersion === '10') { (project.Ableton.LiveSet as any).SceneNames.Scene = scenes; } else { project.Ableton.LiveSet.Scenes.Scene = scenes; } if (exporterParams.sendEffects) { project.Ableton.LiveSet.SendsPre = { SendPreBool: { '@Id': 0, '@Value': 'false', }, }; } else { project.Ableton.LiveSet.SendsPre = {} as any; } const fixedRoot = fixIds(project); fixedRoot.Ableton.LiveSet.NextPointeeId['@Value'] = getId(); if (import.meta.env.DEV) { console.log('ROOT', fixedRoot); } const newXml = create(fixedRoot).end({ prettyPrint: true, indent: '\t' }); const gzipped = gzipString(newXml); return gzipped; }