Quickstart

Creating a synth

A fresh Synth starts on Vital’s init preset.

import vita

synth = vita.Synth()
synth.set_bpm(120.0)
synth.set_sample_rate(48000)

set_sample_rate affects every subsequent render, so set it before rendering rather than between calls.

Loading and saving presets

Presets are JSON. You can go through a file or a string, and round-trip either way.

synth.load_preset("path/to/patch.vital")   # from a .vital file
json_text = synth.to_json()                # serialize current state
synth.load_json(json_text)                 # restore from a string
synth.load_init_preset()                   # back to the default patch

load_preset and load_json return False for a malformed preset, or one saved by a newer version of Vital, rather than raising.

A Synth also pickles, which is what makes it usable with multiprocessing:

import pickle

restored = pickle.loads(pickle.dumps(synth))

Controls

get_controls() returns a dict of live handles. Setting one takes effect on the synth immediately.

controls = synth.get_controls()
controls["filter_1_on"].set(1.0)
controls["filter_1_cutoff"].set(80.0)

print(controls["filter_1_cutoff"].value())        # 80.0
print(controls["filter_1_cutoff"].get_text())     # display string

Every control also has a normalized 0-1 view, which is usually what you want when sweeping parameters programmatically or feeding them from a model:

controls["filter_1_cutoff"].set_normalized(0.5)
print(controls["filter_1_cutoff"].get_normalized())

Control metadata

get_control_details describes a control’s range and how it should be presented.

info = synth.get_control_details("delay_style")
print(info.min, info.max, info.default_value)
print(info.scale)         # e.g. ValueScale.Indexed
print(info.is_discrete)   # True
print(info.options)       # ["Mono", "Stereo", "Ping Pong", "Mid Ping Pong"]
print(info.display_name, info.display_units)

Discrete controls take integer values within [min, max], and options gives the label for each. Continuous controls have an empty options list and are set with any float in range.

Modulation

Sources and destinations are addressed by name. The full sets are available up front:

print(vita.get_modulation_sources())        # 'lfo_1', 'env_2', 'random_1', ...
print(vita.get_modulation_destinations())   # 'filter_1_cutoff', 'osc_1_level', ...
synth.connect_modulation("lfo_1", "filter_1_cutoff")
synth.get_controls()["modulation_1_amount"].set(1.0)

synth.disconnect_modulation("lfo_1", "filter_1_cutoff")
synth.clear_modulations()   # remove all of them at once

connect_modulation returns False if every modulation slot is already in use.

Rendering

render plays one note and hands back a NumPy array shaped (2, render_dur * sample_rate) – channel-major, float32.

audio = synth.render(
    midi_note=60,
    midi_velocity=0.7,   # 0-1, not 0-127
    note_dur=1.0,        # seconds the note is held
    render_dur=3.0,      # seconds of audio returned, including the tail
)

render_dur should exceed note_dur by enough to capture the release tail; anything still sounding at the end is faded out to avoid a click.

To write a wav directly, skipping NumPy:

synth.render_file("out.wav", 60, 0.7, 1.0, 3.0)

Most audio libraries want frames-major data, so transpose on the way out:

from scipy.io import wavfile

wavfile.write("out.wav", 48000, audio.T)

Constants

Discrete controls have named values in vita.constants, which is easier to read than the raw integers:

from vita.constants import SyncedFrequency, Effect, FilterModel

synth.get_controls()["lfo_1_tempo"].set(SyncedFrequency.k1_16)

Next: rendering many presets in parallel.