VOLTAreference

Reference / Samples and recording

Import an SFZ instrument

Samples and recording

Open in the workbench →
On this page · 5 sections

Turn an existing SFZ instrument into a Volta sample bank you can play by name.

What it is#

SFZ is a plain-text instrument format: a list of <region>s, each naming an audio file and the keys and velocities it answers. SFZ import converts one into a zoned bank in the Multisampled instruments manifest schema: it copies the referenced audio into your song's samples/<bank>/ folder, merges one entry into samples/index.json, writes a ready instrument module samples/<bank>/<bank>.volta (as Capture an instrument as samples does), and reports every opcode it did not map, with its file and line. Conversion happens once, on the host; the engine never reads SFZ.

Usage#

volta import sfz "Salamander Grand.sfz" --out mysong/
volta import sfz strings.sfz --name strings_ensemble
FlagMeaningDefault
--out DIR / -othe song folder; audio goes to DIR/samples/<bank>/, the entry to DIR/samples/index.jsonthe current directory
--namebank namethe .sfz file stem

Bank names are made URL-safe: lowercase letters, digits, _ and - (Salamander Grand.sfz → salamander-grand). The output lists the bank, its zone and region counts, every ignored opcode as file:line <header> name=value, and every warning. The same text is saved as samples/<bank>/sfz-import.txt.

Desktop Studio: drop a .sfz file, or a folder with .sfz files at its top level (the usual layout of an SFZ library), anywhere on the workbench. Each instrument becomes one bank under the song's samples/, the bank reloads, and a toast gives the summary, for example imported tinypiano (tinypiano: 7 zones from 8 regions, 2 opcodes ignored, 1 warning). The full report is in sfz-import.txt. A folder that contains no .sfz imports as a plain variant bank, as before. The browser workbench does not accept drops yet.

Play the result through the generated module, or like any zoned sample:

import "samples/salamander-grand/salamander-grand" (salamanderGrand)
play salamanderGrand !"c2 e3 g4 b5"
keys = patch { source = sample "salamander-grand", ampEnv = adsr 2ms 0ms 1 600ms }
play keys !"c2 e3 g4 b5"

The generated module (it compiles standalone; the bank name resolves at run time) records the source file and what the bank holds, and exposes one patch — named after the bank as a binding, salamander-grand → salamanderGrand, with an sfz prefix when the name would start with a digit — plus a demo track over the recorded span, so opening it as a song makes sound:

-- tinypiano — imported from tinypiano.sfz (generated by volta import sfz)
-- Bank "tinypiano": 7 zones, roots C3..C5, 2 velocity layers, 2 round-robin slots, 1 release zone.
-- Use it from a song beside samples/: import "samples/tinypiano/tinypiano" (tinypiano)

tinypiano = patch {
  source = sample "tinypiano",
  ampEnv = adsr 2ms 0ms 1 400ms,
  gain   = vel * 0.7 + 0.3
}

demo = play tinypiano !"c3 g3 c4 c5"

A re-import rewrites the module. The CLI prints its path and the import line; the report file ends with it too.

What converts#

The parser handles // and /* */ comments, #define $NAME value, #include "file" (looked up beside the including file, then beside the root .sfz), values with spaces (sample=Grand C4.wav), and backslash paths. Headers follow SFZ v2 inheritance: <global> → <master> → <group> → <region>, with the most specific level winning. A new <global> clears master and group, and a new <master> clears the group. key= sets lokey, hikey and pitch_keycenter at its own level. Note names use C4 = 60 (c#4, db4, c-1), shifted by <control> octave_offset / note_offset. Sample paths are <control> default_path + sample, relative to the root .sfz file's folder.

SFZManifest zone field
samplefile — copied to <bank>/<its relative path>, each component made URL-safe (# → s, other unsafe characters → -; .. dropped; name collisions get -2)
pitch_keycenter (or key)rootPitch as a note name; default C4. pitch_keycenter=sample reads the audio file's WAV smpl chunk MIDI unity note (dwMIDIUnityNote); a file without one (or not a WAV) keeps C4 with a warning
lokey, hikeyloKey, hiKey. Omitted when the region gives no key range, so the loader's Voronoi split applies
lovel, hivelloVel, hiVel (clamped to 1..127). Omitted when the region covers every velocity
seq_length > 1 + seq_positionrr (1-based)
lorand / hirandrr slots numbered in lorand order within each key/velocity cell, plus "rrMode": "random" on the entry
trigger=release (release_key too)"trigger": "release"
xfin_lokey/xfin_hikey, xfout_lokey/xfout_hikeyxfadeKeys = the wider span
xfin_lovel/xfin_hivel, xfout_lovel/xfout_hivelxfadeVel = the wider span
volume, group_volume, master_volume, global_volume, amplitudegain as a dB string ("-9.02dB"), summed
tune (alias pitch), transposetune in cents (tune + 100 × transpose)
offset, endoffset, end — sample frames, as SFZ writes them (end is the last frame played)
loop_mode (alias loopmode)loopMode: "continuous", "sustain", "oneShot"; no_loop writes nothing
loop_start, loop_end (aliases loopstart/loopend)loopStart, loopEnd — frames; loopEnd is SFZ's inclusive loop_end + 1 (the manifest's end is exclusive)
ampeg_attack, ampeg_decay, ampeg_release (seconds), ampeg_sustain (percent), on a release regionampAttack, ampDecay, ampRelease, ampSustain (0..1): the release one-shot's own envelope. On attack regions they are consumed with one warning: attack zones play through the patch's ampEnv

Zones are ordered attack-first, then by root, then loudest layer first, because the loader keeps the first zone under the flat key used by sound hits and the waveform view.

Reported, not converted: every other opcode (envelope stages with no zone field such as ampeg_hold/ampeg_delay, fileg_*/pitcheg_*, filters, amp_veltrack, group/off_by choke groups, keyswitches sw_*, CC modulation, <curve>/<effect>/<midi> headers and their opcodes). Each one is listed once at its source line, however many regions inherit it. Warnings cover approximations and skips:

  • A region is skipped if it has no sample, uses a built-in generator (*sine, *noise), names a missing audio file, has lokey > hikey or lovel > hivel, has a seq_position outside seq_length, has end ≤ 0 or amplitude=0, or has an empty lorand/hirand range.
  • Unequal lorand/hirand weights are approximated as uniform. Ranges that do not cover [0, 1) still always sound one slot.
  • Asymmetric crossfades are written as the wider span.
  • pitch_keycenter=sample on a file with no smpl unity note: C4 is used.
  • ampeg_* on attack regions (once per instrument).
  • trigger=first/legato are treated as attack.
  • An entry mixing seq_length and lorand round-robins gets rrMode "random" for both.
  • A missing #include, an include cycle, or unparsable text.

An instrument with no playable region is an error and writes nothing. A re-import reuses audio files already present with the same length and rewrites the entry (last write wins per bank name, the loader's rule). The merge edits only that bank's entry in the TEXT of index.json (volta_sfz::upsert_entries): every other entry keeps its formatting and order — one-line hand-written entries stay one line — and a new bank is appended at the file's own indentation. A file that is not a JSON object is refused, not overwritten.

What plays today#

Every field above is written now. Which ones sound depends on the engine stages:

  • Now (Sampling Workflow Stage 5): file, rootPitch, loKey/hiKey, loVel/hiVel, rr and rrMode, xfadeKeys/xfadeVel, trigger: "release" with its amp* envelope, gain, tune and offset/end, on desktop, in the browser and in every export (Multisampled instruments). engine/tests/multisample_depth.rs plays the converted tinypiano fixture through the zone table.
  • Loops (Stage 3): loopStart/loopEnd become the zone buffer's loop points, and loopMode turns the loop on without a |> loop in the song: continuous loops through the release, sustain leaves the loop at note-off; oneShot plays through. An explicit loop wins.

How it's implemented#

  • sfz/ (crate volta-sfz, dependency-free apart from serde_json): parse.rs turns text into regions with merged opcodes plus every opcode occurrence with its level. convert.rs is pure (no filesystem): regions → Zones → the §6 manifest entry, with the ignored/warning report. import.rs does the copy, the index.json merge, the smpl unity-note read (resolve_sample_roots, smpl_unity_note), the generated module (module_source, patch_name) and the report file.
  • CLI: cli/src/sfzimport.rs, dispatched before the MIDI/DAWproject import handler in cli/src/main.rs.
  • Desktop: app-native/src/samples.rs::import_files routes .sfz files, and folders with top-level .sfz files, through volta_sfz::import_sfz on the existing volta-import worker. The bridge result ImportedSamples { names, notes } (ui/src/bridge.rs) carries one summary line per instrument into the toast (ui/src/app/dropimport.rs).
  • Tests: parser and mapping unit tests in sfz/src/{parse,convert}.rs. The fixture instrument sfz/tests/fixtures/tinypiano/ has groups, two velocity layers, round-robins, a release group, an #include and a #define; its WAVs are generated in-test, and it must convert to tinypiano.expected.json (sfz/tests/suite.rs; SFZ_BLESS=1 rewrites the file) and write a module that compiles; the suite also reads a WAV with a hand-built smpl chunk (unity note A3) beside one without, and maps ampeg_* on a release region. cli/tests/sfz_cli.rs drives the binary. The app-native test a_dropped_sfz_folder_imports_as_one_zoned_bank_the_loader_reads loads the converted bank through the desktop loader into its (root, buffer) zone table.