VOLTAreference

Reference / Samples and recording

Record a loop

Samples and recording

Open in the workbench →
On this page · 4 sections

Capture audio into a reusable loop and bring that recording back into the musical flow.

What it is#

A loop pedal as a shred op: record "name" <dur> [from track] captures live audio — the microphone/line-in, or another track's bus — into the sample bank during the performance, bar-placed and tempo-elastic. The take is then just a bank sample: every sample verb (chop, slice, granular, reverse, stretch, pitched, …) applies to it immediately.

voiceBack = patch { source = sample "voice" |> chop 8, gain = vel }
stut      = play voiceBack !"0 2 4 1 6 3 7 5"
arpTrack  = play keys arpPat

looper = do {
  wait 2c;
  record "keys" 2c from arpTrack;  -- the arp track's own bus — deterministic
  wait 2c;
  record "voice" 2c;              -- the MIC — flags the module non-deterministic
  wait 2c
}

Semantics#

  • Timing — capture starts at the op's shred beat (sample-accurate on the native engine) and runs the folded-to-beats duration, so it retunes with tempo like every wait.
  • Sources — no from: the live input hub (input / follower "input"'s hub; stereo, mono-downmixed into the take). from track: that track's post-fader bus, zero-latency (same-block), mono-downmixed. The track is its binding (from arpTrack, or qualified through an import, from kit.arpTrack) — renamed and found by the editor like any reference, and a misspelling is refused at the argument; its name as a string (from "arpTrack") is accepted legacy, qualified with its binding inside an imported library. Unknown from tracks are a compile error.
  • Replace, not layer — a completed take replaces any prior take under the same name; a re-record mid-capture restarts the window. Layer by recording to a second name.
  • Non-blocking — record doesn't advance shred time; wait over the take to block.
  • Before the first take — sample "name" for a record target plays silence (no error, no missing-sample warning — collect_record_names exempts looper targets in compiler/src/doctor.rs and ui/src/intel.rs).
  • Punch-in audibility — native voices resolve bank names per render block, so a replacing take becomes audible at the swap block even in already-sounding voices.
  • Determinism — a mic record marks the module non-deterministic (status line, inexact replay/export) exactly like input, and lazily opens the desktop mic (uses_live_input). from track records an internal render and stays deterministic.
  • Lifecycle — stop-all / module swap drop in-flight captures. Completed loops reach both the audio and control-side banks. Desktop saves each completed pass as a Float32 WAV alias named <loop>.l<n> (keys.l1, keys.l2, …, never reused) with its originating source stamp; the loop-pedal name remains the replaceable live name. Aliases saved before this naming (keys-loop-<hex>.t1) keep loading.
  • Keep as take (desktop) — Track → Recording lists the loops the song has captured; Keep keys as take writes the loop's CURRENT buffer (an overdubbed loop keeps the sum you heard) as the ordinary take keys.k1, keys.k2, …: stamped kept-loop with the version the loop was captured against, persisted beside the song, comp-able, reloaded on open. There is no score op for it. Browser track-bus loops are captured by Save idea after the loop-captured notice, and can then be recalled from History. Offline export renders mic takes silent (no mic in the offline engine); track-bus takes render for real.

Where it's implemented#

  • Compiler: ScoreStmt::Record / ScoreOp::Record (ast.rs, parse.rs — the statement-position record keyword, matched as an ident followed by a string — check.rs, resolve.rs, lower/score.rs + validate_score_records, nondeterminism in lower.rs, helpers ScoreOp::records_mic / collect_record_names in ir.rs).
  • Engine (one implementation everywhere): engine/src/shreds.rs (ShredAction::Record), engine/src/lib.rs (ActiveRecorder, step_recorders after the bus mix, cleanup on stop/swap), engine/src/graph.rs (uses_live_input includes mic records). Tests: engine/tests/looper.rs (synthetic-input e2e: bar-accurate mic capture + playback frequency check, within-setup determinism, track-bus dub difference test).
  • Browser: the same engine runs as volta-engine-wasm in an AudioWorklet, so record — including from track — behaves identically. Live mic input is the exception: the engine drains it from its worklet node's input (push_input, runtime/public/worklets/native-engine-core.js), and the workbench shell (runtime/public/studio-host.js) builds that node with numberOfInputs: 0, so a mic record in the browser workbench currently captures silence. The embed/radio host wires the hub (runtime/src/native-runtime.ts::ensureAudioInput).

Limits (v0)#

  • Browser workbench: no mic capture yet (above); from track works.
  • No overdub-sum or punch-out score op. Use a second name to layer loops.
  • Browser loops remain session-only until Save idea succeeds. Desktop disk failures are reported and leave PCM in memory; check the recording status.
  • A historical loop snapshot does not supply a live microphone to its score. Print/rewrite live-input parts before using deterministic history previews.