VOLTAreference

Reference / Samples and recording

Slice a recording at its attacks

Samples and recording

Open in the workbench →
On this page · 7 sections

Detect attack points in a recording and rearrange the resulting slices into new rhythms.

What it is#

slice is chop's rhythm-aware sibling. Where chop n cuts a sample buffer into n equal time slots — so a slice can begin halfway through a snare — slice n finds the buffer's transients (onset detection) and puts each slice boundary on a hit. The result: every slice starts clean on a drum, the standard "slice-to-pads" interaction of Ableton Simpler, the Akai MPC, and Bitwig. It shares all of chop's machinery; only the origin of the [start, end] boundary differs.

Like chop, it comes in two bindings, disambiguated by argument shape:

  • a pattern combinator — sound (slice n "name" idxPattern) — the headline;
  • a sample modifier — sample "name" |> slice n / |> sliceAt n ix — the synth path (granular repitch/stretch through the engine's chop engine).

Surface#

-- A. Pattern combinator: count, sample name, then a numeric index pattern.
sound (slice 8 "breaks152" !"0 1 2 3 4 5 6 7")   -- eight onsets, in order
sound (slice 16 "breaks152" !"<0 4 8 12> 2 6")   -- pattern-driven onset index

-- B. Sample modifier (synth path): repitch/stretch each onset slice.
chopped = sample "breaks152" |> slice 8                      -- index from the track pattern
glitch  = sample "breaks152" |> sliceAt 16 (randomPerNote (uniform [0, 16]))

The combinator's index pattern is numeric: each token i plays onset-slice i of the named sample; a rest stays a rest. slice and sliceAt parallel chop and chopAt exactly — sliceAt reads the slice index from a control signal instead of the pattern.

Builtin signatures (all Hole-typed like chop, disambiguated at lowering):

slice    : Count -> Str -> Pattern -> Pattern        -- combinator (3 args)
slice    : Sig Audio Bipolar -> Count -> Sig Audio Bipolar   -- modifier (2 args)
sliceAt  : Sig Audio Bipolar -> Count -> Sig Control _ -> Sig Audio Bipolar

How it's implemented#

Detection (engine/src/onset.rs). detect_onsets(mono) returns onset positions in [0, 1) with their strengths, sorted strongest-first and capped at 64: per 256-sample hop a 1024-point Hann STFT frame, per-bin log-compressed magnitude, onset strength = the half-wave-rectified per-bin first difference summed, then local-maxima peak picking above 10 % of the strongest peak with a ~10-frame minimum spacing. It runs control-side only — a plain pass over the decoded PCM, never on the audio thread.

slice_boundaries(onsets, n) returns n + 1 ascending boundaries in [0, 1]: the n strongest onsets become slice starts (sorted by time, padded with an equal grid if detection was sparse, with a minimum slice width enforced), and 1 is the final end. ui/src/benchmodel.rs::slice_boundaries mirrors it so the bench's markers match what plays.

IR (Stage 2, ir/src/ir.rs). TrackEvent.sample_region generalised from Option<(f64,f64)> to Option<RegionSpec>, an untagged enum:

  • Fixed(f64, f64) — a chop/trim [start, end]; serialises as the same [start, end] array as before, so every existing golden snapshot is byte-identical;
  • Onset { count, index } — a slice combinator slice; serialises as a {count, index} object the engine resolves at trigger time. The lowerer can't compute the boundary (it has no buffer), so it emits this sentinel.

Onset-table discovery visits voice and sampler graphs, macro/shared/parameter controls, plugin signal lanes and audio inserts, including all sampleAt candidates. Explicitly authored boundary tables need no onset analysis.

The synth-path modifier adds a slice_onset: bool on the Sample node (skip-serialised when false, so chop goldens are untouched).

Lowering (compiler/src/lower/combinator.rs). pat_slice mirrors pat_chop: it maps each numeric event of the index pattern to a sound event playing the named sample with an Onset { count, index } region (rests pass through; output payload becomes Sample). It dispatches from the same combinator arm as chop in resolve_pattern. The modifier arms (slice 2-arg, sliceAt 3-arg) set the Sample node's slices / slice_index and flip slice_onset = true.

Engine resolution. One engine plays both readings, desktop and browser alike. The bank carries an onset slice-bounds side table (SampleBank::slice_bounds, name → count → the n + 1 boundaries), computed control-side from the shared detector for exactly the (name, count) pairs the module references (graph::wanted_slice_bounds) and re-tried as late samples land. The scheduler resolves each sound event's region at trigger — Fixed directly, Onset {count, index} through the table — into the voice's playback window (EventCtl::region); the |> slice n modifier reads the same table in the chop engine, intersected with the trim window. Until a buffer (and so its table) lands, the equal grid plays — degraded, never silent.

Precedence & composition#

Modifiers fold the last one applied (the Sample rule for trim / reverse): sample "x" |> chop 8 |> slice 4 ends up onset-sliced into 4 (the slice win sets slices = 4, slice_onset = true). For the combinator, chop over an already-Onset-regioned event chops the whole buffer (an onset region can't be subdivided statically — no buffer at lowering).

Snap and marks (2026-08-21 — the Recycle batch)#

  • |> slice n |> snap — quantize the detected boundaries to the sample's own 1/16 grid (manifest bpm, else the detected estimate): Recycle's marker quantize. Each marker moves to its NEAREST gridline; 0/1 stay pinned; the ascending/min-width discipline re-enforces afterwards. Without any tempo there is no grid and the unquantized boundaries play (the missing-BPM warp rule). Snapped and unsnapped tables live under separate bank keys (SampleBank::slice_bounds's (count, snap)), so both can play at once. snap without slice/sliceAt is a compile error.
  • |> marks [0, 0.23, 0.61] — EXPLICIT slice boundaries as buffer fractions: each value starts a slice, the last runs to the buffer end. Sorted + deduplicated at compile, slices mirrors the count, and the engine reads them straight from the node — no detection, no bank table. This is the hand-editable escape hatch (and the write-back syntax for a dragged marker); index selection (numeric pattern / chopAt / trigger order) works exactly as with chop.
  • The detector itself is spectral flux now (see Detection above), replacing the RMS-envelope diff: it hears a changed spectrum (a hat over a sustained bass) where the old detector only heard louder. Positions, the noise floor, minimum spacing, strongest-first order, and the 64 cap are unchanged, so slice boundaries, grooveOf, the bench markers, and BPM estimation keep their contracts.

What it still isn't (see Slice a recording at its attacks §7)#

  • Per-file thresholding. No sensitivity knob in v0 — the count: N cap masks most spurious onsets on noise-floor-dominant material.
  • Multi-variant onsets. A bank:N slice detects against variant 0's buffer.
  • Bench marker drag. The slicer dock still edits counts/order/trim only; writing a dragged marker back as |> marks [..] is the natural follow-up now the syntax exists.

Example#

examples/songs/slicevox.volta — the Amen break (breaks152) sliced on its transients with slice, re-sequenced, then sliceAt-stuttered. A/B against chopvox.volta (the same idea with equal chop slices) to hear onset slicing land each hit clean.