2021-11-21
word count: 479
approx reading time: 2 mins
i made a small thingy that let's you merge and randomize vital presets from the browser. you can check it out here
the following is a bit of an explanation of how it works. the code is relatively simple so you should be able to follow along even if you don't know much js. hopefully it's interesting!
vital presets are just json files with a ton (a ton) of fields. it's not the best, but it makes our lives a lot easier! here's an example of a preset. it's the init preset:
{ "author": "<your name here>", "comments": "", "macro1": "MACRO 1", "macro2": "MACRO 2", "macro3": "MACRO 3", "macro4": "MACRO 4", "preset_name": "init", "preset_style": "", "settings": { "beats_per_minute": 2.0, "bypass": 0.0, "chorus_cutoff": 60.0, "chorus_delay_1": -9.0, "chorus_delay_2": -7.0, "chorus_dry_wet": 0.5, "chorus_feedback": 0.4000000059604645, ... }, "synth_version": "1.0.7" }
the settings
object contains the important values that we want to play
with. it's also huge, so im not gonna list all of the fields here
side note: samples and waveforms are stored in text using base 64 :)
the code is relatively simple, it first sets an onclick for the submit button, which takes care of loading the presets, randomizing them, merging them, and then saving the merged preset:
// input type=file const files = document.getElementById("files"); // input type=range min=0 max=100 const random = document.getElementById("random"); // button const button = document.getElementById("merge"); // main submit function button.onclick = async function() { // stop if there are no files if (files.files.length == 0) { alert("please upload some presets to be merged :)"); return; } // load all uploaded files at the same time, using `Promise.all` const presets = await Promise.all( // we have to do [...files.files] cause `files` does not have a `map`, // but it is iterable :) // js moment :) [...files.files].map(async f => { return JSON.parse(await f.text()); }) ); // then we randomize the presets, // and we merge them all using the `reduce` method const output = presets.map(randomize).reduce(merge); savePreset(output); };
as you can see,
FileList
is fun in that it's kinda an array, but not really cause fuck you :) you
can do array access and also use the spread operator, but you can't
actually map through it. to be able to map it, we have to first do
[...files.files]
so it becomes an array. i do not enjoy the js apis
continuing with js funny-ness, we have to make a small util function to check the types of stuff:
// js does not have a built in thing :) // im not angry im just disappointed function isNumeric(something) { return typeof(something) === 'number'; }
the rest of the functions are not much more complex, but they pull some
dynamic-language tricks. the randomize function for example, maps over
all of the numeric fields in the settings
object, modifying them a
random ammount:
function randomize(preset) { // don't do anything if we don't have to do anything if (random.value == 0) return; // go through all the keys in the settings object, and modify them only if they are numeric Object.keys(preset.settings).forEach((key) => { if (isNumeric(preset.settings[key])) { // we want to randomize a normalized ammount, so we do a bit of maths // // r = Math.random() - 0.5 // r is in [-0.5, 0.5] // random = random.value / 100 // random is in [0.0, 1.0] // change = (1 + r * random) // change is in [0.5, 1.5] // val = val * change preset.settings[key] *= 1 + (Math.random() - 0.5) * (random.value / 100); } }); return preset; }
since some fields have big values and others have really small values,
it wouldn't make sense to modify them all by the same ammount. to
normalize the randomization value, we do val = val * (1 + r)
, where
r
is a random number from -0.5
to 0.5
. since we want to be able to
control the ammount of randomization, we multiply r
by the value in
the slider
since the slider value goes from 0 to 100, we divide it by 100 to get a
value from 0 to 1, which when we multiply it by r
, it will give us
control over how much we randomize each field. in the end, each field
can go as low as 50% it's original value, to up to 150%
the merge
function is similar, but we have to first get the union of
the keys:
function merge(a, b) { // since not every preset might have all keys defined, we want to get a unique union list of the keys // we use the spread operator to put all the keys in an array, then make a `Set` to get uniqueness let keys = new Set([...Object.keys(a.settings), ...Object.keys(b.settings)]); // loop throuhg the keys, and if they're defined in both, set it as the average // if the key is not defined in either of them, we ignore it and leave whatever is in the first preset Array.from(keys).forEach((key) => { if ((a.settings[key] !== undefined && isNumeric(a.settings[key])) || (b.settings[key] !== undefined && isNumeric(b.settings[key]))) { // we modify the value in the first preset, which is the one we return a.settings[key] = (a.settings[key] + b.settings[key]) * 0.5; } }); return a; }
finally, the savePreset
function makes an <a>
tag, and uses it to
download the preset:
function savePreset(preset) { // we turn the preset into a json string, then make a data thingy out of it var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(preset)); // we create a button, set the data as the href, give it a name, and click it // this causes the preset to be downloaded as lethargic.vital var dlAnchorElem = document.createElement("a"); dlAnchorElem.setAttribute("href", dataStr); dlAnchorElem.setAttribute("download", "lethargic.vital"); dlAnchorElem.click(); }
and that's it really! there's not much going on tbh, but hopefully it was worth reading
i'd like to add more features, but if i do i will probably not edit
this post, so if you read this and the code here and on the lethargic
page are different, you know why