diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b48667b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +Changelog +========= + +## v0.01 +* The first version. +* It makes banks from any Vital presets, LFOs, or wavetables that have the delimiter. +* It probably sucks. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8819cac --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +vitalBank +========= + +# U wot m8? +You're probably wondering why I made this garbage, given that there's already a way to make banks from within Vital, right? + +Well, sure. + +But maybe you don't want to manually select stuff. Or something. + +Or maybe you are working on multiple banks. + +Or maybe I just wanted an excuse to code. + +Who knows, but this thing is here. And it's awful. + +# Whatever, just show me how to use this thing. +Yeah, well, fine then. + +## Config +So first thing is to make a config file. Just look at `config.json.example` as a reference. + +Basically you're going to be setting the `base_preset_dir`, which is the directory where Vital installs Presets and LFOs and all the other stuff. + +Then you specify the directory name (in relation to the `base_preset_dir`) where you want to output the vital bank(s). + +Then you specify the delimiter. This is what separates the bank name from the preset name. + +## Make content +Now, obviously you gotta make content. + +When naming, regardless if it's a patch, LFO, or wavetable, do it in the format `bankname [delimiter] object name`. + +So for example: `Garbage Pack: Part Deux $$ This Preset is Awful`. + +## Run bankify +So.. I developed this against Python 3.8. + +I'm guessing 3.7 should work and probably 3.6. But who knows. + +Anyway so really all you need to do is run `python bankify.py` and then go look at your bank directory and, well there you are. + +You should have your bank(s) all wrapped up. + +# Things to know +For this initial release, the source files do not get deleted. Hell, there aren't any arguments you can pass in. + +It's literally just the script for now. I'll probably change that later, but, proof of concept or something. \ No newline at end of file diff --git a/bankify.py b/bankify.py new file mode 100644 index 0000000..e73a226 --- /dev/null +++ b/bankify.py @@ -0,0 +1,253 @@ +from __future__ import annotations +from pathlib import Path +from typing import Dict, Iterable, Optional, Tuple, Union +from zipfile import ZipFile, ZIP_DEFLATED +import json + + +class Lfo: + """ + A Vital LFO + """ + EXTENSION = 'vitallfo' + DIR = 'LFOs' + + @classmethod + def from_file(cls, filepath: Path) -> Lfo: + """ + Create an LFO from a file + :param filepath: + :return: + """ + with filepath.open('r') as f: + return cls(data=json.load(f), filepath=filepath) + + def __str__(self): + return self.name + + def __repr__(self): + return str(self) + + def __init__(self, data: dict, filepath: Optional[Path]): + self._data = data + self.original_path = filepath + self.name = self._data['name'] + + def rename(self, new_name: str): + """ + Rename + + :param new_name: + :return: + """ + self._data['name'] = new_name + self.name = self._data['name'] + + +class Preset: + """ + A Vital Preset + """ + EXTENSION = 'vital' + DIR = 'Presets' + + @classmethod + def from_file(cls, filepath: Path) -> Preset: + """ + Create an Preset from a file + :param filepath: + :return: + """ + with filepath.open('r') as f: + return cls(data=json.load(f), filepath=filepath) + + def __str__(self): + return self.name + + def __repr__(self): + return str(self) + + def __init__(self, data: dict, filepath: Optional[Path], **kwargs): + self._data = data + self.original_path = filepath + + if filepath is not None: # Differs because it appears the name comes from the file + self.name = filepath.stem + else: + self.name = kwargs['name'] + + def rename(self, new_name: str): + """ + Rename + + :param new_name: + :return: + """ + self.name = new_name + + +class Wavetable: + """ + A Vital Wavetable + """ + EXTENSION = 'vitaltable' + DIR = 'Wavetables' + + @classmethod + def from_file(cls, filepath: Path) -> Wavetable: + """ + Create an LFO from a file + :param filepath: + :return: + """ + with filepath.open('r') as f: + return cls(data=json.load(f), filepath=filepath) + + def __str__(self): + return self.name + + def __repr__(self): + return str(self) + + def __init__(self, data: dict, filepath: Optional[Path]): + self._data = data + self.original_path = filepath + self.name = self._data['name'] + + def rename(self, new_name: str): + """ + Rename + + :param new_name: + :return: + """ + self._data['name'] = new_name + self.name = self._data['name'] + + +class Bank: + """ + Represents a bank comprised of LFOs, Presets, and Wavetables + """ + EXTENSION = 'vitalbank' + + def __str__(self): + return self.name + + def __repr__(self): + return str(self) + + def __init__(self, bank_name: str, lfos: Optional[Iterable[Lfo]], presets: Optional[Iterable[Preset]], + wavetables: Optional[Iterable[Wavetable]]): + self.name = bank_name + self.elements = { + Lfo: {lfo.name: lfo for lfo in lfos} if lfos is not None else list(), + Preset: {preset.name: preset for preset in presets} if presets is not None else list(), + Wavetable: {wavetable.name: wavetable for wavetable in wavetables} if wavetables is not None else list(), + } + + def write_file(self, filedir: Path): + """ + Write the bank file to disk + + :param filedir: The directory to write to + :return: + """ + filepath = filedir.joinpath(f'{self.name}.{Bank.EXTENSION}') + + bankfile = ZipFile(file=filepath, mode='w', compression=ZIP_DEFLATED, compresslevel=9) + + for ct, ctd in self.elements.items(): + zdir = f'{self.name}/{ct.DIR}' + for i in ctd: + zfile = f'{zdir}/{i.name}.{ct.EXTENSION}' + bankfile.writestr(zinfo_or_arcname=zfile, data=json.dumps(i._data)) + + bankfile.close() + + +TYPE_DATA = { + Lfo: ('LFOs', 'vitallfo'), + Preset: ('Presets', 'vital'), + Wavetable: ('Wavetables', 'vitaltable'), +} + +CONFIG_FILE = Path('config.json') +with CONFIG_FILE.open('r') as config_file: + CONFIG = json.load(config_file) + BASE_DIR = Path(CONFIG['base_preset_dir']) + BANK_DIR = BASE_DIR.joinpath(CONFIG['bank_dir']) + USER_DIR = BASE_DIR.joinpath('User') + BANK_DELIM = CONFIG['delimiter'] + + +def is_bank_obj(delim: str, vobj: Union[Lfo, Preset, Wavetable]) -> Optional[Union[Lfo, Preset, Wavetable]]: + """ + If an object name contains the delmiter and matches the pattern for belonging in a bank, return it. None otherwise. + First delimiter must be at the beginning + + :param delim: The delimiter for a bank name + :param vobj: The object + :return: + """ + name_components = str(vobj).split(sep=delim) + + if len(name_components) > 2: + raise Exception(f'{type(vobj).__name__} {vobj.name} appears to have multiple instances of the delimiter.') + if len(name_components) == 2: + return vobj + else: + return None + + +def bank_comps(delim: str, vobj: Union[Lfo, Preset, Wavetable]) -> Tuple[str, str]: + """ + Return the bank name and the name of the object with the bank bits stripped out. + + :param delim: + :param vobj: + :return: + """ + + name_components = str(vobj).split(sep=delim) + if len(name_components) > 2: + raise Exception(f'{type(vobj).__name__} {vobj.name} appears to have multiple instances of the delimiter.') + if len(name_components) == 2: + bank_name = name_components[0].strip() # Clean out leading and trailing white space + obj_name = name_components[1].strip() # Same + return bank_name, obj_name + else: + raise Exception('Vital Object Name does not appear to contain a bank component.') + + +def main(): + banks: Dict[str, Bank] = dict() + + for clstype, typedata in TYPE_DATA.items(): + subdir = USER_DIR.joinpath(typedata[0]) + for f in subdir.glob('*'): + if not f.is_dir() and f.suffix == f'.{clstype.EXTENSION}': + vobj = clstype.from_file(filepath=f) + if is_bank_obj(delim=BANK_DELIM, vobj=vobj) is not None: + bank_name, obj_name = bank_comps(delim=BANK_DELIM, vobj=vobj) + banks.setdefault(bank_name, Bank(bank_name=bank_name, lfos=None, presets=None, wavetables=None)) + vobj.rename(new_name=obj_name) # Set the "clean" name + banks[bank_name].elements[clstype].append(vobj) + + for bank in banks.values(): + print(bank.name) + for ct, ctd in bank.elements.items(): + print(f'\t{ct.__name__}') + for i in ctd: + print(f'\t\t{i}') + + print(f'Writing Bank {bank.name}') + + if not BANK_DIR.exists(): + BANK_DIR.mkdir() + + bank.write_file(filedir=Path(BANK_DIR)) + + +if __name__ == '__main__': + main() diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..0323f54 --- /dev/null +++ b/config.json.example @@ -0,0 +1,5 @@ +{ + "base_preset_dir": "Path to your base vital preset dir here", + "bank_dir": "banks", + "delimiter": "$$" +} \ No newline at end of file