Keep a bounceOf or sampleOf render you like as a file beside the song, so it plays exactly as it is however the source above it changes.
What a pin is#
A bounceOf / bouncesOf / sampleOf binding is a recipe: every load renders it again (Resample your own sounds, Create samples from synthesis). Pinning writes the installed render to the song's prints folder; from then on every host installs that file under the binding's bank key instead of rendering it. The source is never rewritten and never names the pin — the recipe stays authoritative for what the print is, and a pin is an override the bake consults.
- Where.
<song dir>/prints/<song stem>/<binding>.wav(32-bit float, stereo when the print is — bit-exact with the render, and authored-level peaks past ±1 survive) plus<binding>.print.json. The stem level keeps songs that share a folder apart. AbouncesOfgeneration is<binding>#<k>.prints/is not a sample-bank root: a pin never registers as an ordinary!"name"sample. - The sidecar records the binding, kind (
bounce/master/sample), the recipe key it was pinned against, the engine version, rate, frames, channels, the WAV's SHA-256, the binding's source excerpt, macros held (a take), and the time. A WAV whose digest no longer matches its sidecar is refused (the def renders from its recipe) rather than played under a stale identity. - A missing folder is today's behavior. A song shared without
prints/renders every recipe.
Current, stale, unknown#
Each def has a durable print key (offline::print_keys, and every bake_prints report): SHA-256 over the same typed recipe the bounce cache hashes, without the cache's version salt and without the sample rate, with every buffer read entering by name (a pinned upstream by its WAV digest, an unpinned one by its own key). So:
- a pin taken against today's recipe is current; edit the recipe (or an unpinned upstream of it) and it is stale — and it keeps playing: a pin never silently re-renders;
- a pinned upstream shields its descendants: pinning
gen1and editing the synth behind it leaves a pinnedgen2current; - an engine DSP change does not mark pins stale (the point of keeping audio), and the key is the same on every machine and in the browser;
- unknown when a recipe cannot be identified: live input, hosted plugin state, or embedded song sources.
Limits of the key, by construction: a bank FILE replaced under the same name is not detected (buffers enter by name), and a change to the engine's typed plan encoding re-keys bounce defs (sampleOf keys hash only the kernel). Either way the pin keeps playing and Keep re-stamps it. PRINT_KEY_VERSION leads every key; engine/tests/pinned_prints.rs::a_print_key_is_a_fixed_value pins one.
The verbs#
| Verb | What happens |
|---|---|
| Pin | writes the installed print + sidecar (no second render) |
| Unpin | deletes both (and an emptied folder); the recipe renders again |
| Keep | re-stamps a stale pin's key to the current recipe; audio unchanged |
| Re-print | renders the current recipe and replaces the pin (old files stay until the render lands) |
| Pin while playing | renders the binding with every macro held where the knobs are now and pins that — a take; its sidecar lists the macros |
A take is the one print a recipe cannot reproduce (bakes otherwise hold macros at their initials); Re-print would lose it. sampleOf reads no macros, so it has no take.
Where pins apply#
- Studio (desktop).
app-native/src/prints.rs::PrintHostloads the open song's folder on open and re-reads it when the folder changes (onestatper bake — a pin written by MCP or another window is picked up on the next Run). Every bake job takes the pin map (EngineHandle::bounce_bake_pinned), and the handle keeps it too (set_print_pins), because every load re-bakessampleOfdefs and a pinned one must install its pin there as well. The boot bake, device restarts and the Run bake all honor pins. - Export, measure, MCP renders.
app-native/src/export.rstakes the pins (ExportSnapshot::pins,run_measure,render_span— the span cache key folds pin digests in). One-shot renders bake internally from the bank they are handed, so a pin also rides in the bank aspinned-print:<binding>@<sha>(prints::bank_entries); every bake merges those (prints::merge_bank_pins). Never installed in a live bank. - CLI.
volta export stemsandvolta export videouse the song's pins and say so;--no-printsrenders the recipes. - MCP.
list_prints(each def: pinned,current/recipe changed/unknown, take) andpin_print(pin/unpin/keep/reprint/takewith explicitmacros). - Sharing. The community feed and registry store text: the publish dialog and
volta publishsay how many pins stay on this machine; listeners hear the recipes.
In the editor#
Every bounceOf / sampleOf binding gets a chip at the end of its first line (ui/src/app/printchips.rs, painted by ui/src/editor/linechips.rs):
- live — the print's length, ▶ (auditions the installed bank entry), pin, and pin playing (bounces only, and only where the shell can take);
- pinned — ▶, unpin;
- pinned · recipe changed — amber, with a gutter note that needs no hover ("playing pinned print — the recipe changed"), and re-print / keep / unpin; on a take, re-print is greyed and its tooltip says why;
- unchecked (no key), rendering…, pin not loaded (unpin only).
A bouncesOf family shows one summary chip ("3 prints · 1 pinned ▾") whose menu lists each generation with its own verbs.
Finding prints across songs#
Sounds → Samples → Prints lists every pin in the workspace and the open song's folder (app-native/src/print_index.rs: a background walk, cached per folder by mtime). Search matches song, binding and the pinned source excerpt. A print from another song offers Preview, Use in this song (copies the WAV into this song's samples/ as <song>-<binding>.wav, suffixing on a collision, with the sidecar beside it as provenance, and inserts !"<name>") and Open origin (opens that song at the binding). A print from the open song offers Preview and Jump to binding. The bridge seam is separate from the bank listing (EngineBridge::print_index, audition_print, use_print) because a print is not a bank entry.
In the browser#
Pins live in IndexedDB (volta-song-samples v2, store prints, keyed scope\nbinding with the same sidecar JSON and float WAV bytes; runtime/public/studio-print-store.js), scoped like imports (liveDoc ?? songPath). loadIr runs module → assets → pins → wavetables → bake → report; a pin whose WAV digest no longer matches is refused. The four verbs run page-side with the desktop's semantics; an unsaved session cannot pin. Not yet: the Prints section (the store lists across scopes, nothing renders it), takes, and prints for bundled songs.
Where things live#
engine/src/prints.rs— print key hasher, sidecar + folder I/O, pin map, bank-carried pins,PinState.engine/src/offline.rs—bake_prints,bake_prints_with_macros,print_keys,print_key_for;recipe/resources.rs::hash_resource_names.engine/src/lib.rs—BounceBake::{run_report, macros},EngineHandle::{bounce_bake_pinned, bounce_take, set_print_pins, print_keys}.app-native/src/prints.rs(host state and verbs),app-native/src/main.rs(wiring,start_take/take_poll),app-native/src/mcp/prints.rs(tools),app-native/src/print_index.rs(the cross-song walk).- Tests:
engine/tests/pinned_prints.rs,app-native/src/mcp/mod.rs::pin_print_*,cli/src/interchange.rs::export_stems_plays_pinned_prints_unless_told_not_to.