Creating VCV Rack patch files from software

This is not a request for help, just me dropping some notes about something I’m experimenting with. Maybe some of my fellow plugin developers will find this interesting.

I’m intrigued by the idea of writing programs that output VCV Rack patches. I have this vague idea that I could define “recipes” for patches based on my own experience as a modular synth enthusiast, then the program could generate variations on the recipe.

In cooking, making gravy is basically the same thing as making pudding: they both involve cooking flour in a little fat and gradually stirring in a water-soluble base. The result can be very different depending on whether you add savory stuff (gravy) or sweet stuff (pudding).

In the same vein, we modular synth people all have recipes, basic stuff like “VCO goes through VCA, with VCA level controlled by ADSR envelope, …”. We have conceptual patterns in our minds, from which we can derive an astronomical number of actual patches by substituting specific modules for general ideas.

So my first idea is to experiment with reading/writing the VCV file format. Here is a working Python program that opens a vcv file, extracts the patch JSON out of it, then turns around and re-creates another vcv file.

#!/usr/bin/env python3
import zstandard as zstd
import io
import tarfile
import json
from typing import Optional, Any, Dict, cast


def ExtractVcvPatchJson(inVcvFileName:str) -> Optional[Dict[str, Any]]:
    # Decompress the vcv file into a tar image.
    with open(inVcvFileName, 'rb') as infile:
        decomp = zstd.ZstdDecompressor()
        reader = decomp.stream_reader(infile)
        buffer = io.BytesIO(reader.read())

    # Find the json contents inside the tar image.
    with tarfile.open(fileobj=buffer) as archive:
        for member in archive.getmembers():
            if member.name == './patch.json':
                if extracted := archive.extractfile(member):
                    return cast(Dict[str,Any], json.load(extracted))

    # Unable to find patch.json inside the tar image.
    return None


class Patch:
    def __init__(self, patch: Dict[str, Any]) -> None:
        self.dict = patch

    def writeJson(self, outFileName:str) -> None:
        with open(outFileName, 'wt') as outfile:
            outfile.write(json.dumps(self.dict, indent=4))

    def writeVcv(self, outFileName:str) -> None:
        # Create in-memory tar archive
        tarbuf = io.BytesIO()
        with tarfile.open(fileobj=tarbuf, mode='w') as tar:
            # Add patch.json
            binary = json.dumps(self.dict, separators=(',', ':')).encode('utf-8')
            patch_info = tarfile.TarInfo(name='patch.json')
            patch_info.size = len(binary)
            tar.addfile(patch_info, io.BytesIO(binary))

            # Add empty modules/ directory
            modules_info = tarfile.TarInfo(name='modules/')
            modules_info.type = tarfile.DIRTYPE
            tar.addfile(modules_info)

        # Compress tar archive with Zstandard
        tarbuf.seek(0)
        compressor = zstd.ZstdCompressor(level=3)
        with open(outFileName, 'wb') as outfile:
            outfile.write(compressor.compress(tarbuf.read()))

    @staticmethod
    def ReadVcv(inFileName:str) -> 'Patch':
        pdict = ExtractVcvPatchJson(inFileName)
        if pdict is None:
            raise Exception('Cannot load VCV Rack patch from file: ' + inFileName)
        return Patch(pdict)


if __name__ == '__main__':
    patch = Patch.ReadVcv('input/beetle_breakfast.vcv')
    patch.writeJson('output/beetle_breakfast.json')
    patch.writeVcv('output/beetle_breakfast.vcv')

I thought I would share this in case it inspires anyone else to write programs to read or write vcv format.

Also, I’m interested to hear from anyone else who has written code to generate novel patch files. Again, I’m not yet sure where this is leading, but my intuition is strongly pulling me in this direction, and I’ve learned to trust that feeling…

3 Likes

Interesting approach!

Probably it all comes down to how many degrees of freedom the program has. Will it just reproduce some of the developer’s favorite patch layouts or will it actually create new things and new sounds? Sort of like “Randomize” for the whole rack, including choice of modules and cables

The latter is probably extremely difficult to achieve unless you are willing to accept a lot of misses between a few hits with patches that actually produce anything meaningful.

Thanks for sharing. I actually have been doing the same recently. With json and python, it’s all super straightforward to do actually and a lot of fun.

While I haven’t gotten as far as trying to actually create whole patches I have been using python to analyze or modify patches, presets, or selections.

It started with a desire not to have to use the file manager with Voxglitch Wavbank. Every time you have to use windows’ directory picker

which always starts you in an inconvenient place so you have to drill down to the correct folder every time and then you also have to save the preset so I just made a blank preset and used python to walk the whole sample library and make presets for everything so now my wavbank preset menu mirrors the folder structure for my sample library.

(in case anyone else want to test it out, here’s the link: rack_scripts/mksamplerpresets.py at main · dustractor/rack_scripts – you just need to supply the location of your samples folder with the --path argument.)

I also used this approach of making presets for a couple other modules such as:

a script for voxglitch autobreak – it doesn’t walk a folder structure, you have to supply the path with loops and it makes a preset for five samples until it runs out of enough samples to make the full five. I need to go back and work on that one now that I figured out how to do drag-and-drop with tkinter.

a bunch of one-off scripts to make patterns for voxglitch onezero and sickozell trigseq+ where it makes patterns based off of decimal expansions of irrational numbers. (I was bored!)

a script that read midi files, extracted the pitch information ignoring timing or simultaneous notes and dumped that into text files and made presets for onepoint.

I toyed around with the idea that it would be cool to get graphviz to render a patch as a diagram so I did get as far as figuring out that vcv patches are compressed with zstandard but then I realized that you don’t get any information about the names of inputs or outputs so the diagram is not very informative:

In another attempt at reading vcv patches, I made a script that reads all the patches in a directory and looks for instances of the wavbank module and then reverse-engineers which sample is actually selected based off the position of the SampleSelectKnob (which is useless if you’re using the wave selection input which I usually do.) I wish there were a way to interact with a patch ‘live’ so that I could get this value.

I’m still learning c++ and the rack api but I’m hoping it will be possible to make a module that can hook into the dataToJson event (for other modules or the whole patch?) and divert that data to a python script for further processing.

The most advanced script I have made so far – last week I figured out how to get tkinter to accept a drag-and-dropped list of files/folders so I made a thing where you save a patch selection (in this case it was – big-surprise – a bunch of wavbank modules already hooked up to a mixer and a sequencer and a sample-randomizer) but none of the wavbanks had a folder selected yet. The script lets you drag some folders onto a window and it saves a patch (or strip) selection with those folders selected in the wavbanks. It worked for the patch selections but when I tried loading a strip it crashed.

I haven’t even begun to consider making something that actually constructed a patch by adding modules, connecting cables, positioning things. That seems like way too much to consider.

2 Likes

Funny enough, sickozell trigseq+ is also how I started down this rabbit hole. Oh cool, there’s a bunch of values in the vcvm, what if I use python to generate a bunch of these presets. Then started doing that for other modules to make vcvm files, similar to your example of pointing to a folder for modules that save a reference to a file path in the vcvm.

1 Like

FWIW, the starting folder of file pickers can be specified. If it’s always starting from the default location, then the plugin isn’t doing what it needs to do to make picking files friendlier. So, open an issue with the developer of plugins that aren’t remembering where you last picked things from.

So in this case, both @dustractor and I are referring to having multiple folders mapped in presets, where there might be different sets of files like samples. This is a lot easier than navigating through the os file browser.

1 Like