u-he-preset-randomizer
Version:
Create random u-he synth presets through randomization and merging of your existing presets.
243 lines • 8.49 kB
JavaScript
/**
* @file Preset parser and serializer for u-he preset files.
* Provides functions to read, parse, modify, and write .h2p preset files.
*/
import * as path from 'node:path';
import chalk from 'chalk';
//////////////////////////////////////////
// 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, filePath, binary) {
const meta = getPresetMetadata(fileString);
let categories = [];
const categoriesMeta = meta.find((el) => el.key === 'Categories');
if (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) {
if (!fileString.includes('/*@Meta') && !fileString.includes('/*@meta')) {
return [];
}
const split = fileString.split('*/');
const metadataHeader = split[0]?.replace('/*@Meta', '').replace('/*@meta', '') ?? '';
const cleanedRows = metadataHeader.split('\n').filter((el) => el);
const metadata = [];
for (let i = 0; i < cleanedRows.length; i = i + 2) {
const keyRow = cleanedRows[i];
if (!keyRow) {
continue;
}
const key = keyRow.replace(':', '');
const valueRow = cleanedRows[i + 1];
if (!valueRow) {
continue;
}
let value = valueRow.split("'").join('');
if (value.includes(', ')) {
value = value.split(', ');
}
metadata.push({
key,
value,
});
}
return metadata;
}
/**
* Extracts and parses parameter data from a u-he preset file string.
*
* @param fileString - The content of the preset file as a string.
* @param presetPath - The path to the preset file (used for warnings).
* @returns An array of PresetParam objects representing all parameters.
*/
export function getPresetParams(fileString, presetPath) {
const params = [];
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 row = cleanedRows[i];
if (!row)
continue;
const paramSplit = row.split('=');
const key = paramSplit[0];
const value = paramSplit[1];
if (!key || !value)
continue;
if (key === '#cm') {
currentSection = value;
}
const param = {
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.length > 0) {
const lastParam = params[params.length - 1];
if (lastParam) {
lastParam.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, 10);
param.type = 'integer';
}
else if (isNumeric(value)) {
param.value = parseFloat(value);
param.type = 'float';
}
params.push(param);
}
return params;
}
/**
* Extracts the binary section from a u-he preset file.
* The binary section contains advanced settings like MSEG curves.
*
* @param fileString - The content of the preset file as a string.
* @returns The binary section as a string, or empty string if not present.
*/
export function getPresetBinarySection(fileString) {
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 //
//////////////////////////////////////////
/**
* Serializes a Preset object back to u-he preset file format (.h2p).
*
* @param preset - The Preset object to serialize.
* @returns The serialized preset as a string in .h2p format.
*/
export function serializePresetToFile(preset) {
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;
}
/**
* Validates that a preset has required data and no corrupted values.
* Logs warnings for invalid presets.
*
* @param preset - The Preset object to validate.
* @returns True if the preset is valid, false otherwise.
*/
export function isValidPreset(preset) {
if (!preset.params ||
!preset.meta ||
preset.params.length === 0 ||
preset.meta.length === 0) {
console.warn(chalk.yellow(`Warning: Ignoring preset ${preset.filePath} due to empty params or meta (params: ${preset.params?.length || 0}, meta: ${preset.meta?.length || 0})`));
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 //
//////////////////////////////////////////
export function isInt(value) {
const num = parseFloat(value);
return !Number.isNaN(num) && Number.isFinite(num) && Math.floor(num) === num;
}
export function isNumeric(value) {
const num = parseFloat(value);
return !Number.isNaN(num) && Number.isFinite(num);
}
//# sourceMappingURL=parser.js.map