u-he-preset-randomizer
Version:
Create random u-he synth presets through randomization and merging of your existing presets.
446 lines (388 loc) • 15.2 kB
text/typescript
import { ParamsModel, getDictionaryOfNames } from "./analyzer.js";
import { Preset } from "./parser.js";
import { PresetLibrary } from "./presetLibrary.js";
import {
uniqueNamesGenerator,
adjectives,
colors,
names,
} from "unique-names-generator";
import { Config } from "./config.js";
import chalk from "chalk";
/**
* Fully randomized presets, with real values from library
*/
export function generateFullyRandomPresets(
presetLibrary: PresetLibrary,
paramModel: ParamsModel,
config: Config
): PresetLibrary {
console.log('----------------------------------------------------------------------')
console.log(`Fully random presets with modes: stable=${config.stable || false}, binary=${config.binary || false}`)
if (presetLibrary.presets.length === 0) {
console.error(chalk.red('Error: No presets available for randomization.'))
process.exit(1)
}
const newPresetLibrary: PresetLibrary = {
...presetLibrary,
userPresetsFolder: presetLibrary.userPresetsFolder + "/RANDOM",
presets: [],
};
for (let i = 0; i < (config.amount || 16); i++) {
const randomPreset: Preset = JSON.parse(
JSON.stringify(getRandomArrayItem(presetLibrary.presets))
) as Preset;
if (config.stable) {
const presetPerSectionMap: {[section: string]: Preset} = {}
for (const param of randomPreset.params) {
if (!paramModel[param.id]) {
console.error(chalk.red(`Error: Unknown parameter ${param.id} in preset ${randomPreset.filePath}, missing in analyzed parameter model`))
}
if (!presetPerSectionMap[param.section]) {
presetPerSectionMap[param.section] = getRandomArrayItem(presetLibrary.presets)
}
if (paramModel[param.id].type === 'string' || paramModel[param.id].distinctValues.length <= 2) {
// Do not randomize string type values or values that only have 1 or 2 distinct values
continue;
}
if (paramModel[param.id].keepStable) {
// Do not randomize if parameter is marked as to be kept stable
continue;
}
const randomNewParam = presetPerSectionMap[param.section].params.find(el => el.id === param.id);
if (randomNewParam) {
param.value = randomNewParam.value
} else {
// If the preset doesn't have the particular param, fall back to a fully random value
param.value = getRandomArrayItem(paramModel[param.id]!.values);
}
}
} else {
for (const param of randomPreset.params) {
if (!paramModel[param.id]) {
console.error(chalk.red(`Error: Unknown parameter ${param.id} in preset ${randomPreset.filePath}, missing in analyzed parameter model`))
}
if (paramModel[param.id].keepStable === 'always') {
continue;
}
param.value = getRandomArrayItem(paramModel[param.id]!.values);
}
}
let randomName = uniqueNamesGenerator({
dictionaries: [adjectives, colors, names],
separator: " ",
style: "capital",
});
if (config.dictionary) {
const names = getDictionaryOfNames(presetLibrary)
randomName = `${getRandomArrayItem(names)} ${getRandomArrayItem(names)} ${getRandomArrayItem(names)}`
}
randomPreset.filePath = `/Fully Random/RND ${randomName}.h2p`;
if (config.category && config.category !== true) {
randomPreset.filePath = `/Fully Random/${config.category.split(':').join(' ')}/RND ${randomName}.h2p`;
}
randomPreset.presetName = `RND ${randomName}`;
(randomPreset.meta = [
{
key: "Author",
value: "Random Generator",
},
{
key: "Description",
value:
`Fully random preset. ${getPresetDescriptionSuffix(config)}`,
},
])
if (config.category) {
randomPreset.meta.push({
key: "Categories",
value: [config.category as string]
})
}
newPresetLibrary.presets.push(randomPreset);
}
return newPresetLibrary;
}
/**
* Randomize a given preset, with specific randomization ratio
*/
export function generateRandomizedPresets(
presetLibrary: PresetLibrary,
paramModel: ParamsModel,
config: Config
): PresetLibrary {
console.log('----------------------------------------------------------------------')
console.log(`Randomizing preset with modes: stable=${config.stable || false}, binary=${config.binary || false}`)
const newPresetLibrary: PresetLibrary = {
...presetLibrary,
userPresetsFolder: presetLibrary.userPresetsFolder + "/RANDOM",
presets: [],
};
let basePreset: Preset;
// If no preset given or "?" is passed, choose a random preset
if (config.preset === "?" || !config.preset) {
basePreset = getRandomArrayItem(presetLibrary.presets);
} else if (config.preset.startsWith("?")) {
basePreset = getRandomArrayItem(presetLibrary.presets.filter((el) => {
const searchString = config.preset.split('?').join('').toLowerCase();
return el && el.filePath && el.filePath.toLowerCase().includes(searchString);
}));
} else {
basePreset = presetLibrary.presets.find((el) => {
return el.filePath.includes(config.preset);
});
if (!basePreset) {
console.error(chalk.red(`Error: No preset with name ${config.preset} found!`))
process.exit(1);
}
}
console.log('Randomizing base preset: ' + basePreset.filePath);
for (let i = 0; i < (config.amount || 8); i++) {
const randomPreset = randomizePreset(basePreset, paramModel, config)
let randomName = uniqueNamesGenerator({
dictionaries: [adjectives, colors],
separator: " ",
style: "capital",
});
if (config.dictionary) {
const names = getDictionaryOfNames(presetLibrary)
randomName = `${getRandomArrayItem(names)} ${getRandomArrayItem(names)}`
}
randomPreset.filePath = `/Randomized Preset/${randomPreset.presetName}/RND ${randomName} ${randomPreset.presetName}.h2p`;
randomPreset.presetName = `RND ${randomName} ${randomPreset.presetName}`;
const descriptionMeta = randomPreset.meta.find(el => el.key === 'Description')
if (descriptionMeta) {
descriptionMeta.value += `. Randomized existing preset: ${randomPreset.presetName}. ${getPresetDescriptionSuffix(config)}`
}
newPresetLibrary.presets.push(randomPreset);
}
return newPresetLibrary;
}
/**
* Merge multiple presets together, with randomization amount
*/
export function generateMergedPresets(
presetLibrary: PresetLibrary,
paramModel: ParamsModel,
config: Config,
): PresetLibrary {
console.log('----------------------------------------------------------------------')
console.log(`Merging presets with modes: stable=${config.stable || false}, binary=${config.binary || false}`)
const newPresetLibrary: PresetLibrary = {
...presetLibrary,
userPresetsFolder: presetLibrary.userPresetsFolder + "/RANDOM",
presets: [],
};
let mergePresets: Preset[] = [];
if (!Array.isArray(config.merge)) {
config.merge = [config.merge];
}
for (const presetTitle of config.merge) {
let mergePreset: Preset;
// If no preset given or "?" is passed, choose a random preset
if (presetTitle === "?") {
mergePresets.push(getRandomArrayItem(presetLibrary.presets));
} else if (presetTitle === "*") {
mergePresets = presetLibrary.presets.filter((el) => {
return el && el.presetName;
});
break;
} else if (presetTitle.startsWith("*")) {
mergePresets.push(...presetLibrary.presets.filter((el) => {
const searchString = presetTitle.split('*').join('').toLowerCase();
return el && el.filePath && el.filePath.toLowerCase().includes(searchString);
}));
} else if (presetTitle.startsWith("?")) {
mergePresets.push(getRandomArrayItem(presetLibrary.presets.filter((el) => {
const searchString = presetTitle.split('?').join('').toLowerCase();
return el && el.filePath && el.filePath.toLowerCase().includes(searchString);
})));
} else {
mergePreset = presetLibrary.presets.find((el) => {
return el.filePath.includes(presetTitle);
});
if (!mergePreset) {
console.error(chalk.red(`Error: No preset with name "${presetTitle}" found!`));
process.exit(1);
}
mergePresets.push(mergePreset);
}
}
console.log(
`Merging ${mergePresets.length} presets:\n * ${mergePresets.map((el) => el.presetName).join("\n * ")}\n`
);
if (mergePresets.length < 2) {
console.error(chalk.red("Error: Merge presets only works when at least two presets have been chosen"));
process.exit(1);
}
for (let i = 0; i < (config.amount || 8); i++) {
let newPreset: Preset = JSON.parse(
JSON.stringify(getRandomArrayItem<Preset>(mergePresets))
) as Preset;
// Create random ratios, that still add up to 1 total
const mergeRatios = calculateRandomMergeRatios(mergePresets.length);
for (const param of newPreset.params) {
if (!paramModel[param.id]) {
console.error(chalk.red(`Error: Unknown parameter ${param.id} in preset ${newPreset.filePath}, missing in analyzed parameter model`))
}
if (config.stable) {
if (paramModel[param.id].type === 'string' || paramModel[param.id].distinctValues.length <= 2) {
// Do not randomize string type values or values that only have 1 or 2 distinct values
continue;
}
if (paramModel[param.id].keepStable) {
// Do not randomize if parameter is marked as to be kept stable
continue;
}
}
if (paramModel[param.id].keepStable === 'always') {
continue;
}
const oldParamValue = JSON.parse(JSON.stringify(param.value)) as string | number;
let newParamValue = oldParamValue;
if (param.type === "string") {
// Randomly pick a string enum value from one of the merge patches
const pick = getRandomArrayItem<Preset>(mergePresets);
const findParam = pick.params.find((el) => el.id === param.id);
if (findParam && findParam.value) {
newParamValue = findParam.value;
}
} else {
let newParamValue = 0;
for (const [i, preset] of mergePresets.entries()) {
const findParam = preset.params.find((el) => el.id === param.id);
if (findParam && findParam.value) {
newParamValue += (findParam.value as number) * mergeRatios[i];
} else {
newParamValue += oldParamValue as number * mergeRatios[i];
}
}
if (param.type === "integer") {
newParamValue = Math.round(newParamValue);
} else if (param.type === "float") {
newParamValue = Math.trunc(newParamValue * 100) / 100;
}
}
if (typeof newParamValue === 'object') {
console.error(chalk.red('Error: New param value is object, but should not be.'), newPreset.filePath, newParamValue)
}
param.value = newParamValue;
}
if (config.randomness) {
newPreset = randomizePreset(newPreset, paramModel, config)
}
let randomName = uniqueNamesGenerator({
dictionaries: [adjectives, colors, names],
separator: " ",
style: "capital",
});
if (config.dictionary) {
const names = getDictionaryOfNames(presetLibrary)
randomName = `${getRandomArrayItem(names)} ${getRandomArrayItem(names)} ${getRandomArrayItem(names)}`
}
newPreset.filePath = `/Merged Preset/RND ${randomName}.h2p`;
newPreset.presetName = `RND ${randomName}`;
newPreset.meta = [
{
key: "Author",
value: "Random Generator",
},
{
key: "Description",
value: `Merged preset, based on ${mergePresets
.map((el) => el.presetName)
.join(
", "
)}. ${getPresetDescriptionSuffix(config)}`,
},
]
if (config.category) {
newPreset.meta.push({
key: "Categories",
value: [config.category as string]
})
}
newPresetLibrary.presets.push(newPreset);
}
return newPresetLibrary;
}
//////////////////////////////////////////
// HELPER FUNCTIONS //
//////////////////////////////////////////
export function getRandomArrayItem<T>(list: T[]) {
return list[Math.floor(Math.random() * list.length)];
}
function calculateRandomMergeRatios(amount: number) {
const randomNumbers: number[] = [];
let randomTotal = 0;
for (let i = 0; i < amount; i++) {
const rnd = Math.random();
randomNumbers.push(rnd);
randomTotal += rnd;
}
const mergeRatios = randomNumbers.map((el) => {
return el / randomTotal;
});
return mergeRatios;
}
export function randomizePreset(
basePreset: Preset,
paramModel: ParamsModel,
config: Config
): Preset {
const randomness = config.randomness || 20;
const randomRatio = Math.min(Math.max(0, randomness / 100), 100);
const stableRatio = 1 - randomRatio;
const randomPreset: Preset = JSON.parse(JSON.stringify(basePreset)) as Preset;
for (const param of randomPreset.params) {
if (!paramModel[param.id]) {
console.error(chalk.red(`Error: Unknown parameter ${param.id} in preset ${randomPreset.filePath}, missing in analyzed parameter model`))
}
if (config.stable) {
if (paramModel[param.id].type === 'string' || paramModel[param.id].distinctValues.length <= 2) {
// Do not randomize string type values or values that only have 1 or 2 distinct values
continue;
}
if (paramModel[param.id]?.keepStable) {
// Do not randomize if parameter is marked as to be kept stable
continue;
}
}
if (paramModel[param.id]?.keepStable === 'always') {
continue;
}
let randomParamValue = getRandomArrayItem(paramModel[param.id]!.values);
const oldParamValue = param.value;
if (randomParamValue !== oldParamValue) {
if (param.type !== "string") {
randomParamValue = (oldParamValue as number) * stableRatio + (randomParamValue as number) * randomRatio;
if (param.type === "integer") {
randomParamValue = Math.round(randomParamValue as number);
} else if (param.type === "float") {
randomParamValue = Math.trunc(randomParamValue as number * 100) / 100;
}
} else {
// Randomly decide between the two values by randomness ratio
if (Math.random() > randomRatio) {
randomParamValue = oldParamValue; // Revert to old value if random threshold is not met
}
}
param.value = randomParamValue;
}
}
return randomPreset;
}
function getPresetDescriptionSuffix(config: Config): string {
const niceDate = new Date().toISOString().split('.')[0].replace('T', ' ')
let suffix = `Generated on ${niceDate} by https://github.com/Fannon/u-he-preset-randomizer.`
if (config.category) {
suffix += ` Based on presets of category: ${config.category}.`;
}
if (config.stable) {
suffix += ` Randomization mode=stable.`;
}
if (config.binary) {
suffix += ` Randomization mode=binary.`;
}
return suffix;
}