1162 lines
40 KiB
TypeScript
1162 lines
40 KiB
TypeScript
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<object, AblTrack>();
|
|
|
|
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<string, any>[]) {
|
|
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<ALSMidiClipContent> {
|
|
const midiClipTemplate = await loadTemplate<ALSMidiClip>('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<string, typeof koClip.notes> = koClip.notes.reduce(
|
|
(acc, note) => {
|
|
const group = note.note.toString();
|
|
acc[group] = acc[group] || [];
|
|
acc[group].push(note);
|
|
|
|
return acc;
|
|
},
|
|
{} as Record<string, typeof koClip.notes>,
|
|
);
|
|
|
|
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<FaderParam> = new Set(),
|
|
) {
|
|
const simplerTemplate = await loadTemplate<ALSSimpler>('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<any>('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<ALSFilter>('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<FaderParam>,
|
|
) {
|
|
const [drumRackTemplate, drumBranchTemplate] = await Promise.all([
|
|
loadTemplate<ALSDrumRack>('drumRack', abletonVersion),
|
|
loadTemplate<ALSDrumBranch>('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<ALSMidiTrack> {
|
|
const abletonVersion = getAbletonVersion(exporterParams);
|
|
const midiTrackTemplate = await loadTemplate<ALSMidiTrack>('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<ALSTrackSendHolder>(
|
|
'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<ALSGroupTrack>('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<ALSScene>('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<ALSReturnTrack>('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<ALSReverb>('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<ALSDelay>('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<ALSDistortion>(
|
|
'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<ALSChorus>('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<ALSFilter>('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<ALSCompressor>(
|
|
'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<ALSProject>('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;
|
|
}
|