Add support for MIDI controller targets and clip envelopes in Ableton export
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
|||||||
filterEffectValues,
|
filterEffectValues,
|
||||||
reverbEffectValues,
|
reverbEffectValues,
|
||||||
} from './effects';
|
} from './effects';
|
||||||
|
import { AbletonMidiControllerTargets, buildMidiClipEnvelopes } from './midi';
|
||||||
import { getSimplerPlaybackState } from './playback';
|
import { getSimplerPlaybackState } from './playback';
|
||||||
import { ALSDrumBranch } from './types/drumBranch';
|
import { ALSDrumBranch } from './types/drumBranch';
|
||||||
import { ALSDrumRack } from './types/drumRack';
|
import { ALSDrumRack } from './types/drumRack';
|
||||||
@@ -274,6 +275,7 @@ async function buildMidiClip(
|
|||||||
clipForLauncher: boolean = false,
|
clipForLauncher: boolean = false,
|
||||||
abletonVersion: AbletonVersion = '11',
|
abletonVersion: AbletonVersion = '11',
|
||||||
automationTargets: AbletonAutomationTargets = {},
|
automationTargets: AbletonAutomationTargets = {},
|
||||||
|
midiControllerTargets: AbletonMidiControllerTargets = {},
|
||||||
koTrack?: AblTrack,
|
koTrack?: AblTrack,
|
||||||
): Promise<ALSMidiClipContent> {
|
): Promise<ALSMidiClipContent> {
|
||||||
const midiClipTemplate = await loadTemplate<ALSMidiClip>('midiClip', abletonVersion);
|
const midiClipTemplate = await loadTemplate<ALSMidiClip>('midiClip', abletonVersion);
|
||||||
@@ -371,17 +373,21 @@ async function buildMidiClip(
|
|||||||
|
|
||||||
midiClip.Notes.NoteIdGenerator.NextId['@Value'] = _noteId;
|
midiClip.Notes.NoteIdGenerator.NextId['@Value'] = _noteId;
|
||||||
|
|
||||||
|
const clipEnvelopes =
|
||||||
|
abletonVersion === '10'
|
||||||
|
? buildMidiClipEnvelopes(koClip.midiEvents, midiControllerTargets)
|
||||||
|
: [];
|
||||||
if (clipForLauncher && koTrack) {
|
if (clipForLauncher && koTrack) {
|
||||||
const clipEnvelopes = buildClipEnvelopes(
|
clipEnvelopes.push(
|
||||||
koClip.groupPattern,
|
...buildClipEnvelopes(koClip.groupPattern, automationTargets, koTrack, false),
|
||||||
automationTargets,
|
|
||||||
koTrack,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
clipEnvelopes.forEach((envelope, index) => {
|
||||||
|
envelope['@Id'] = index;
|
||||||
|
});
|
||||||
midiClip.Envelopes.Envelopes = clipEnvelopes.length
|
midiClip.Envelopes.Envelopes = clipEnvelopes.length
|
||||||
? ({ ClipEnvelope: clipEnvelopes } as any)
|
? ({ ClipEnvelope: clipEnvelopes } as any)
|
||||||
: {};
|
: {};
|
||||||
}
|
|
||||||
|
|
||||||
return midiClip;
|
return midiClip;
|
||||||
}
|
}
|
||||||
@@ -780,6 +786,7 @@ async function buildTrack(
|
|||||||
false,
|
false,
|
||||||
abletonVersion,
|
abletonVersion,
|
||||||
automationTargets,
|
automationTargets,
|
||||||
|
midiTrack.DeviceChain.MainSequencer.MidiControllers,
|
||||||
koTrack,
|
koTrack,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -821,6 +828,7 @@ async function buildTrack(
|
|||||||
true,
|
true,
|
||||||
abletonVersion,
|
abletonVersion,
|
||||||
automationTargets,
|
automationTargets,
|
||||||
|
midiTrack.DeviceChain.MainSequencer.MidiControllers,
|
||||||
koTrack,
|
koTrack,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
83
src/lib/exporters/ableton/midi.ts
Normal file
83
src/lib/exporters/ableton/midi.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { AblMidiEvent } from '../../transformers/ableton';
|
||||||
|
|
||||||
|
export type AbletonMidiControllerTargets = Record<string, { '@Id': number }>;
|
||||||
|
|
||||||
|
type AutomationPoint = {
|
||||||
|
time: number;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHANNEL_PRESSURE_TARGET = 129;
|
||||||
|
const SENTINEL_TIME = -63072000;
|
||||||
|
|
||||||
|
export function decodeChannelPressure(event: AblMidiEvent) {
|
||||||
|
if (
|
||||||
|
event.data.length !== 4 ||
|
||||||
|
event.data[0] !== 0xd0 ||
|
||||||
|
event.data[1] > 127 ||
|
||||||
|
event.data[2] !== 0xff ||
|
||||||
|
event.data[3] !== 0
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.data[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function floatEvents(points: AutomationPoint[]) {
|
||||||
|
return points.map((point, index) => ({
|
||||||
|
'@Id': index,
|
||||||
|
'@Time': point.time,
|
||||||
|
'@Value': point.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMidiClipEnvelopes(
|
||||||
|
events: AblMidiEvent[],
|
||||||
|
controllerTargets: AbletonMidiControllerTargets,
|
||||||
|
) {
|
||||||
|
const target = controllerTargets[`ControllerTargets.${CHANNEL_PRESSURE_TARGET}`]?.['@Id'];
|
||||||
|
if (target === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const points: AutomationPoint[] = [{ time: SENTINEL_TIME, value: 0 }];
|
||||||
|
let previousValue = 0;
|
||||||
|
let eventCount = 0;
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
const value = decodeChannelPressure(event);
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = event.position / 96;
|
||||||
|
points.push({ time, value: previousValue });
|
||||||
|
points.push({ time, value });
|
||||||
|
previousValue = value;
|
||||||
|
eventCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventCount === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'@Id': 0,
|
||||||
|
EnvelopeTarget: { PointeeId: { '@Value': target } },
|
||||||
|
Automation: {
|
||||||
|
Events: { FloatEvent: floatEvents(points) },
|
||||||
|
AutomationTransformViewState: {
|
||||||
|
IsTransformPending: { '@Value': 'false' },
|
||||||
|
TimeAndValueTransforms: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoopSlot: { Value: {} },
|
||||||
|
ScrollerTimePreserver: {
|
||||||
|
LeftTime: { '@Value': 0 },
|
||||||
|
RightTime: { '@Value': 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user