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)— achop/trim[start, end]; serialises as the same[start, end]array as before, so every existing golden snapshot is byte-identical;Onset { count, index }— aslicecombinator 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 (manifestbpm, 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-BPMwarprule). Snapped and unsnapped tables live under separate bank keys (SampleBank::slice_bounds's(count, snap)), so both can play at once.snapwithoutslice/sliceAtis 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,slicesmirrors 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 withchop.- 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
sliceboundaries,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: Ncap masks most spurious onsets on noise-floor-dominant material. - Multi-variant onsets. A
bank:Nslice 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.