u-he-preset-randomizer
Version:
Create random u-he synth presets through randomization and merging of your existing presets.
250 lines (215 loc) • 6.88 kB
text/typescript
import chalk from "chalk";
import * as path from "path";
export interface Preset {
/** Relative filePath to preset folder */
filePath: string;
/** Preset file name, without file extension or sub-folders */
presetName: string;
categories: string[];
meta: PresetMetaEntry[];
params: PresetParam[];
binary?: string;
}
export interface PresetMetaEntry {
key: string;
value: string | string[];
}
export interface PresetParam {
id: string;
key: string;
section: string;
value: string | number;
index: number;
type: "string" | "float" | "integer";
}
//////////////////////////////////////////
// PARSER FUNCTIONS //
//////////////////////////////////////////
/**
* Parses a u-he preset file and returns a Preset object.
*
* @param fileString - The content of the preset file as a string.
* @param filePath - The path to the preset file.
* @param binary - A boolean indicating whether to include the binary section of the preset file.
* @returns The parsed Preset object.
*/
export function parseUhePreset(fileString: string, filePath: string, binary: boolean): Preset {
const meta = getPresetMetadata(fileString)
let categories: string[] = []
const categoriesMeta = meta.find(el => el.key === 'Categories')
if (categoriesMeta && categoriesMeta.value) {
if (Array.isArray(categoriesMeta.value)) {
categories = categoriesMeta.value
} else {
categories.push(categoriesMeta.value)
}
}
return {
filePath: filePath,
presetName: path.parse(filePath).name,
meta: meta,
params: getPresetParams(fileString, filePath),
binary: binary ? getPresetBinarySection(fileString) : undefined,
categories: categories
};
}
/**
* Retrieves the metadata entries from the given u-he preset file string.
*
* @param fileString - The string representation of the file.
* @returns An array of PresetMetaEntry objects representing the metadata entries.
*/
export function getPresetMetadata(fileString: string): PresetMetaEntry[] {
const split = fileString.split("*/");
const metadataHeader = split[0]!
.replace("/*@Meta", "")
.replace("/*@meta", "");
const cleanedRows = metadataHeader.split("\n").filter((el) => el);
const metadata: PresetMetaEntry[] = [];
for (let i = 0; i < cleanedRows.length; i = i + 2) {
if (!cleanedRows[i]) {
continue;
}
const key = cleanedRows[i].replace(":", "");
if (!cleanedRows[i + 1]) {
continue;
}
let value: string | string[] = cleanedRows[i + 1]!.split("'").join("");
if (value.includes(", ")) {
value = value.split(", ");
}
metadata.push({
key,
value,
});
}
return metadata;
}
export function getPresetParams(fileString: string, presetPath: string): PresetParam[] {
const params: PresetParam[] = [];
const split = fileString.split("*/");
if (!split[1]) {
return params;
}
const paramBody = split[1]!.split("// Section")[0];
if (!paramBody) {
throw new Error("Could not parse preset parameter body");
}
const cleanedRows = paramBody.split("\n").filter((el) => el);
let repeatCounter = 1;
let currentSection = "HEAD";
let currentSectionAndKey = "";
for (let i = 0; i < cleanedRows.length; i++) {
const paramSplit = cleanedRows[i]!.split("=");
const key = paramSplit[0]!;
const value = paramSplit[1]!;
if (key === "#cm") {
currentSection = value;
}
const param: PresetParam = {
id: `${currentSection}/${key}`,
key: key,
section: currentSection,
value: value,
index: i,
type: "string",
};
// Some parameters appear more than once in the same section
// Here we need to add the index of their appearance to get a unique ID
if (currentSectionAndKey === param.id) {
currentSectionAndKey = param.id;
param.id += `/${repeatCounter}`;
if (repeatCounter === 1) {
params[params.length -1].id += `/0`;
}
repeatCounter++;
if (!param.id.includes('#mv') && !param.id.includes('#ms')) {
console.warn(chalk.yellow(`Unexpected duplicated header + key for: ${param.id} in preset "${presetPath}"`))
}
} else {
currentSectionAndKey = param.id;
repeatCounter = 1;
}
if (isInt(value)) {
param.value = parseInt(value);
param.type = "integer";
} else if (isNumeric(value)) {
param.value = parseFloat(value);
param.type = "float";
}
params.push(param);
}
return params;
}
export function getPresetBinarySection(fileString: string): string {
const split = fileString.split("// Section for ugly compressed binary Data\n// DON'T TOUCH THIS\n");
if (split[1]) {
return split[1].trim();
} else {
return ''
}
}
//////////////////////////////////////////
// SERIALIZER FUNCTIONS //
//////////////////////////////////////////
export function serializePresetToFile(preset: Preset): string {
let file = "";
// Add meta header
file += "/*@Meta\n\n";
for (const entry of preset.meta) {
file += `${entry.key}:\n`;
if (Array.isArray(entry.value)) {
file += `'${entry.value.join(", ")}'\n\n`;
} else {
file += `'${entry.value}'\n\n`;
}
}
file += "*/\n\n";
// Add params
for (const param of preset.params) {
file += `${param.key}=${param.value}\n`;
}
// Add footer
file += "\n\n\n\n";
file += "// Section for ugly compressed binary Data\n";
file += "// DON'T TOUCH THIS\n\n";
if (preset.binary) {
file += preset.binary
}
file += ``; // binary end of file marker?
return file;
}
export function isValidPreset(preset: Preset) {
if (!preset.params.length || !preset.meta.length) {
return false;
}
for (const param of preset.params) {
if (typeof param.value === "string" && param.value.includes('[object Object]')) {
console.warn(chalk.yellow(`Warning: Ignoring preset ${preset.filePath} due to invalid value: ${param.id}`))
return false;
}
if (typeof param.value === "string" && param.value.includes('undefined')) {
console.warn(chalk.yellow(`Warning: Ignoring preset ${preset.filePath} due to invalid value: ${param.id}`))
return false;
}
if (param.id.includes('[object Object]')) {
console.warn(chalk.yellow(`Warning: Ignoring preset ${preset.filePath} due to invalid parameter: ${param.id}`))
return false;
}
}
return true
}
//////////////////////////////////////////
// HELPER FUNCTIONS //
//////////////////////////////////////////
function isInt(value: unknown): boolean {
return (
!isNaN(value as number) &&
(function (x) {
return (x | 0) === x;
})(parseFloat(value as string))
);
}
function isNumeric(value: unknown): boolean {
return !isNaN(parseFloat(value as string)) && isFinite(value as number);
}