Chop listens to a piece of audio and builds a TrackMap: tempo, beat grid, bars, sections, drops, kicks, snares, hats, energy and key. It can then play the track and expose its playhead as a clock that patterns and games follow.
How Chop differs from Strudel's sample tools
Strudel can already cut up a sample. Its tools follow TidalCycles: chop, striate, slice and splice divide a sample into a number of equal parts, and loopAt stretches it to a number of cycles you supply. These tools are powerful, but they work by count and by time. None of them knows where the beats are, so they work best on loops that are already trimmed to the bar.
Chop starts from the other end. It listens first, then cuts on musical boundaries:
| Strudel / TidalCycles | Chop | |
|---|---|---|
| How it cuts | by count: slice(8, ...) gives eight equal parts |
by music: beats, bars, phrases and named sections |
| What it needs to know | you trim the loop and supply its length in cycles | nothing: it finds the tempo and the first downbeat |
| Where the grid comes from | the sample's length divided by n | the beat grid found in the audio, from bar 0's downbeat |
| What you can ask for | slice number 3 | "bars 17–24", "the drop", "every kick", "the backbeat", "the key" |
| Whole tracks | awkward: a three-minute track divided into 8 parts doesn't land on anything | the point: intros, builds, drops and breakdowns are found and named |
| As a clock | no: samples play inside Strudel's tempo clock | yes: the track's playhead is the clock, so other things follow the record |
| Events | none: a sample is audio | kicks, snares, hats and drops are lists of times, ready to drive a game or visuals |
In short, Strudel cuts a sample up and plays it in time with your code. Chop works out where the music is in a whole track, then lets code follow the record.
Use
import { analyse, decodeTrack, TrackPlayer, describe } from "@chop-jump/chop";
const ctx = new AudioContext();
const { buffer, mono } = await decodeTrack(ctx, file);
const map = await analyse(mono, buffer.sampleRate, {
onProgress: (p, stage) => console.log(Math.round(p * 100), stage),
});
describe(map); // "126 BPM · A minor · 72 bars · 2 drops"
const player = new TrackPlayer(ctx, buffer, map);
player.start();
player.clock.cycle(); // bars since bar 0's downbeat: the timeline games follow
player.bar; // the bar of the track that is sounding, following any loop
player.loop(16, 24); // loop bars 17–24 (bars count from 0)
player.loop(); // stop looping and play on from where the loop has got to
player.slice(t, 64, 1); // play beat 64 (bar 16's downbeat) at time t
player.stutter(t, 64, 6); // beat-repeat that beat's first sixteenth six times
player.muffle(0.7); // sweep the filter down; muffle(0) opens it again
In Node or a test, analyseSync(samples, sampleRate) does the same synchronously. readWav and writeWav handle WAV files.
Loops are cut on the grid. If the playhead is already past the loop, playback jumps to the loop's start on the next bar line rather than mid-beat. While a loop plays, clock keeps counting bars forward, because it is the timeline that games and patterns follow; bar and playhead tell you which part of the track is sounding.
More DJ moves, all timed to the clock:
player.jump(timeOfBar(map, 16), player.clock.next("1m")); // cut to bar 16 on the next bar line
player.jump(start, at, { start, end }); // ...and loop from there
player.duck(0.2, at); // duck the track (dry slices stay up)
player.backspin(at, player.playheadAt(at), 1, 0.5); // spin the last second back over half a second
player.tapeStop(at, player.playheadAt(at), 0.9); // slow to a halt
player.playheadAt(t); // what's sounding at any clock time, past or future
Underneath is a Transport: a pure model of where the playhead is through every loop and jump, with no audio. Games ask it what part of the track will be heard at a given moment, and headless tests use it without a browser.
The TrackMap
| Field | What it is |
|---|---|
bpm, beatsPerBar |
tempo to a hundredth of a BPM, and four beats to a bar |
firstDownbeat |
seconds to bar 0's downbeat; the clock's origin |
beats, bars |
every beat time, and every bar's downbeat from bar 0 |
sections |
{ kind, startBar, endBar, start, end, intensity }, with kind one of intro, build, drop, breakdown, groove or outro |
drops |
start time of every drop |
kicks, snares, hats |
hits snapped to the grid: kicks on beats, snares on the backbeat, hats on eighths and sixteenths |
onsets |
raw transients, not snapped |
energy, intensity |
per bar, 0 to 1; intensity mixes loudness, low end, bassline, brightness and busyness |
bands |
per bar levels for low, bass, mid and high |
key |
{ tonic, mode, name, confidence } |
envelope |
loudness at ten points a second, for drawing |
confidence |
{ tempo, downbeat }, 0 to 1 |
Questions you can ask a map, with no audio needed: barAt, timeOfBar, timeOfBeat, sectionAt, energyAt, intensityAt, between(times, from, to), nextDrop, barRange, beatRange, fingerprint and trackClock.
fingerprint(map) is built from coarse musical facts (tempo to half a BPM, length to the second, bar count, key, sections and drops), so the same file gives the same fingerprint at 44.1 and 48 kHz.
Chopping with a pattern
player.voice() returns a sound voice for Jump. Events carrying { beat, length } play that slice of the track, so a Jump music layer can chop the record with mini-notation on the record's own grid:
sink.define("chop", player.voice());
g.music({
chop: {
pattern: "0 0 [2 3] 4 ~ 6 [6 6] 7",
voice: "chop",
// Step values are eighth notes into the current bar.
map: (v, hap) => ({ beat: Math.floor(hap.whole.begin) * 4 + Number(v) / 2, length: 0.5 }),
},
});
The Asteroids demo uses stutter when your ship is destroyed: the track muffles, and the sixteenth you died on repeats six times on the grid.
How it works
- Spectrum. A short-time FFT at about 22 kHz measures energy and spectral flux (how much each band jumped since the last frame) in four bands.
- Tempo. Autocorrelation of the onset curve picks a tempo, weighted towards dance tempos. A comb filter across the whole track then refines it to a hundredth of a BPM and finds the beat phase.
- Downbeats. Of the four ways to group beats into bars, Chop picks the one where harmony and texture change on the one and claps land on two and four.
- Bars. For each bar it measures loudness, low end, brightness and busyness. It also measures bassline: low end between the beats compared with low end on them, which separates a drop's rolling bass from a build's bare kick.
- Sections. Phrase boundaries are found where the texture changes, preferring four-bar lines. Each section is named from its intensity, its bassline and its neighbours: the loudest sections with a full low end are drops, the ones right before a drop are builds, and the quiet ones after a drop are breakdowns.
- Hits. Kicks are jumps in low end on the beat. Snares are mid-range hits on the backbeat that stand out from the off-beats. Hats are high-band hits on the sixteenth grid.
- Key. Chroma from spectral peaks (with parabolic interpolation, for accurate bass notes) is matched against key profiles. Major and its relative minor share every note, so Chop also checks which tonic chord the music sits on at the top of each phrase.
Accuracy
Tested against procedurally rendered tracks where every answer is known: 118 to 140 BPM, 44.1 and 48 kHz, with and without silence before the first beat, one starting straight on a drop, and a quiet copy with added noise.
| Measure | Result |
|---|---|
| Tempo | within 0.05 BPM on every track |
| Beat grid | within 15 ms of the true beats (typically 1–3 ms) |
| First downbeat | within 15 ms |
| Sections | every boundary found, every section named correctly |
| Drops | every drop found, within 15 ms |
| Key | correct on all four, including major and minor |
| Speed | about 1.5 seconds for a two-minute track in Node |
On a real recording (the drum and bass excerpt from librosa's example set), Chop reads 136 BPM, the same tempo librosa's own beat tracker reports for that clip.
Limits
- Steady tempo only. Tempo drift gives an average tempo and a grid that wanders.
- Four beats to a bar is the default.
beatsPerBarcan be changed, but section and snare detection are tuned for four. - Section names assume a dance-music arrangement.
- Analysis runs on the calling thread. In a page,
analyseyields every 24 ms to stay responsive.