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
tempolike everywait. - 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. Unknownfromtracks are a compile error. - Replace, not layer — a completed take replaces any prior take under the same name; a re-
recordmid-capture restarts the window. Layer by recording to a second name. - Non-blocking —
recorddoesn't advance shred time;waitover the take to block. - Before the first take —
sample "name"for a record target plays silence (no error, no missing-sample warning —collect_record_namesexempts looper targets incompiler/src/doctor.rsandui/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
recordmarks the module non-deterministic (status line, inexact replay/export) exactly likeinput, and lazily opens the desktop mic (uses_live_input).from trackrecords 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, …: stampedkept-loopwith 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-positionrecordkeyword, matched as an ident followed by a string —check.rs,resolve.rs,lower/score.rs+validate_score_records, nondeterminism inlower.rs, helpersScoreOp::records_mic/collect_record_namesinir.rs). - Engine (one implementation everywhere):
engine/src/shreds.rs(ShredAction::Record),engine/src/lib.rs(ActiveRecorder,step_recordersafter the bus mix, cleanup on stop/swap),engine/src/graph.rs(uses_live_inputincludes 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-wasmin an AudioWorklet, sorecord— includingfrom 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 withnumberOfInputs: 0, so a micrecordin 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 trackworks. - 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.