VOLTAreference

Reference / Samples and recording

Resample your own sounds

Samples and recording

Open in the workbench →
On this page · 7 sections

Render a musical idea into audio you can reuse as a sample, turning synthesis and processing into new source material.

What it is#

bounceOf renders a track offline at load time into the sample bank, so rendered audio becomes material for further synthesis — the render-and-mangle workflow (bounce → distort → re-chop → bounce again) as source code instead of a DAW ritual.

chopLine = !"c2 ~ eb2 g2"

growl = patch { source = saw (freq pitch) |> drive 0.5,                -- generation 0, live
                ampEnv = adsr 4ms 160ms 0.5 180ms, gain = vel * 0.5 }
gen1  = bounceOf 4c (play growl bassline |> mbcomp 0.6)                -- print it
chops = patch { source = sample gen1 |> speed (const 1.5) |> chop 16,
                ampEnv = adsr 2ms 200ms 0.6 150ms, gain = vel * 0.5 }
gen2  = bounceOf 4c (play chops chopLine |> reverb 0.4)                -- print the print
  • Surface. bounceOf <duration> <trackExpr>, top-level binding only. The duration is a constant (4c cycles, or seconds — folded via the beat-0 cycle length); the track expression is anything play/sound accepts, including |> track-effect chains. The binding's type is Str: it IS the bank handle.
  • Use. sample <handle> anywhere a sample source goes. The handle is an ordinary value: tape2 = tape, a record field, a list element, an import or a function's result all name the same bank entry, because the receiver asks the VALUE what it is rather than matching the declaration's name (compiler/src/lower/handles.rs). Every sample verb works on the render: chop, chopAt, speed, stretch, granular, warp, pitched, … warping a bounce is a documented no-op (ratio 1.0, no fallback warning) — a bounce has no manifest bpm to declare, and native rate is correct anyway since it was rendered at the project's own tempo, not an external loop's.
  • Generations. A bounce's track may sample another bounce. The compiler orders the defs so dependencies bake first, whatever the source order; a feedback loop (a bounce that transitively samples its own output) is a compile error.
  • Determinism. Bakes run on the offline engine path (origin at sample 0, no realtime grace, coordinate-addressed RNG): the same source bakes byte-identically on every machine, every load.

Semantics (Stage 1)#

  • Length is grid-exact: exactly Nc of transport (integrating the tempo map from beat 0), no release tail — so chop N divides the buffer evenly. A hit ringing past the end is truncated (tail policy is spec §10).
  • Stereo where the render is (Stereo Samples §2). The offline render is a stereo bus, and both channels are kept: a bounce of a panned, reverbed or widened track keeps its image. A render whose channels come out identical — anything centred — collapses back to a mono bank entry, so mono songs are unchanged in memory, CPU, and audio. (Before stereo shipped every bounce folded to (L+R)/2.) The on-disk bounce cache stores 2-channel WAVs for stereo bakes; its version salt was bumped so mono-fold entries miss once and re-render.
  • Prints are captured at authored level (the level contract, 2026-08-17): a print replayed through a unity-gain sampler patch nulls against the track played live. The bake bypasses the device stage (master gain, safety limiter, saturator) and — for track prints — the declared master section, and divides out the per-voice headroom, because playback runs all of that again. (Before this, every generation quietly lost ×VOICE_GAIN×MASTER_GAIN ≈ −9 dB — "patches after a bounceOf are very quiet".) A master print (bounceOf Nc master) keeps the declared master section: it is part of the mix being printed; only the device stage and voice headroom are compensated. The cache stores 32-bit float WAVs since the version-4 salt — an authored-level print can peak past ±1, which the old 16-bit entries would clip.
  • Macros at initials during the bake; the live knobs keep moving generation 0 but never re-print a bounce. (Bake-time macro overrides are spec §10.)
  • A tap inside a bounced track reads its real source. The bake starts the bounced track plus every track its taps and controls depend on, as source context whose own contribution is left out of the print (the stem export's selection, engine/src/routing.rs::select_export_tracks), and a bounce is ordered after any bounce those routed sources play (ir/src/routing.rs::order_bounces). A bounce whose routes reach live input (tap "input") is a compile error — record the input first and bounce the take. See Route audio between tracks § Export and bounce context.
  • Hidden track. The track expression lowers as an ordinary Module::tracks entry named bounce:<binding> that no scene references; the bank key is the same string.

Where things live#

  • IR: Module.bounceDefs — { name, track, master?, cycles, depends_on }, dependency-ordered (ir/src/ir.rs::BounceDef; lowering + topo sort in compiler/src/lower.rs, extended through audio/control routes by ir/src/routing.rs::order_bounces; handle resolution in compiler/src/lower/handles.rs). depends_on is the def's direct upstream bounce names, sorted — the bounces its track samples plus those its routed sources sample — exposed so a host-side cache (§8 below) never has to re-derive the dependency graph.
  • Engine: engine/src/offline.rs::bake_bounces — one offline sub-render per def, earlier bakes visible to later ones. Called automatically by render_export / render_export_arrangement / render_track_mono.
  • Desktop host (app-native/src/main.rs, volta-studio): boot bakes inline after module + banks load — nothing is painting yet, and the first ▶ must find a real print. Every later load (hot-swap, File→Open, the songs panel) hands EngineHandle::bounce_bake() to a volta-bounce-bake worker thread and installs the result via install_baked from the next poll frame. Both paths go through the recipe cache (§8). This is load-bearing, not an optimization. load_source runs on the paint thread; a 4-cycle pad def renders in ~2 s (release), and it re-bakes on every recompile — baked inline and uncached, that is a 2-second editor freeze per edit, with the audio thread playing on so nothing sounds wrong. The previous bake stays installed until the new one lands, so a bounce-backed voice keeps playing the old print rather than dropping out; a load that supersedes an in-flight bake discards its result by generation.
  • Browser: the same offline::bake_bounces, reached through WasmEngine::bake_bounces (engine-wasm/src/lib.rs). The page host (runtime/public/studio-host.js, loadIr) runs it as a load-bearing step in a fixed order — load_module → sample/wavetable assets → pinned prints (set_print_pins) → bake_bounces — because a bounce whose hidden track plays a sample bakes SILENCE unless the bank already holds it, and a pinned def installs its pin instead of rendering (pinned-prints.md §7: pins live in the prints IndexedDB store, runtime/public/studio-print-store.js; print_report returns each def's key and pin after the bake). The embed/radio player takes the same path via runtime/src/native-runtime.ts. There is no browser bounce cache: every load re-renders. (The TypeScript OfflineAudioContext bake was deleted with the TypeScript workbench on 2026-08-09.)
  • Late-arriving buffers resolve at trigger, never at load. A bounce print, a baked resynthesis/modal/partials analysis and an onset slice table can each arrive after the module that reads them — a bounce bakes after load_module, a sample bank streams in later. So none of them is bound into a node when the module loads. Each is computed control-side, handed to the audio thread as a side table on the sample bank, and looked up when a note triggers; the bake step runs from both load_module and load_samples (engine/src/lib.rs), so whichever of module and buffer arrives second completes it. Until the buffer lands the reader is silent — or, for slice, falls back to an equal-partition grid (engine/src/sample.rs). engine/tests/bounce_live_order.rs pins the stretched-bounce case, which was once bound at load and stayed silent when its print arrived later.

Printing the master (bounceOf Nc master)#

Shipped 2026-08-16, closing production-gaps §27 V1's 🟡 half-step.

mixPrint = bounceOf 8c master                       -- the WHOLE mix, one handle
spool = patch { source = sample mixPrint |> speed (const 0.7) |> chop 16,
                ampEnv = adsr 2ms 400ms 0.7 200ms, gain = vel * 0.5 }

The bare word master in bounceOf's track slot means the master bus: every ordinary track plays, through its channel strip, its sends, the aux buses and the master section, and the summed result lands in the bank under the usual bounce:<binding> key. One print then respools the whole arrangement's material instead of one part at a time.

  • No hidden track. A track bounce lowers a bounce:<name> entry into Module::tracks; a master bounce lowers nothing — the mix that every other track already builds is the source. BounceDef carries master: true and an empty track.
  • Ordered last, always. A master def is given a dependency on every other bounce def, since any of them may be audible in the mix, so it bakes after all of them.
  • A track that plays the print is excluded from it. Playing a print back is the point of printing it, so spool = patch { source = sample mixPrint … } is not an error — that track is simply left out of the render (a mix cannot contain a print of itself). The compiler emits a MasterBounceExcludesTrack warning so the omission is visible in the source rather than only in the audio, and the bake drops the same tracks.
  • Live input is refused here too. A master print whose tracks or return inserts tap "input" is a compile error, as for a track bounce.
  • No shadowing rule to remember. The word normally resolves through a builtin, which name resolution tries last, so a module's own top-level master shadows it — but in bounceOf's track slot both readings mean the mix, because a bare binding reference was never a legal bounce target (bounceOf takes play …/sound … and pipe chains, not names). bounceOf Nc master against a declared master track was a compile error before this shipped, so reading it as the mix cannot change an existing song. Everywhere else master is an ordinary name, or — undeclared — a diagnostic pointing here.
  • Varispeed-aware length. A mix with a declared varispeed k takes 1/k as long in real time, and the print's frame count follows it — measured, not assumed: the bake records where the ring's read head is per inner block (varispeed::Retime) and ends on the device frame it reaches the last cycle on, so a knob- or scene-driven ratio prints the right length too. bounceOf 8c master still captures eight cycles of music, so chop 8 divides it evenly.
  • Caching. The recipe hash for a master def (offline.rs::master_recipe_hash) covers every ordinary track and its synth, the strips, the buses, the master section (varispeed included) and the tempo map, plus the upstream bounces' own keys — which for a master def is all of them.
  • Deferred: the arrangement walk. The print starts every track at bar 0 and runs Nc; it does not follow an arrange schedule (the compiled module the bake sees carries no arrangement — that lives in the IR JSON the export path resolves). For a song with sections, print the passage you want by writing the cycle count, or use volta export for the arranged render.

Engine regression: engine/tests/varispeed.rs::a_master_bounce_captures_the_whole_mix; compiler: lower::tests::bounce_of_master_prints_the_mix and …::a_track_that_plays_the_master_print_is_excluded_with_a_warning.

Generations as a playable axis (Stage 4)#

chopPat = !"c3 e3 g3 c4"

dry  = bounceOf 1c (play tone !"c3 e3 g3 c4")
gens = bouncesOf 6 1c (\t -> play (patch {
  source = sample t |> chop 4, ampEnv = adsr 2ms 200ms 0.6 150ms, gain = vel * 0.5
}) chopPat |> reverb 0.4) dry     -- a chain of 6 real renders, gen k samples gen k-1

gen    = macro "gen" 0
melody = patch { source = sampleAt gens gen,       -- the "passes" knob, made real
                 ampEnv = adsr 2ms 200ms 0.6 150ms, gain = vel * 0.5 }
  • bouncesOf n dur f seed — binds name to a compile-time list of n handles (bounce:name#0 … bounce:name#(n-1)); each renders f applied to the previous generation's handle (seed for gen 0). Expands entirely at lower time into ordinary bounceOf decls — no new IR node. f's body may build its own inline voice (play (patch {…}) pattern), hoisted to a synthetic top-level synth automatically. Max 64 generations.
  • sampleAt names idx — plays a Sample node whose bank key is chosen from names by idx (any control signal — a macro, typically), read once per note trigger (same contract as chopAt's slice index): hard switch, no crossfade. Every other sample verb composes normally since it resolves to an ordinary named Sample node from that point on. NodeKind::Sample carries names: Option<Vec<String>> + name_index: Option<NodeId> for this; name still holds names[0] as the static fallback.

The live workflow (Stage 5)#

  • Recipe-hash cache — skips a re-render when a def's recipe (its hidden track + synth + cycles + upstream bounces' own keys) is unchanged from a previous bake.
    • Native: engine/src/offline.rs::bake_bounces_cached (bake_bounces itself is cache_dir: None, so every one-shot caller — tests, render_export, the CLI, volta-shot — is byte-for-byte unaffected and hermetic). The live-editing host opts in: volta-studio, via EngineHandle::bake_bounces_cached / BounceBake::run, writing WAVs under bounce_cache_dir() (~/.volta/bounces/ — a hand-rolled HOME, else USERPROFILE, lookup; no home directory means no cache). A cached bounce retains stereo in a 32-bit float WAV, preserving the rendered samples exactly. BOUNCE_CACHE_VERSION salts every key — bump it whenever an engine DSP change could alter a bake given byte-identical IR, or the cache serves renders from an older engine. (It went to 3 when volta-studio picked the cache back up: nothing had written it between app-tauri's deletion on 2026-07-20 and then, so every entry on disk predated the 2026-08 DSP audits.) Version 9 includes output sample rate, seed, tempo/meter maps, key/tuning, varispeed and the selected instruments' voice-allocation policy. Prepared plans use typed serialization, preserving float bits and canonical map order independently of diagnostic formatting. Ordinary and alternative events and track, bus and master inserts retain their shared typed identity projections. Moving highlights, playback anchors, nested graph editor metadata or EQ argument spans keeps the key; musical values and user-map fields remain significant. Cached WAVs at a different sample rate are rejected. Old version-8 entries miss once. Referenced samples include their exact mono and stereo PCM, source rate, source BPM (including bank fallback), variants and sorted multisample zones. Wavetables include every frame and mip; generated sample/comp/bounce buffers and defined tables use their prepared contents. Each buffer enters the key as a content digest computed once per buffer and memoized (SampleData::content_digest), so a key costs the same on a 40-zone instrument as on a click; the resource encoding is version 2 for that, and entries written before it miss once. Zone tables are installed before a bounce renders. Content-equal resource reloads and unrelated bank additions retain the key. Shared controls, audio adapters and return/master resource reads are conservatively included. Embedded songs and external model/plugin state bypass this cache. Prepared routing dependencies also key upstream voices, consumed macros (defaults, modulation and contracts), shared controls, strips, sends and insert stages. Unused controls, unrelated tracks and inserts downstream of a pre-fader tap preserve hits. Master dependencies remain conservative for track prints. The digest is noncryptographic; complete score/timeline, generated-program and implicit global dependency identity remains open.
    • Browser: no cache — the wasm engine re-renders every load. (The IndexedDB store went with the TypeScript workbench on 2026-08-09.)
    • The cache is never load-bearing — every read/write failure (missing home dir, a corrupt entry) is a silent miss, never an error.
  • Freeze — ui/src/benchmodel.rs::freeze_block, reached from the bench cell menu (ui/src/bench.rs). It comments the track's current binding in place and writes <track>Bounce = bounceOf <Nc> (…) + a generic <track>Voice playback patch + a new <track> = play <track>Voice !"c4" under the same name, so scenes keep resolving. <Nc> is the track's own cycle length, rounded to the nearest quarter-cycle; the rewrite is scoped to the cell's own block.

Demo#

  • examples/songs/resample.volta — one bass line at three generations at once: the live wobble, its printed-and-re-chopped render, and a render of the render as a plate bed.
  • examples/songs/sitting.volta — Alvin Lucier's I Am Sitting in a Room: a spoken fragment folded through the same reverb eight real renders deep (bouncesOf), sampleAt walking the chain one generation per bar.
  • examples/songs/passreal.volta — fmtest's fake "passes" knob rebuilt on five real, distinct bounces (a hand-written list, not bouncesOf — see bounce.md §6 for why that's the right call here).

Engine regressions: offline::tests::bounce_bake_audible_deterministic_and_chains, offline::tests::bounces_of_and_sample_at_switch_generations.