UNPKG

u-he-preset-randomizer

Version:

Create random u-he synth presets through randomization and merging of your existing presets.

243 lines 8.49 kB
/** * @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