u-he-preset-randomizer
Version:
Create random u-he synth presets through randomization and merging of your existing presets.
725 lines • 28.3 kB
JavaScript
/**
* @file CLI entry point for u-he preset randomizer tool.
* Handles argument parsing, interactive mode, and preset generation orchestration.
*/
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import checkbox from '@inquirer/checkbox';
import confirm from '@inquirer/confirm';
import number from '@inquirer/number';
import searchPrompt from '@inquirer/search';
import select from '@inquirer/select';
import chalk from 'chalk';
import fg from 'fast-glob';
import fs from 'fs-extra';
import { getConfigFromParameters, getDefaultConfig, } from './config.js';
import { generatePresets } from './generatePresets.js';
import { narrowDownByAuthor, narrowDownByCategory, narrowDownByFavoritesFile, } from './libraryFilters.js';
import { loadPresetLibrary } from './presetLibrary.js';
import { getSynthConfig } from './synthConfigs/index.js';
import { detectPresetLibraryLocations, } from './utils/detector.js';
import { isDirectExecution } from './utils/entrypoint.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = fs.readJSONSync(`${__dirname}/../package.json`);
let config = getDefaultConfig();
function logCliBanner() {
console.log('');
console.log(chalk.cyan.bold(`u-he Preset Randomizer v${packageJson.version}`));
console.log('');
console.log('Create new synth presets through:');
console.log(`${chalk.dim(' •')} Fully random generation`);
console.log(`${chalk.dim(' •')} Variations of existing presets`);
console.log(`${chalk.dim(' •')} Merging multiple presets`);
console.log('');
console.log(chalk.dim('https://github.com/Fannon/u-he-preset-randomizer'));
console.log('');
console.log(chalk.dim('Tip: Use ↑↓ to navigate, Space to select, Enter to confirm'));
console.log('');
}
export async function startCli(overrides) {
config = getConfigFromParameters(overrides);
logCliBanner();
if (!config.synth) {
await runInteractiveMode();
return;
}
try {
const result = generatePresets(config);
logGenerationSuccess(result);
}
catch (error) {
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
if (isDirectExecution(import.meta.url)) {
startCli().catch((err) => {
console.error(err);
process.exit(1);
});
}
async function runInteractiveMode() {
// Detect correct Preset Library Location
const locations = detectPresetLibraryLocations(config);
// 1) Detect available u-he synths and offer user choice
const synthChoices = locations.map((el) => {
return {
value: el.synthName,
name: el.synthName,
};
});
if (!synthChoices.length) {
console.log('');
console.log(chalk.red.bold('⚠ No u-he synths found on your system'));
console.log('');
console.log(chalk.bold('Please make sure:'));
console.log(`${chalk.dim(' •')} You have at least one u-he synth installed`);
console.log(`${chalk.dim(' •')} The synth is installed in the standard location`);
console.log(`${chalk.dim(' •')} You have created or loaded some presets`);
console.log('');
console.log(chalk.dim('If your u-he folder is in a custom location, use:'));
console.log(' ' +
chalk.cyan('npx u-he-preset-randomizer --custom-folder "path/to/u-he"'));
console.log('');
process.exit(1);
}
const synth = await searchPrompt({
message: 'Which u-he synth to generate presets for?',
pageSize: synthChoices.length,
source: (input) => {
if (!input) {
return Promise.resolve(synthChoices);
}
const normalized = input.toLowerCase();
return Promise.resolve(synthChoices.filter((el) => {
return el.value.toLowerCase().startsWith(normalized);
}));
},
});
config.synth = synth;
const synthConfig = getSynthConfig(synth);
if (config.binaryTemplate === undefined &&
synthConfig?.defaults?.binaryTemplate) {
config.binaryTemplate = true;
}
const detectedLocation = locations.find((el) => el.synthName === synth);
// 2) Choose random generation mode
// 2) Choose random generation mode
const mode = await select({
message: 'What would you like to do?',
default: 'Fully randomized presets',
choices: [
{
value: 'Fully randomized presets',
name: 'Create fully random presets (best for discovering new sounds)',
},
{
value: 'Randomize existing preset',
name: 'Create variations of a specific preset (keep the character, add variation)',
},
{
value: 'Merge existing presets',
name: 'Blend multiple presets together (combine your favorites)',
},
],
});
// 2.5) Choose optional modifiers for randomization
if (!config.stable || !config.binary) {
const binaryEnabled = synthConfig?.defaults?.binary ?? false;
// First: Basic filtering options
// First: Basic filtering options
// First: Options & Filters
const basicOptions = await checkbox({
message: 'Select filters and configuration (optional, press ENTER to continue, select with SPACE):',
choices: [
{
value: 'folder',
name: 'Filter by specific folder',
},
{
value: 'category',
name: 'Filter by category (e.g., Bass, Lead, Pad)',
},
{
value: 'author',
name: 'Filter by preset creator',
},
{
value: 'favorites',
name: 'Filter by favorited presets',
},
{
value: 'advanced',
name: 'Customize randomization (Style, Binary support, Naming)',
},
],
});
// Apply basic options
if (basicOptions.includes('folder')) {
config.folder = true;
}
if (basicOptions.includes('category')) {
config.category = true;
}
if (basicOptions.includes('author')) {
config.author = true;
}
if (basicOptions.includes('favorites')) {
config.favorites = true;
}
if (basicOptions.includes('advanced')) {
// First: Choose randomization mode
const randomizationMode = await select({
message: 'Randomization approach:',
choices: [
{
value: 'stable',
name: '[Stable] Safer randomization, stays close to library character (recommended)',
},
{
value: 'balanced',
name: '[Balanced] More randomization, balance between stability and creativity',
},
{
value: 'creative',
name: '[Creative] Experimental, explores unusual combinations',
},
],
default: 'stable',
});
// Set mode flags
if (randomizationMode === 'stable') {
config.stable = true;
config.creative = false;
}
else if (randomizationMode === 'creative') {
config.stable = false;
config.creative = true;
}
else {
// balanced mode
config.stable = false;
config.creative = false;
}
// Then: Other advanced options
const advancedOptions = await checkbox({
message: 'Additional advanced settings:',
choices: [
{
value: 'binary',
name: '[Binary] Include binary encoded preset data like curves (may cause crashes)',
checked: binaryEnabled,
},
{
value: 'binaryTemplate',
name: '[Binary Templates] Use safe binary templates (Recommended for Z3)',
checked: config.binaryTemplate,
},
{
value: 'dictionary',
name: '[Dictionary] Generate realistic preset names',
},
],
});
if (advancedOptions.includes('binary')) {
config.binary = true;
}
if (advancedOptions.includes('binaryTemplate')) {
config.binaryTemplate = true;
}
if (advancedOptions.includes('dictionary')) {
config.dictionary = true;
}
}
else {
// Default: stable mode enabled
config.stable = true;
config.creative = false;
}
}
// Narrow down by folder selection
if (config.folder === true) {
// Detect correct Preset Library Location
const location = detectedLocation ??
detectPresetLibraryLocations(config).find((el) => el.synthName.toLowerCase() === (config.synth ?? '').toLowerCase());
if (!location) {
throw new Error(`Could not find preset library location for synth: ${config.synth ?? ''}`);
}
const userFolders = fg
.sync('**/*', {
cwd: location.userPresets,
onlyDirectories: true,
})
.map((el) => {
return `/User/${el}/`;
});
const presetFolders = fg
.sync('**/*', {
cwd: location.presets ?? '',
onlyDirectories: true,
})
.map((el) => {
return `/Local/${el}/`;
});
const folders = [
'/',
'/User/',
'/Local/',
...userFolders,
...presetFolders,
].sort();
const folderChoice = await searchPrompt({
message: 'Which folder to narrow down to?',
pageSize: 12,
source: (input) => {
if (!input) {
return Promise.resolve(folders);
}
const normalized = input.toLowerCase();
return Promise.resolve(folders.filter((el) => {
return el.toLowerCase().includes(normalized);
}));
},
});
if (folderChoice && folderChoice !== '/') {
config.folder = folderChoice;
config.pattern = `${config.folder}${config.pattern ?? '**/*'}`;
}
}
if (!config.synth) {
throw new Error('Synth not specified in config');
}
console.log(chalk.dim('Loading preset library...'));
const presetLibrary = loadPresetLibrary(config.synth, config, detectedLocation);
console.log(chalk.green(`✓ Loaded ${presetLibrary.presets.length} presets`));
// Optionally: Narrow down by u-he favorites
if (config.favorites === true && presetLibrary.favorites.length) {
const allChoices = presetLibrary.favorites.map((el) => {
return {
value: el.fileName,
name: `${el.fileName} (${el.presets.length})`,
};
});
const favoritesPrompt = await checkbox({
message: 'Which favorite file(s) to use as a selection?',
choices: allChoices,
});
config.favorites = favoritesPrompt;
}
// Filter out presets by favorite file (if given)
if (config.favorites &&
config.favorites !== true &&
config.favorites.length > 0) {
presetLibrary.presets = narrowDownByFavoritesFile(presetLibrary, config.favorites);
}
else {
console.log(chalk.dim('No selection made, skipping this step.'));
}
// Optionally: Narrow down by author
if (config.author === true) {
const availableAuthors = {};
for (const preset of presetLibrary.presets) {
const authorMeta = preset.meta.find((el) => el.key === 'Author');
if (authorMeta && typeof authorMeta.value === 'string') {
const author = authorMeta.value;
availableAuthors[author] = (availableAuthors[author] ?? 0) + 1;
}
}
const allChoices = [];
for (const author in availableAuthors) {
const count = availableAuthors[author];
if (count !== undefined) {
allChoices.push({
value: author,
name: `${author} (${count})`,
});
}
}
allChoices.sort((a, b) => a.value.localeCompare(b.value));
if (allChoices.length) {
const author = await searchPrompt({
message: 'Which author to narrow down to?',
pageSize: 12,
source: (input) => {
if (!input) {
return Promise.resolve(allChoices);
}
const normalized = input.toLowerCase();
return Promise.resolve(allChoices.filter((el) => {
return el.name.toLowerCase().includes(normalized);
}));
},
});
config.author = author;
}
}
// Filter out presets by author (if given)
if (config.author && config.author !== true) {
presetLibrary.presets = narrowDownByAuthor(presetLibrary, config.author);
}
// Optionally: Narrow down by category
if (config.category === true) {
const availableCategories = {};
for (const preset of presetLibrary.presets) {
for (const category of preset.categories) {
const parentCategory = category.split(':')[0];
if (parentCategory) {
availableCategories[parentCategory] ??= 0;
availableCategories[parentCategory]++;
}
availableCategories[category] ??= 0;
availableCategories[category]++;
}
}
const allChoices = [];
for (const category in availableCategories) {
const count = availableCategories[category];
if (count !== undefined) {
allChoices.push({
value: category,
name: `${category} (${count})`,
});
}
}
allChoices.sort((a, b) => a.value.localeCompare(b.value));
if (allChoices.length) {
const category = await searchPrompt({
message: 'Which category to narrow down to?',
pageSize: 12,
source: (input) => {
if (!input) {
return Promise.resolve(allChoices);
}
const normalized = input.toLowerCase();
return Promise.resolve(allChoices.filter((el) => {
return el.name.toLowerCase().includes(normalized);
}));
},
});
config.category = category;
}
}
// Filter out presets by category (if given)
if (config.category && config.category !== true) {
presetLibrary.presets = narrowDownByCategory(presetLibrary, config.category);
}
if (mode === 'Fully randomized presets') {
//////////////////////////////////////////////////
// MODE 1: Generate fully randomized presets //
//////////////////////////////////////////////////
config.amount ??= await chooseAmountOfPresets(32);
}
else if (mode === 'Randomize existing preset') {
//////////////////////////////////////////////////
// MODE 2: Randomize existing preset //
//////////////////////////////////////////////////
console.log('');
console.log(chalk.dim('Select presets one by one. Choose "no choice" when done.'));
console.log('');
const presetSelections = [];
const foundPresets = presetLibrary.presets.map((el) => el.filePath);
while (true) {
const presetChoice = await choosePreset(foundPresets, false);
if (presetChoice) {
presetSelections.push(presetChoice);
const displayName = presetChoice === '?' ? 'random' : presetChoice;
console.log(chalk.green(`✓ Added ${displayName}`));
}
else {
break;
}
}
if (presetSelections.length === 0) {
console.log(chalk.yellow('No presets selected. Exiting.'));
process.exit(0);
}
console.log(chalk.dim(`Selected ${presetSelections.length} preset(s)`));
console.log('');
config.preset =
presetSelections.length === 1 ? presetSelections[0] : presetSelections;
config.randomness ??= await chooseRandomness(20);
config.amount ??= await chooseAmountOfPresets(16);
}
else if (mode === 'Merge existing presets') {
//////////////////////////////////////////////////
// MODE 3: Merge Random Presets //
//////////////////////////////////////////////////
console.log('');
console.log(chalk.dim('Select presets one by one. Choose "no choice" when done.'));
console.log('');
config.merge = [];
const foundPresets = presetLibrary.presets.map((el) => el.filePath);
while (true) {
const presetChoice = await choosePreset(foundPresets, true);
if (presetChoice) {
config.merge.push(presetChoice);
const displayName = presetChoice === '?'
? 'random'
: presetChoice.startsWith('*') || presetChoice === '*'
? 'all matching'
: presetChoice;
console.log(chalk.green(`✓ Added ${displayName}`));
if (presetChoice === '*' || presetChoice.startsWith('*')) {
break;
}
}
else {
break;
}
}
console.log(chalk.dim(`Selected ${config.merge.length} preset(s)`));
console.log('');
// Choose amount of randomness
config.randomness ??= await chooseRandomness(10);
config.amount ??= await chooseAmountOfPresets(16);
}
// Show summary before generating
console.log('');
console.log(chalk.bold('Summary:'));
console.log(`${chalk.dim(' • ')}Synth: ${chalk.cyan(config.synth)}`);
console.log(`${chalk.dim(' • ')}Mode: ${chalk.cyan(mode)}`);
console.log(`${chalk.dim(' • ')}Amount: ${chalk.cyan(`${config.amount} presets`)}`);
if (config.randomness) {
console.log(`${chalk.dim(' • ')}Randomness: ${chalk.cyan(`${config.randomness}%`)}`);
}
if (config.preset) {
if (Array.isArray(config.preset)) {
console.log(chalk.dim(' • ') +
`Base Presets: ${chalk.cyan(`${config.preset.length} selected`)}`);
}
else {
console.log(`${chalk.dim(' • ')}Base Preset: ${chalk.cyan(config.preset)}`);
}
}
if (config.category && typeof config.category === 'string') {
console.log(`${chalk.dim(' • ')}Category: ${chalk.cyan(config.category)}`);
}
if (config.author && typeof config.author === 'string') {
console.log(`${chalk.dim(' • ')}Author: ${chalk.cyan(config.author)}`);
}
if (config.folder && typeof config.folder === 'string') {
console.log(`${chalk.dim(' • ')}Folder: ${chalk.cyan(config.folder)}`);
}
// Show randomization mode
if (config.stable) {
console.log(chalk.dim(' • ') +
`Randomization: ${chalk.green('Stable')} ${chalk.dim('(safer, stays close to library)')}`);
}
else if (config.creative) {
console.log(chalk.dim(' • ') +
`Randomization: ${chalk.yellow('Creative')} ${chalk.dim('(experimental, explores edges)')}`);
}
else {
console.log(chalk.dim(' • ') +
`Randomization: ${chalk.blue('Balanced')} ${chalk.dim('(default behavior)')}`);
}
if (config.binary) {
console.log(chalk.dim(' • ') +
`Binary Mode: ${chalk.yellow('Enabled')} ${chalk.dim('(may cause issues)')}`);
}
if (config.binaryTemplate) {
console.log(chalk.dim(' • ') +
`Binary Templates: ${chalk.green('Enabled')} ${chalk.dim('(weighted random selection)')}`);
}
if (config.dictionary) {
console.log(chalk.dim(' • ') +
`Dictionary: ${chalk.green('Enabled')} ${chalk.dim('(realistic names)')}`);
}
console.log('');
const proceed = await confirm({
message: 'Generate presets with these settings?',
default: true,
});
if (!proceed) {
console.log(chalk.yellow('Cancelled. Run the command again to start over.'));
process.exit(0);
}
console.log('');
console.log(chalk.dim('Generating presets...'));
let result;
try {
// If binary mode is enabled but the current library lacks binary data,
// force a reload by passing undefined.
const needsReload = config.binary &&
presetLibrary.presets.length > 0 &&
presetLibrary.presets[0]?.binary === undefined;
result = generatePresets(config, needsReload ? undefined : presetLibrary);
}
catch (error) {
console.error(chalk.red('✗ Failed to generate presets'));
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
logGenerationSuccess(result);
logRepeatCommand(config);
}
//////////////////////////////////////////
// HELPER FUNCTIONS //
//////////////////////////////////////////
function logGenerationSuccess(result) {
console.log('');
console.log(chalk.green('✓ Successfully generated ' +
result.presetCount +
' ' +
(result.presetCount === 1 ? 'preset' : 'presets')));
console.log('');
// Show list of generated presets
if (result.writtenFiles.length > 0) {
console.log(chalk.bold('Generated presets:'));
for (const file of result.writtenFiles) {
// Extract just the preset name from the full path
const fileName = file.split('/').pop() || file;
console.log(chalk.dim(' • ') + fileName);
}
console.log('');
}
console.log(chalk.bold('Output folder:'));
console.log(chalk.cyan(` ${result.outputFolder}`));
console.log('');
console.log(chalk.dim('Open your synth and look for the RANDOM folder in your user presets.'));
console.log('');
}
/** Choose number of presets to generate */
async function chooseAmountOfPresets(initial = 8) {
console.log('');
const amount = await number({
message: 'How many presets would you like to generate?',
default: initial,
validate: (input) => {
if (typeof input !== 'number' || Number.isNaN(input))
return 'Please enter a number';
if (input < 1)
return 'Please enter at least 1';
return true;
},
});
return amount ?? initial;
}
/** Choose randomness amount */
async function chooseRandomness(initial = 20) {
console.log('');
console.log(chalk.dim('Randomness guide:'));
console.log(chalk.dim(' 0-20% = Subtle variations'));
console.log(chalk.dim(' 20-50% = Noticeable differences'));
console.log(chalk.dim(' 50-100% = Dramatically different'));
const amount = await number({
message: 'How much variation? (0-100)',
default: initial,
validate: (input) => {
if (typeof input !== 'number' || Number.isNaN(input))
return 'Please enter a number';
if (input < 0 || input > 100)
return 'Please enter a value between 0 and 100';
return true;
},
});
return amount ?? initial;
}
async function choosePreset(foundPresets, allowSelectAll = false) {
const controlChoices = [
{ value: '', name: ' (no choice / complete)' },
{ value: '?', name: '? (random pick)' },
];
if (allowSelectAll) {
controlChoices.push({ value: '*', name: '* (select all)' });
}
const allChoices = controlChoices.concat(foundPresets.map((el) => {
return {
value: el,
name: el,
};
}));
const presetChoice = await searchPrompt({
message: 'Select preset(s):',
pageSize: 12,
source: (input) => {
if (!input) {
return Promise.resolve(allChoices);
}
const normalized = input.toLowerCase();
const staticChoices = [];
if (allowSelectAll) {
staticChoices.push({
value: `*${input}*`,
name: `*${input}* (all presets including search string)`,
});
}
staticChoices.push({
value: `?${input}?`,
name: `?${input}? (random pick of presets including search string)`,
});
return Promise.resolve(staticChoices.concat(allChoices.filter((el) => {
return el.name.toLowerCase().includes(normalized);
})));
},
});
return presetChoice;
}
function logRepeatCommand(config) {
let cliCommand = `npx u-he-preset-randomizer@latest --synth ${config.synth ?? ''} --amount ${config.amount ?? 8}`;
if (config.preset) {
const presets = Array.isArray(config.preset)
? config.preset
: [config.preset];
for (const preset of presets) {
if (typeof preset === 'string') {
cliCommand += ` --preset "${preset}"`;
}
}
}
else if (config.merge) {
const merges = Array.isArray(config.merge) ? config.merge : [config.merge];
for (const merge of merges) {
if (typeof merge === 'string') {
cliCommand += ` --merge "${merge}"`;
}
}
}
if (config.randomness) {
cliCommand += ` --randomness ${config.randomness}`;
}
if (config.pattern && config.pattern !== '**/*') {
cliCommand += ` --pattern "${config.pattern}"`;
}
if (config.folder && typeof config.folder === 'string') {
cliCommand += ` --folder "${config.folder}"`;
}
if (config.favorites) {
const favs = Array.isArray(config.favorites)
? config.favorites.join(',')
: String(config.favorites);
if (favs !== 'true') {
cliCommand += ` --favorites "${favs}"`;
}
}
if (config.category && typeof config.category === 'string') {
cliCommand += ` --category "${config.category}"`;
}
if (config.author && typeof config.author === 'string') {
cliCommand += ` --author "${config.author}"`;
}
if (config.dictionary) {
cliCommand += ` --dictionary`;
}
if (config.stable) {
cliCommand += ` --stable`;
}
if (config.creative) {
cliCommand += ` --creative`;
}
if (config.debug) {
cliCommand += ` --debug`;
}
if (config.binary) {
cliCommand += ` --binary`;
}
if (config.binaryTemplate) {
cliCommand += ` --binary-template`;
}
console.log(chalk.dim('To repeat with same settings:'));
console.log(chalk.cyan(cliCommand));
console.log('');
}
//# sourceMappingURL=cli.js.map