Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Vital preset file

This page states what is known about the .vital preset file. No official format document exists. The facts come from the open-source snapshot of Vital, from renders of its open engine and from sampled presets.

Every row names a build, a status and a source.

  • Builds. source 1.0.6 is the open-source snapshot at commit 636ca0e. open engine is that engine compiled into the vita 0.1.0 Python binding, which reports itself as 99999.9.9. A retail build is named by its version, such as 1.0.7.
  • Statuses.
    • checked: read in the source.
    • verified: confirmed by a probe or a load check of ours on the named build.
    • reported: an outside source says so.
    • open: unknown.
    • untestable by rule: a check that needs a step the project never takes, such as running the app.

A row verified on the open engine is not verified on retail. The pinned retail build is stable 1.0.7. The early-access build is 1.6.4 (reported). The checks still open are listed under Open items.

File and top-level keys

FactBuildStatusSource
A preset is one compact JSON text.source 1.0.6checkedload_save.cpp
The top level holds synth_version, preset_name, author, comments, preset_style, macro1 to macro4 and settings.open engine, 1.0.7verifiedprobe: saved default states
settings holds 772 numeric controls and four structured parts: sample, modulations, wavetables and lfos.open engine, 1.0.7verifiedprobe: saved default states
The app saves the keys of each object in alphabetical order.1.0.7verifiedload check: the re-save of Bass
A re-save keeps every control within one 32-bit float step and rewrites the version of each wavetable to the app’s build. A load and save rewrites the sample: our noise sample came back 4 frames later with some values one step off, so compare a re-saved sample by its name, length and rate, not its bytes.1.0.7verifiedload check: the re-save of Bass; the open engine keeps the sample
A file without synth_version, settings or settings.sample, or a sample without length, sample_rate or samples, makes the app throw.source 1.0.6checkedload_save.cpp
A missing control takes its default.open engineverifiedprobe: minimal.vital reads back 772 controls, each at the fresh engine’s default
An unknown key loads and is dropped on save, in settings and at the top level.open engineverifiedload check: the unknown-keys variant
A file without preset_name and author loads.open engineverifiedload check: the no-name variant
An out-of-range control is applied as it is. The loader does not clamp.source 1.0.6checkedload_save.cpp
Factory presets hold values outside the table range, such as a dry/wet above its maximum or the 32-bit float form of a bound. preset apply warns about such a value in a key no edit touched and refuses it in an edited key.1.0.xverifiedobserved in factory presets of Vital 1.0.x
A text in a control fails the load. A bool loads. The writer of this project writes neither.open engineverifiedprobe: filter_1_cutoff set to "x" and to true
The app overwrites preset_name with the file name on open.source 1.0.6checkedsynth_base.cpp
The writer of this project prints each number as the shortest text that reads back to the same value. 1E5 becomes 100000.0. Values stay bit for bit.this projectverifiedunit test number_text_is_canonical_and_values_are_kept

Version gate

The app compares the major.minor part of synth_version with its own build. The patch number is ignored.

CaseBuildStatusSource
A file whose major and minor are newer is rejected with a dialog.source 1.0.6checkedload_save.cpp
A file one patch newer loads.source 1.0.6checkedload_save.cpp
An older file runs about 30 upgrade steps before it loads. Files from 1.0.0 on need none.source 1.0.6checkedload_save.cpp
The plugin state path ignores a rejected file. The patch keeps its last sound, and no dialog appears.source 1.0.6checkedsynth_plugin.cpp
The stamps 1.0.6, 1.0.9, 1.1.0, 1.5.5 and 0.9.0 load. 100000.0.0 and a text that is not a version are rejected.open engineverifiedprobe 5 and the stamp fixtures
A file stamped 1.0.6 loads, with every edited control in place.1.0.7verifiedload check: Bass
Files stamped 1.5.5, 1.6.0 and 1.7.0 are refused with the dialog “Preset was created with a newer version”.1.0.7verifiedload check: the stamp variants
The app writes its own build, 1.0.7, as the stamp on save.1.0.7verifiedload check: the re-save of Bass
Presets saved by 1.6.x do not open in 1.5.5.1.5.5reportedVital forum
Which stamps the early-access 1.6.4 accepts.1.6.4openoptional forward check

Stamp rule. Files built from the built-in template carry 1.0.6, the open-source build, as DEFAULT_STAMP. Stable 1.0.7 loads them: the gate compares only major and minor, and files from 1.0.0 on run no upgrade step. preset apply keeps the stamp of its template and never lowers it. A template older than 1.0.0, or one that holds sub_octave, is refused. For the early-access builds, pass a template saved by your own app with --template.

Retail-only keys

Retail builds write keys that the open source does not know.

KeyTypeFirst seenDefaultStatusSource
custom_warpslist of line-generator states, one per oscillator1.5.3openchecked8 of 24 sampled presets
random_valueslist of {seed} objects, one per random LFO1.5.3opencheckedthe same sample
osc_1_spectral_morph_phase to osc_3_number1.5.3opencheckedthe same sample

Stable 1.0.7 writes none of them. It loads a file that holds them, and so does the open engine, which drops them on save (verified, load check: the retail-keys variant). preset inspect marks them. Whether builds 1.6.x add more keys is open.

Template

FactBuildStatusSource
The built-in template holds all 772 controls at their registry defaults, 64 empty modulation slots, three sawtooth wavetables, eight triangle LFO shapes and a seeded noise sample.this projectcheckedsrc/preset/vital/template.rs
Its key set and every default match a fresh open engine and the saved default state of 1.0.7.open engine, 1.0.7verifiedprobe 8: 0 keys differ
It loads, and every control reads back unchanged.open engineverifiedload check: the template variant
The built-in template is not the app’s Init preset. Its wavetables and sample are our own.this projectcheckedsrc/preset/vital/template.rs

Parameters and laws

The registry in src/preset/vital/registry.json holds 794 controls. 22 are legacy keys that no module creates. Our own tool extracts the names, ranges, defaults, engine scales and labels from the source; the units, laws and provenance are ours.

FactBuildStatusSource
The open engine and 1.0.7 create exactly the 772 non-legacy controls.open engine, 1.0.7verifiedprobe: key sets
The 1.0.7 default state holds all 772 controls at the registry default. These carry verified_on = 1.0.7, which confirms the key and the default only.1.0.7verifiedload check: the 1.0.7 default state
The open engine reports the same range, default and first build as the registry for 763 controls; the other 9 are listed below. The registry evidence says which.open engineverifiedprobe: control details
A filter cutoff follows Hz: stored 66.86 shows about 390 Hz.1.0.7verifiedprobe: a UI edit read back from the saved file
Display laws read and typed in the value box, each matching the registry law: portamento_time is seconds (0.5 s stores -1), osc_1_unison_detune is percent (25 % stores 5), filter_1_cutoff shows semitones (12 stores 72), distortion_drive is dB (12 stores 12). The registry tags these four measured, with verified_on = 1.0.7.1.0.7verifiedprobe: values typed in the app and read back from the saved file
A route amount shows in the destination’s units, as the amount times the destination’s stored span. On filter_1_cutoff, span 128, 64 semitones stores 0.5. preset apply converts a semitone amount the same way.1.0.7verifiedprobe: a value typed in the app; unit tests of the edit list
An LFO tempo menu stores the index of its label: 2/1 is 5 and 1/4 is 8.1.0.7verifiedprobe: a UI edit read back from the saved file
The macOS bundles of 1.0.7 are x86_64 only, checked with lipo -archs on 2026-10-26. A plugin host running natively on arm64 cannot load them. On Apple silicon an x86_64 app runs under Rosetta.1.0.7verifiedprobe 1; Rosetta is a platform fact
delay_tempo and delay_aux_tempo show the menu label at the stored value itself. Value 4 is 4/1. The vita binding counts from the range start instead and shows Freeze.source 1.0.6checkedsynth_slider.cpp; the binding differs
filter_*_style has values 0 to 9 and five labels.source 1.0.6 and open engineverifiedprobe: control details
osc_*_destination and sample_destination allow 0 to 14, but only 0 to 13 have labels. The open engine reports 13 as the top.source 1.0.6checkedsynth_parameters.cpp; the binding differs
An oscillator level is the square of its stored value: .open engineverifiedrender: halving the stored level quarters the amplitude
The outer unison offset is detune_range times unison_detune squared, in cents.open engineverifiedrender: range 2 and detune 10 give 200 cents
Unison voice offsets follow laws::unison_offsets for 4 and 8 voices at powers 0 and 1.5, within 0.03 cents. Odd counts are not rendered.open engineverifiedrender, unit test offsets_match_the_render
Unison gains follow laws::unison_gains for 3, 4, 5 and 8 voices, within 0.001 of the single-voice amplitude. A gain belongs to a pair of voices, which share it equally.open engineverifiedrender, unit test gains_match_the_render
The voice gain is , with envelope , velocity , velocity track and voice_amplitude .open engineverifiedrender, unit test the_amp_gain_squares_like_the_render
Laws tagged interim in the registry: resonance, filter drive, filter shape, distortion except distortion_drive, compressor, EQ, effect feedback, depth and mix, reverb, oscillator warp, curve powers, and the controls with no display unit.source 1.0.6checkedsrc/preset/vital/registry.rs; each carries a tuning note

The interim laws wait for the render calibration. They are not settled here.

Modulation, LFO shapes, wavetables and names

FactBuildStatusSource
modulations holds 64 {source, destination} entries. A slot’s amount, bipolar flag, power, stereo and bypass are the controls modulation_N_*.open engineverifiedprobe: a fresh engine’s saved state
The 32 source names. A route from each of them loads.open engine, 1.0.7verifiedload check: the every-source variant; in 1.0.7 the matrix shows 32 rows, and the value box reads 12.8 for amount 0.1 into the cutoff
A route amount is a fraction of the destination’s stored range: .source 1.0.6checkedlaws::modulation_delta
Routes target numbers, counts, menus such as osc_1_transpose and lfo_1_tempo, and the amount or power of another slot, never a switch. preset apply refuses a switch and the bipolar, stereo and bypass flags of a slot.1.0.xverifiedobserved in factory presets of Vital 1.0.x
A Wave Source holds name, author, version, remove_all_dc, full_normalize and groups. A component holds type, interpolation, interpolation_style and keyframes. A keyframe holds position and wave_data.open engineverifiedprobe 9: the engine’s saved state has the same fields as ours
wave_data is base64 of 2048 little-endian 32-bit floats: 10924 characters. Positions run from 0 to 256.source 1.0.6checkedwave_source.cpp
Tables of 1 and of 257 keyframes load. One keyframe shows a single frame; with 257 the frame position sweeps the wave.open engine, 1.0.7verifiedload check: the wavetable variants
An LFO shape holds name, num_points, points, powers and smooth, with one power per point. A shape of 100 points loads.open engine, 1.0.7verifiedprobe 9 and the 100-point variant
A stored point y is drawn upside down: the output is , so a stored 0 is the top. The default triangle stores and peaks at the centre. LfoShape takes outputs and stores .1.0.7verifiedload check: a shape of rising stored values showed falling saws
effect_chain_order codes the effect list, top to bottom. Code 0 is Chorus, Compressor, Delay, Distortion, EQ, Filter, Flanger, Phaser, Reverb. Number the effects in that order. Each effect after the first adds a mixed-radix digit, the count of effects above it with a higher number, in base its position plus one. The first digit is the most significant: for from 1 to 8, . The codes run from 0 to .1.0.7verifiedprobe 10: lists rearranged in the app saved 260650, 8 and 181440; unit tests the_effect_order_codes_match_vital and every_effect_order_code_round_trips

Fixtures

The fixtures in tests/fixtures/vital/ are generated by cargo xtask vital-fixtures and listed with their SHA-256 in tests/fixtures/PROVENANCE.md. cargo xtask vital-fixtures --check checks them.

FileHoldsTests
minimal.vitalthe stamp and the three sample fieldsevery_fixture_meets_the_format_contract
marks.vitalevery value preset inspect marksthe preset-inspect command cases
routes.vitalthree routes, an LFO shape, a two-frame wavetablethe_routes_fixture_holds_its_generated_values, the_routes_wavetable_has_two_full_frames
stamps/*.vitalthe version-gate setthe_stamps_pass_the_version_gate

The round trip read, write, read is checked for every fixture by every_fixture_meets_the_format_contract.

Limits

FactBuildStatusSource
The reader refuses files over 64 MiB and nesting over 128 levels.this projectverifiedunit tests the_size_cap_applies_to_both_reads, deep_nesting_names_the_depth_limit
Three tables of 257 keyframes come to 8,422,404 bytes of base64. A sample adds more.source 1.0.6checkedwave_source.cpp
Parsing takes about 3 times the file size for long texts and up to about 38 times for short numbers.this projectverifiedmeasured in a release build
Ten minutes of fuzzing per target found no crash.this projectverifiedfuzz/: 5.4 million reads, 4.0 million round trips, 95 thousand writes

Builds seen

BuildSeen how
source 1.0.6read, commit 636ca0e
open engine (vita 0.1.0)probes and load checks of ours
stable 1.0.7probes and load checks of ours
1.5.3, 1.5.5sampled presets
early access 1.6.4reported, not checked

Terms

Factory presets, samples and wavetables are not redistributable, and no file here holds them. The source is GPL-3.0-or-later. This page states facts in our own words and copies no code, comment or table.

Plugin state

A host loads a preset into the VST3 plugin through its state, not through a file. The rows below come from the host spike (issue 244): the retail plugin loaded headless as a library in an x86_64 host process through vst3-host 0.10.0, with no editor window. The probe output is in issue 10.

FactBuildStatusSource
The plugin state is the preset JSON plus a tuning object, followed by a NUL.source 1.0.6checkedsynth_plugin.cpp
The macOS bundles hold only an x86_64 binary, so a host process must run as x86_64.1.0.7verifiedlipo -archs of the bundle
The plugin loads and renders with no editor window. The probe process wrote nothing to standard error and exited with code 0.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
The single audio output bus has 2 channels and reports itself inactive after the load. With the bus left inactive, the render was silent; after the host activated it, the render sounded.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch
The saved component state starts with VstW, then CcnK at byte 16. The preset JSON starts at byte 176 and is followed by a NUL, zero bytes and the text JUCEPrivateData. The JSON holds tuning and the app’s stamp 1.0.7. The plugin saves no controller state.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; state read-back
A bare preset JSON passed to IComponent::setState loads, with or without a trailing NUL. The saved state then holds the name SpikeProbe and the key filter_1_cutoff.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; state read-back
A host needs no VC2! envelope, the form some Python hosts save: the bare JSON loads.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
A state without tuning keeps the instance’s current tuning: setStateInformation loads tuning only when the key is there. The app reads no tuning from a preset file.source 1.0.6checkedsynth_plugin.cpp, synth_base.cpp
Tuning::jsonToState reads scale_start_midi_note, reference_midi_note, tuning_name, mapping_name and scale without a check, and default and mapping when present. A missing key or a wrong type throws, and the plugin then shows a dialog box. The default tuning starts the scale at MIDI 60 with reference 0 and the scale 0 to 12.source 1.0.6checkedtuning.cpp, synth_plugin.cpp
In one plugin instance, a preset with reference_midi_note 2 played MIDI 60 at 293.66 Hz. A preset without tuning loaded next still played at 293.66 Hz. With the default tuning added to its state, it played at 261.63 Hz.1.0.7verifiedrender_vital::a_tuning_does_not_carry_over_to_the_next_preset: minimal.vital, the lead material at 120 BPM
In a host, the plugin sets the engine’s tempo from the host’s tempo on every block, so beats_per_minute, in beats per second, follows the host whatever the preset holds. SoundEngine::setBpm divides the tempo by 60 in 32-bit floats.source 1.0.6checkedsynth_plugin.cpp, sound_engine.cpp SoundEngine::setBpm
The plugin stores f32(bpm) * f32(1 / 60), a multiply by the reciprocal: at 140 BPM it saves 2.3333334922790527, and at 100.3 BPM a value two f32 steps from the correctly rounded 100.3 / 60. A preset with beats_per_minute 2 rendered at another tempo saves that value.1.0.7verifiedrender_vital::a_render_at_another_tempo_passes_the_load_check at 140 and 100.3 BPM, and the_saved_tempo_follows_the_model_over_a_sweep: every whole tempo 4 to 1000, every tenth 4.0 to 400.0 and 600 random tempos on 0.01 and 0.001 steps
A loaded preset renders unlike the default patch: RMS 0.017 for the probe preset against 0.118 for the default patch.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch
Two renders of one patch differ sample by sample.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch
In one probe, offline against realtime processing changed the render by less than two repeat renders differ: a difference RMS of 0.0100 against 0.0132.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
Whether the block size changes the render by more than repeat renders differ. In one probe, blocks of 64 against 512 samples gave a difference RMS of 0.0235, against 0.0132 between two repeat renders. The 64-sample render ran in a second instance.1.0.7openhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
IMidiMapping returns a parameter for CC 1, CC 7, CC 64, channel aftertouch and pitch bend. A full bend up raised MIDI 60 by 2 semitones.1.0.7verifiedhost spike: default patch, MIDI 60 with and without a full bend; test a_full_pitch_bend_raises_the_note_two_semitones
CC 1 from the host moves a route from mod_wheel: with the route at -1 on osc_1_level, a full wheel lowered the level.1.0.7verifiedtest the_mod_wheel_reaches_a_route
A state stamped 1.7.0 loads nothing. The plug-in keeps its init patch and saves the init patch back.1.0.7verifiedtest a_newer_stamp_falls_back_to_init
A settings key that Vital does not know is gone from the state it saves after the load.1.0.7verifiedtest a_key_vital_drops_is_a_readback_mismatch
After a load, the saved state holds every control and the name as sent, a re-encoded sample.samples text and a new version in each wavetable.1.0.7verifiedrender of routes.vital
In one probe, a note right after the state load differed from a note after 0.5 s of silence by less than two repeat renders differ.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
A second instance in the same process, with blocks of 64 samples, loaded the probe preset and rendered it.1.0.7verifiedhost spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100
The standalone app’s --headless --render flags.1.0.7untestable by rulethe project never runs the app

Open items

Stable 1.0.7 loaded a written preset with its edits, refused the newer stamps and re-saved the preset with every control kept. The ignored test a_preset_saved_by_vital_keeps_the_edits reads that re-save from VITAL_SAVED_PRESET and passes. The unknown-key, no-name, every-source, LFO-shape and wavetable variants loaded in 1.0.7 with no dialog.

A bare preset JSON loads as plugin state in an x86_64 host (see Plugin state).

Still open:

  • optionally, the stamps and the retail-only keys of early access 1.6.4.
  • whether the block size changes a render by more than repeat renders differ. The recipe fixes blocks of 512 samples, so renders compare at one block size.

Sources