Skip to content

07 · Ticks are fractional beats

Every DAW timeline question is beatsToSeconds in disguise. A sequencer never asks "when does this note play?" in beats or in seconds directly — it asks in ticks, the integer grid (PPQN: pulses per quarter note) on which every MIDI event is scheduled. This chapter shows that the tick grid adds no new math to chapter 01: a tick is just a fractional beat whose denominator was agreed on in advance, so the tempo map you already have answers every grid question — including the big one, how to warp an audio file so its beats land on a project's grid.

A DAW does not schedule MIDI notes in seconds, and it does not schedule them in beats either. It schedules them in ticks. PPQN — pulses per quarter note — fixes an integer resolution (96, 480 and 960 are common), and every event gets an integer tick count. In this repo a beat IS a quarter note, so PPQN ticks divide one beat into PPQN equal slices. A tick is therefore nothing more exotic than a fractional beat with the fraction's denominator agreed on in advance:

tick / PPQN = beats since the grid origin

The only subtlety is WHERE the grid origin sits, and it is a number-system subtlety. Ticks are 0-based: the first schedulable position is tick 0, which a DAW displays as 1.1.0 — bar 1, beat 1, tick 0. But in this repo's beat maps, beat 1 lives at β = 1, not at β = 0. The interval β in [0, 1) is the map's implicit lead-in — the run-up BEFORE the first beat, before the grid begins. Tick 0 must therefore land on β = 1, which forces a "+ 1" into the conversion:

β(tick) = tick / PPQN + 1

Check it at the anchors: tick 0 gives β = 1 (the first beat, DAW position 1.1.0), and tick = PPQN gives β = 2 (the second beat). Half a beat past beat 3 at PPQN 96 is tick 240:

β(240)=240/96+1=2.5+1=3.5

Inverting is one step of algebra — subtract the anchor, scale by the resolution:

tick(β)=(β1)PPQN

Note the asymmetry between the two directions. β is continuous, so β(tick) accepts any integer tick and tick(β) accepts any β — but tick(β) returns a FLOAT. β = 2.25 at PPQN 96 is tick 120 exactly; β = 2.3 is tick 124.8, which is not a tick at all. Turning that float into an integer is a policy decision (round? floor?) that belongs to the caller, not to this conversion — see nearestTick below.

tick anchor  learn more ↗

β(tick)=tick/PPQN+1

Composing with a map

Once a tick names a β, the chapter-01 map does the rest. The two compositions a sequencer runs on every event:

tickToSecond=beatsToSecondsbetaForTicksecondToTick=tickForBetasecondsToBeats

Worked number (it appears again in the tests): PPQN 96, tick 240 gives β=240/96+1=3.5; on constantMap(120) a beat lasts 0.5 s, so the tick sounds at 0.5×3.5=1.75 s.

Quantize: round or floor?

secondToTick returns a float — 1.748 s at the settings above is tick 239.616. Making it an integer is a policy decision, and the two obvious policies answer different questions:

  • Math.round asks which tick is nearest? That is snapping: a note played 4 ms early should land on the gridline it was aimed at, whether it arrived just before or just after it.
  • Math.floor asks which grid cell am I in? That is bucketing: a playhead readout ("bar 3, beat 2") or a per-slice event count belongs to the cell a position is inside, even at 99% of the way to the next tick.

Both are legitimate. The module exports the snapping one, nearestTick, because that is what the warp workflow needs; the bucketing one is a one-character edit.

The warp rate

The payoff. An audio file's beat tracker produced markers (a wobbly piecewiseConstantMap); the project has a rigid grid (constantMap(projectBpm)). Warping means time-stretching the file so file beat n sounds at project beat n. Between adjacent markers the file runs at a constant segmentBpm, and the rate that segment must play at is the ratio of what the file supplies to what the project allots:

rate=source seconds per beatproject seconds per beat=60/segmentBpm60/projectBpm=projectBpmsegmentBpm

A rate above 1 plays the source faster — the file segment was slower than the project and has to hurry to keep up.

Worked example: a file with beats 1, 2, 3 found at 0.5 s, 1.1 s, 1.6 s — spacings of 0.6 s then 0.5 s, i.e. segments at 100 BPM then 120 BPM — warped onto a 120 BPM project:

SegmentFile spacingSegment BPMProject allotsRate
beat 1 → 20.6 s1000.5 s120/100=1.2
beat 2 → 30.5 s1200.5 s120/120=1.0

After warping, each segment occupies its source duration divided by its rate — 0.6/1.2=0.5 s and 0.5/1.0=0.5 s — exactly one project beat each, and beat n lands at n×60/projectBpm on the timeline. The wobble is erased; that is the whole job. alignBeats produces the marker-pairing table a DAW would persist for the warped clip, and segmentRates produces the per-segment rates.

Hear it

Open the standalone demo ↗ — runs in your browser, no checkout needed. Load a .beats file (or the bundled wobbly sample) and optionally the audio it describes; a metronome clicks the rigid project grid while the file plays warped through one playbackRate automation point per beat, generated by segmentRates(). Untick warp to hear the same file raw, drifting off the grid — that A/B is the chapter's payoff made audible.

What this chapter does not cover

The rate is just a number here. Actually consuming source audio at that rate — resampling, time-stretch DSP, preserving pitch — is playback's problem, and playback belongs to chapter 03. No audio is rendered in this chapter, and nothing here knows what a sample is. The point is narrower and, hopefully, sharper: the grid a DAW lives on is the chapter-01 integral wearing an integer costume.