Files
ep-133-project-export/src/lib/exporters/ableton/utils.ts

214 lines
6.2 KiB
TypeScript

import { create } from 'xmlbuilder2';
export const TIME_SIGNATURES: { [key: string]: number } = {
'1/4': 196,
'2/4': 198,
'3/4': 200,
'4/4': 201,
'5/4': 202,
'6/4': 203,
'7/4': 204,
'8/4': 205,
'5/8': 206,
'6/8': 207,
'7/8': 208,
'9/8': 209,
'12/8': 210,
'3/2': 211,
'4/2': 212,
'5/2': 213,
'6/2': 214,
'7/2': 215,
'1/2': 216,
'1/1': 217,
'2/1': 218,
'3/1': 219,
'4/1': 220,
'5/1': 221,
'3/8': 299,
};
const MIN_ID = 22000;
const START_ID = 22000;
let _id = START_ID;
let assignedNodes = new WeakSet<object>();
const templateCache: Record<string, any> = {};
const templateMap: Record<string, () => Promise<any>> = {
midiClip: () => import('./templates/midiClip.xml?raw'),
midiTrack: () => import('./templates/midiTrack.xml?raw'),
project: () => import('./templates/project.xml?raw'),
simpler: () => import('./templates/simpler.xml?raw'),
scene: () => import('./templates/scene.xml?raw'),
groupTrack: () => import('./templates/groupTrack.xml?raw'),
drumRack: () => import('./templates/drumRack.xml?raw'),
drumBranch: () => import('./templates/drumBranch.xml?raw'),
returnTrack: () => import('./templates/returnTrack.xml?raw'),
trackSendHolder: () => import('./templates/trackSendHolder.xml?raw'),
effectReverb: () => import('./templates/effectReverb.xml?raw'),
effectDelay: () => import('./templates/effectDelay.xml?raw'),
effectChorus: () => import('./templates/effectChorus.xml?raw'),
effectDistortion: () => import('./templates/effectDistortion.xml?raw'),
effectFilter: () => import('./templates/effectFilter.xml?raw'),
effectCompressor: () => import('./templates/effectCompressor.xml?raw'),
};
const live10TemplateMap: Record<string, () => Promise<any>> = {
midiClip: () => import('./templates/live10/midiClip.xml?raw'),
midiTrack: () => import('./templates/live10/midiTrack.xml?raw'),
project: () => import('./templates/live10/project.xml?raw'),
simpler: () => import('./templates/live10/simpler.xml?raw'),
scene: () => import('./templates/live10/scene.xml?raw'),
groupTrack: () => import('./templates/live10/groupTrack.xml?raw'),
drumRack: () => import('./templates/live10/drumRack.xml?raw'),
drumBranch: () => import('./templates/live10/drumBranch.xml?raw'),
returnTrack: () => import('./templates/live10/returnTrack.xml?raw'),
trackSendHolder: () => import('./templates/live10/trackSendHolder.xml?raw'),
effectReverb: () => import('./templates/live10/effectReverb.xml?raw'),
effectDelay: () => import('./templates/live10/effectDelay.xml?raw'),
effectChorus: () => import('./templates/live10/effectChorus.xml?raw'),
effectDistortion: () => import('./templates/live10/effectDistortion.xml?raw'),
effectFilter: () => import('./templates/live10/effectFilter.xml?raw'),
effectCompressor: () => import('./templates/live10/effectCompressor.xml?raw'),
};
export async function loadTemplate<T>(
templateName: string,
abletonVersion: '10' | '11' = '11',
): Promise<T> {
const cacheKey = `${abletonVersion}:${templateName}`;
if (templateCache[cacheKey]) {
return templateCache[cacheKey];
}
const importFn = abletonVersion === '10' ? live10TemplateMap[templateName] : templateMap[templateName];
if (!importFn) {
throw new Error(`Unknown template: ${templateName}`);
}
const templateModule = await importFn();
const parsed = create(templateModule.default).toObject();
templateCache[cacheKey] = parsed;
return parsed as T;
}
export function koEnvRangeToSeconds(value: number, maxSeconds: number) {
if (value < 0 || value > 255) {
throw new RangeError('Value must be between 0 and 255');
}
return (value / 255) * maxSeconds;
}
function isPointeeTarget(key: string, idNum: number) {
return (
idNum > MIN_ID ||
key === 'AutomationTarget' ||
key === 'ModulationTarget' ||
key === 'Pointee' ||
key.endsWith('ModulationTarget') ||
key.startsWith('ControllerTargets.')
);
}
function assignIds(node: any, key: string, idMap: Map<string, number>): any {
if (Array.isArray(node)) {
return node.map((n) => assignIds(n, key, idMap));
}
if (!node || typeof node !== 'object') {
return node;
}
if (node?.['@Id']) {
const idNum = parseInt(String(node['@Id']), 10);
if (isPointeeTarget(key, idNum) && !assignedNodes.has(node)) {
const previousId = String(node['@Id']);
node['@Id'] = _id;
assignedNodes.add(node);
idMap.set(previousId, _id);
_id++;
}
}
Object.keys(node).forEach((childKey) => {
if (!childKey.startsWith('@')) {
node[childKey] = assignIds(node[childKey], childKey, idMap);
}
});
return node;
}
function rewritePointeeIds(node: any, idMap: Map<string, number>): any {
if (Array.isArray(node)) {
return node.map((item) => rewritePointeeIds(item, idMap));
}
if (!node || typeof node !== 'object') {
return node;
}
if (node.PointeeId?.['@Value'] !== undefined) {
const replacement = idMap.get(String(node.PointeeId['@Value']));
if (replacement !== undefined) {
node.PointeeId['@Value'] = replacement;
}
}
Object.keys(node).forEach((key) => {
if (!key.startsWith('@')) {
node[key] = rewritePointeeIds(node[key], idMap);
}
});
return node;
}
export function fixIds(node: any): any {
const idMap = new Map<string, number>();
assignIds(node, '', idMap);
return rewritePointeeIds(node, idMap);
}
export function resetIds() {
_id = START_ID;
assignedNodes = new WeakSet<object>();
}
export function getId() {
return _id;
}
export async function gzipString(content: string) {
const encoder = new TextEncoder();
const input = encoder.encode(content);
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
writer.write(input);
writer.close();
const compressed = await new Response(cs.readable).arrayBuffer();
return compressed;
}
export function filterFreqFromNormalized(x: number) {
const fMin = 30;
const fMax = 22000;
return fMin * (fMax / fMin) ** x;
}
export function toNativePath(posixPath: string): string {
const isWindows = navigator.userAgent.includes('indows');
const pathSep = isWindows ? '\\' : '/';
if (pathSep === '/') {
return posixPath;
}
return posixPath.split('/').join(pathSep);
}