VOLTAreference

Reference / Samples and recording

Capture an instrument as samples

Samples and recording

Open in the workbench →
On this page · 7 sections

Turn an instrument into a reusable sample bank by recording notes across its range and keeping their root pitches organized.

What it is#

Autosample captures a synth into a keymapped multisample instrument: it sends note-ons out a MIDI port (or into a hosted plugin), records the audio return per (note, velocity), trims silence, normalizes the set with one global gain, and writes WAVs plus a generated .volta module and a manifest.json zones fragment (Multisampled instruments schema).

Implementation: the capture/ crate (volta-capture) — the async pipeline over three seams (MidiSink, AudioSource, CaptureSink), with the real midir/cpal seams behind its devices feature and the offline plugin rig behind plugins. Three front-ends drive the same core: the volta autosample CLI (cli/src/autosample.rs, arg grammar + terminal progress), the desktop workbench's File ▸ Autosample Instrument… dialog, and the browser workbench (see below).

Usage#

volta autosample --list
volta autosample --device "minilogue" --notes C1:C6 --step 3 \
    --vel 100,64 --dur 2s --tail 1s --out mysynth/

# hosted-plugin capture — no hardware, offline, faster than realtime
# (build with: cargo run -p volta-cli --features plugins -- autosample …):
volta autosample --plugin "Kontakt 8" --preset "grand piano" \
    --notes C1:C6 --step 3 --vel 100,64 --dur 2s --tail 1s --out grand/
FlagMeaningDefault
--device / -dMIDI output port, case-insensitive substringrequired
--inputaudio input device substringsystem default input
--notes LO:HInote range — names (C1:C6, F#2:a4) or MIDI numbers (24:84)required
--step Nsemitones between captured roots (off-grid HI is appended)3
--vel a,b,…velocity layers (1–127), one pass per value100
--durnote-on hold (2s, 500ms)2s
--tailpost-note-off ring captured1s
--channelMIDI channel 1–161
--out DIRoutput folderrequired
--nameinstrument name (Volta identifier)--out basename
--dry-runprint the plan, touch no hardware—
--resumeskip (note, vel) files that already exist—
--listlist MIDI outputs + audio inputs and exit—
--plugincapture a hosted instrument instead of hardware: a path, au: ref, or bare name resolved against the VST3/CLAP dirs (plugins cargo feature)—
--presetnamed preset from the Host instrument and effect plugins store, loaded before captureplugin default
--settlesilent pre-roll after activation/preset load (Kontakt streams its library asynchronously)1s
--srrender sample rate for --plugin capture48000

With --plugin the MIDI/audio device flags don't apply (--device is rejected); notes go straight to the in-process instance and advance renders 128-frame engine blocks instead of sleeping, so a big capture set takes seconds. Between shots the rig renders 250 ms un-captured so a ringing release never bleeds into the next file. The rest of the pipeline — trim, one-gain normalize, WAV/manifest/module emit — is identical to the hardware path.

Per shot the tool records from just before note-on through dur + tail, trims leading silence (onset at −34 dB of the shot's peak, 5 ms pre-pad) and the sub-noise-floor tail (−40 dB, 20 ms post-pad), and warns if a capture looks silent. Normalization is one gain for the whole set (loudest peak → 0.95), so velocity layers keep their true relative levels. Files are written atomically as they're captured — an interrupted run keeps every completed shot (unnormalized), and --resume picks up where it stopped (a resumed run's gain covers only its own captures; re-run without --resume for a perfectly uniform set).

Output#

mysynth/
  mysynth_c1_v100.wav     -- URL-safe names: cs4 not c#4 (fetched under /samples/)
  mysynth_ds1_v100.wav …
  mysynth_c1_v64.wav …    -- each velocity layer
  manifest.json           -- ONE zoned entry, every layer in it
  mysynth.volta           -- one patch + a demo track

manifest.json holds one entry under the instrument name; with several --vel values each zone carries its layer's loVel/hiVel (neighbouring layers split at their midpoint, capture::velocity_ranges), loudest layer first, so the engine switches layers by note velocity (Multisampled instruments).

{
  "mysynth": { "zones": [
    { "file": "mysynth/mysynth_c1_v100.wav", "rootPitch": "C1", "loVel": 83, "hiVel": 127 }, …
    { "file": "mysynth/mysynth_c1_v64.wav",  "rootPitch": "C1", "loVel": 1,  "hiVel": 82 }, …
  ]}
}

Older captures' per-velocity banks (mysynth_v64) still load unchanged.

The generated module (compiles standalone; sample names are resolved by the runtime, not the compiler):

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

demo = play mysynth !"c3 e3 g3 c4"

The workbench dialog (desktop)#

File ▸ Autosample Instrument… (ui/src/app/autosample.rs) is the same pipeline in-process: hardware pickers (MIDI out + audio in) or an installed-plugin + workspace-preset pair, the grid controls with a live shot-count preview, per-shot progress rows with peak meters, and cancel — which keeps every finished WAV and seeds resume so the next start finishes the set. Device lists come from a probe thread. The run happens on a worker thread (app-native/src/autosample.rs); a completed capture lands itself, on a second worker (only adopting the decoded bank touches the UI thread):

  • WAVs are captured straight into ~/Documents/Volta/samples/<name>/;
  • the zones fragment is merged into ~/Documents/Volta/samples/index.json (the workspace sample library every song resolves), editing only the instrument's own entry so the rest of the file keeps its formatting (app-native/src/samples.rs::merge_manifest_fragment, the same volta_sfz::upsert_entries edit SFZ import and drop-import use);
  • the generated module moves to ~/Documents/Volta/lib/<name>.volta, the first-hit location for import "<name>";
  • the live engine reloads the bank, so sample "<name>" plays zoned and velocity-layered immediately — no stop, no manual copying.

The browser workbench#

The same dialog works in the browser: the workbench's in-app File menu and the command palette carry the same Autosample Instrument… command. The pipeline itself runs in the UI wasm (autosample_drive, ui-wasm/src/web.rs); the page island (runtime/src/autosample-island.ts) executes the seams: Web MIDI output (substring port match), and a recorder worklet (runtime/public/worklets/capture-recorder.js) tapping a raw getUserMedia stream — echo-cancel/AGC/noise-suppression off, no limiter in the path. Onset trim absorbs Web MIDI + input latency (thresholds are relative to each capture's own peak by design). A completed run installs true keymapped, velocity-layered zones into the live wasm engine (stage_sample_zone with each zone's manifest object, then commit_sample_zones; ui-wasm/src/web.rs derives the land payload from the same manifest_fragment) and offers the whole set — WAVs, manifest.json, the module — as downloads. The browser keeps nothing across reloads: download the set to keep it (drop it into a checkout or the desktop workspace like any capture). Plugin capture is desktop-only (no native plugin hosting in wasm), and the first open's device probe is the permission gesture for Web MIDI + the microphone.

Using a CLI capture in a checkout#

  1. Copy the output folder to runtime/public/samples/mysynth/.
  2. Merge manifest.json's entries into runtime/public/samples/index.json.
  3. Import the module (drop mysynth.volta in your workspace / public/lib/) or paste its patch into a song. The runtime picks the nearest-root zone per note like any multisample.

(Or capture from the desktop dialog instead, which lands everything in the ~/Documents/Volta workspace automatically — see above.)

Verification#

The capture loop is factored over the seam triple; tests (capture/src/tests.rs) drive the whole pipeline against a fake synth on a virtual clock — a decaying sine that speaks 50 ms after note-on with velocity-scaled amplitude — asserting the file set, onset-trim within ±10 ms, the global (not per-file) normalization law, the one layered manifest entry (roots and loVel/hiVel per zone), that the fragment parsed by the engine's own manifest_zones selects the soft capture for a soft note and the loud one for a loud note at every root (layer edges included), the event stream's shape, cancel's Ctrl-C-equivalence (kept WAVs, no manifest, resume finishes), and that the generated module compiles. The workbench dialog has its own kittest suite (ui/tests/autosample_panel.rs) over a scripted bridge: caps gating, form→request validation, progress rows, and the cancelled-run resume seeding. End-to-end hardware capture has not been run in this environment (no synth attached); plugin capture was e2e-verified against real FM8/Kontakt via the CLI, and --list/--dry-run against real device enumerations.