A command-line tool and Python library for Novation V-Station patch and bank
files. It reads and writes .fxb, .fxp, .fst, .aupreset and SysEx .mid,
converts freely between them, and builds new banks out of individual presets.
V-Station's own file handling is awkward in a few specific ways, and this tool works around all of them:
- The plugin has no way to export its presets one-by-one, so sharing a single patch means shipping a whole bank.
- Its Audio Unit and VST formats don't interchange, so a bank collected on one platform is unusable on the other.
- A bank dumped over MIDI comes back without any names attached.
Every V-Station container holds the entire 400-program bank, plus a pointer
to whichever program is "current". A .fxp or .fst is not a single patch —
it is the full bank with the pointer aimed at one slot. The same is true of an
.aupreset.
Three consequences that explain most of this tool's behaviour:
- Extracting a preset means slicing one program out of a bank.
- Building a bank means starting from an existing bank and overwriting slots.
- Anything that creates a bank needs a
--templateto supply the slots you aren't setting. Without one you'd get 400 empty programs.
A good template is a Default.fxb exported from a fresh instance of the plugin.
Most examples below assume you have one.
python3 -m venv .venv
source .venv/bin/activate
pip install -e .That puts a vstation-tool command on your path.
Run vstation-tool <command> --help for the full set of options.
Start here with anything unfamiliar. It reports the container type, the program
the file points at, and for a .mid which SysEx flavour it is.
vstation-tool info bank.fxb
vstation-tool info "V-Station - Pad2.fxp"
vstation-tool info dump.midfile: bank.fxb
container: fxb
num programs: 400
pointer: 0 ('Bass1')
Names come from the bank's own name table, so they're the real names the plugin displays.
vstation-tool list bank.fxb
vstation-tool list bank.fxb --filter lead* 0 Bass1
1 Hard Lead1
2 Pad2
With no -i, you get whichever program the file itself points at — the natural
thing to want from a .fxp or .fst:
vstation-tool extract "V-Station - Pad2.fxp" -o Pad2.fxpOr name the program you want, by index or by name:
vstation-tool extract bank.fxb -i 42 -o preset.fxp
vstation-tool extract bank.fxb -i "Pad2" -o Pad2.aupresetExport a whole bank as one file per preset with --all. Here --out is a
directory and --format is required:
vstation-tool extract bank.fxb -o "au/My Bank" --all --format aupresetThird-party banks usually only customise 100–160 of the 400 slots and leave the
rest as stock factory presets. --exclude-duplicates-of skips those, so you
export what the bank actually adds:
vstation-tool extract bank.fxb -o "au/My Bank" --all --format aupreset \
--exclude-duplicates-of Default.fxbMatching is by parameter content at any index, not by slot position, so it still works when a bank has shuffled the factory presets around.
vstation-tool convert bank.fxb bank.aupreset
vstation-tool convert bank.fxb bank.fxp
vstation-tool convert bank.fxb bank.fst --out-template Default.fstWriting .fst needs --out-template, an existing .fst to splice the data
into — the FL Studio wrapper around it isn't reproduced from scratch.
Older V-Station versions use a different chunk layout. --legacy writes that
instead, and is only valid for .fxb output:
vstation-tool convert bank.fxb old-plugin.fxb --legacyTakes any mix of .fxp, .fst, .mid and .aupreset, and splits into
Leads_1.fxb, Leads_2.fxb, … past 400 presets:
vstation-tool pack ./leads -o ./leads/Leads.fxb --template Default.fxbNames come from filenames by default. That's deliberate: a preset saved on
its own usually carries a generic internal name (User 341, Import 128) that
nobody ever changed, so the filename is the only real name it has. Use
--name-source internal for collections you know carry proper embedded names.
Places each patch at a slot you choose:
vstation-tool build MyBank.fxb --template Default.fxb \
--patch 0=Bass1.fxp --patch 1=Lead3.fxp --patch 12=Pad2.aupresetWithout an explicit INDEX=, a .fxp or .aupreset goes to the slot it
already points at. A .mid always needs an explicit index.
--dir merges a whole directory of .fxp files, each at its own embedded
index:
vstation-tool build MyBank.fxb --template Default.fxb --dir ./presetsWrites all 400 programs, names included, as a single .mid. It round-trips
through this tool losslessly:
vstation-tool export-bulk-midi bank.fxb bank-backup.midThree flavours are read, and info tells you which one you have:
| Flavour | Contents |
|---|---|
| single-patch dump | one program with its name |
| bulk bank dump | all 400 programs, parameters only — no names |
| this tool's bulk format | all 400 programs with names |
A bulk bank dump carries no name table at all, so merge it into a template and supply the names yourself:
from vstation_tool import io_ops, blob, midi
base = io_ops.load_blob("Default.fxb")
bank = midi.read_native_bulk_dump("dump.mid", base)
for i, name in enumerate(names_from_your_patchlist):
bank = blob.set_name(bank, i, name)
io_ops.save_blob("recovered.fxb", bank)Such dumps usually ship alongside a printed patchlist. Two things worth
checking before you trust one: the numbering often starts at 100 or 300 while
the patches themselves occupy slots 0..N, and names longer than 20 characters
get truncated.
The CLI is a thin layer over the library, and the library is easy to script against:
from vstation_tool import io_ops, blob
bank = io_ops.load_blob("bank.fxb")
print(blob.get_pointer(bank)) # currently selected program
print(blob.get_name(bank, 0)) # 'Bass1'
print(blob.all_names(bank)) # {0: 'Bass1', 1: 'Hard Lead1', ...}
bank = blob.set_name(bank, 0, "My Bass")
bank = blob.set_slot(bank, 5, blob.get_slot(bank, 0)) # copy a program
io_ops.save_blob("edited.fxb", bank)io_ops.load_blob and io_ops.save_blob dispatch on the file extension, so the
same two calls cover every supported format.
- Names are capped at 20 characters. Longer ones are silently truncated.
- Anything producing a bank needs
--template. Skipping it gives you 400 empty programs rather than a bank with a few presets in it. .fstoutput needs--out-template. The tagless.fstvariant can be read but not written.--legacycan't represent program indices above 255, because that format stores the index in a single byte..aupresetfiles are exactly 59610 data bytes with a leading0x03. Anything else won't load, which makes it a quick sanity check on output.- The plugin checks state length exactly and silently loads nothing on a mismatch. If a converted file appears to load but does nothing at all, check the size first.
After any bulk operation, these are cheap and catch most mistakes:
from vstation_tool import io_ops, blob
bank = io_ops.load_blob("out.fxb")
print(blob.get_pointer(bank), blob.get_name(bank, 0))
print(max(len(n) for n in blob.all_names(bank).values())) # must be <= 20Round-tripping an untouched bank through the same path should return it
byte-identical. One caveat: a Default.fxb straight from the plugin has an
incorrect byteSize field, so a round-trip of that particular file differs by
one byte at offset 7 — that's the tool writing the correct value.
Extracting a bank to loose presets and packing them back should also reproduce the original bank.
Python 3.9 or newer. No third-party dependencies.