Skip to content

01 · The integral

The whole repo lives under one equation. Tempo is the rate of beats with respect to audio time:

dβdt=BPM60

We almost always want the inverse direction — given a beat, what second is it? — so flip the fraction (seconds per beat) and integrate. By the Fundamental Theorem of Calculus, integrating a rate recovers the total:

t(β)=0β60BPM(b)db

The four regimes below are the same integral evaluated for four different shapes of BPM(b).

The whole shape, at a glance

Read the round trip clockwise. Integrate the tempo curve to get the warp map; differentiate the warp map to recover the tempo (a DAW's live BPM readout is this derivative); invert the warp map to go from an audio second back to a beat — closed-form where the regime has one, by bisection where it doesn't. Each chapter formalises one piece of this loop for a different shape of BPM(b).

tempo-map.js

The one idea in this file: a "warp map" is a function that converts MUSICAL time (beats) into AUDIO time (seconds), and back. Everything a beat grid does -- snapping, playback, stretching -- is built on this map.

Why calculus shows up at all Tempo is a RATE. "120 BPM" means beats are arriving at 120 per minute, i.e. 2 per second. If we call musical position β (in beats) and audio position t (in seconds), tempo is literally the derivative of β with respect to t:

d(β)/dt=BPM/60(beatspersecond)

We almost always want the other direction: "given a beat, what second is it?" So we flip the fraction (this is just 1 / rate) and integrate. The Fundamental Theorem of Calculus says integrating a rate recovers the total:

t(β)=0β60/BPM(b)db

In words: "seconds per beat, added up across all the beats so far." That integral is the whole game. The three functions below are just that same integral evaluated for three different shapes of BPM(b).

All functions take a model, an array of warp markers sorted by beat: [{ beat: 1, second: 0.5 }, { beat: 2, second: 1.0 }, ...] A marker PINS a musical beat to an audio second. Tempo is never stored; it is DERIVED from the gap between two markers (see segmentBpm below).

IMPORTANT: the .beat field is the SEQUENTIAL beat number, 1-indexed -- matching beat_this's convention that beat maps never have a "beat 0" (the first row of any .beats file has beatInBar in 1..N). The integration anchor is still β = 0 at second = 0 -- but no marker sits there. The math layer treats the interval from (0, 0) to the first marker as an implicit segment whose slope is consistent with that first marker.

Regime 1 — Constant tempo

BPM(b)=Kt(β)=0β60Kdb=60Kβ

A line through the origin with slope 60/K seconds per beat. The integral of a constant is the easy rule every calculus course starts with.

REGIME 1: CONSTANT TEMPO

BPM(b) = K, a constant. The integral of a constant is the easy one every calculus course starts with:

constant rule  learn more ↗

cdb=cb

So:

t(β)=0β(60/K)db=(60/K)β

The warp map is a straight line through the origin with slope 60/K seconds per beat. Nothing curves. We expose it as a model with a single tempo.

Regime 2 — Piecewise-constant tempo

This is what beat_this gives you: each beat is a marker, so BPM(b) is a step function — flat on each piece, jumping at each marker.

t(β)=completed segments60BPMiΔβi+leftover partial segment

The integral is a running sum of rectangle areas — a Riemann sum that happens to be exact because the integrand is genuinely constant on each piece. The warp map is piecewise-LINEAR, kinking at every marker where the slope changes.

REGIME 2: PIECEWISE-CONSTANT TEMPO (this is what beat_this gives you)

beat_this hands you a timestamp for every beat. Treat each beat as a marker. Between consecutive markers the tempo is held constant, so BPM(b) is a STEP function -- flat on each beat, jumping at each marker.

The integral of a step function is just the running SUM of rectangle areas. Each rectangle is (width in beats) * (height = seconds-per-beat on that step). This is a Riemann sum that happens to be exact, because the function really is constant on each piece:

t(β) = sum over completed segments of (segLenBeats * 60 / segBpm) + leftover partial segment

Geometrically the warp map is piecewise-LINEAR: straight inside each segment, with a kink at every marker where the slope (tempo) changes.

Regime 3 — Linear ramp (the surprising one)

Let tempo vary linearly with beat position between b0 and b1 over L beats, so BPM(b)=b0+sb with s=(b1b0)/L. Then:

t(β)=0β60b0+sbdb=60sln(BPM(β)b0)

A linear change in tempo produces a logarithmic time map. The curve bends — and if you naively interpolate beat positions in a straight line you will drift against the audio. The code derives exactly why, naming the calculus rule 1/(p+qx)dx=(1/q)ln|p+qx| as it is used.

REGIME 3: LINEAR TEMPO RAMP (an accelerando -- the surprising one)

Now let BPM vary LINEARLY with beat position between a start and end tempo over a span of L beats:

BPM(b)=b0+(b1b0)(b/L)

People expect that a straight-line tempo gives a straight-line time map. It does NOT, and the integral shows exactly why. We need:

t(β)=0β60/BPM(b)db=0β60/(b0+sb)db

where s=(b1b0)/L

integral of 1/(linear)  learn more ↗

1/(p+qx)dx=(1/q)ln|p+qx|

(Quick reminder of why: d/dx [ln(p + qx)] = q/(p + qx) by the chain rule, so dividing by q undoes the extra factor.) Applying it with p=b0, q=s:

t(β)=(60/s)[ln(b0+sβ)ln(b0)]=(60/s)ln(BPM(β)/b0)

That ln is the punchline: a LINEAR change in tempo produces a LOGARITHMIC time map. The map bends. If you only ever linearly interpolate beat positions you'll drift against the audio, and this is the formula that says by how much.

Edge case: if b0 == b1 there's no ramp (s = 0), the 1/(p+qx) rule doesn't apply (you'd divide by zero), and you fall back to the constant rule.

Regime 4 — Curved tempo (the one without a formula)

Let tempo follow a power curve with an easing exponent k>0:

BPM(b)=b0+(b1b0)(b/L)k

For general k the integrand 60/(b0+Δ(b/L)k) has no elementary antiderivative. There is simply no combination of polynomials, exponentials, logarithms, and trig that evaluates this integral for arbitrary k. So you stop trying to solve symbolically and evaluate it numerically:

  • Trapezoidal rule for the forward direction beatsToSeconds. Slice [0,β] into n pieces of width h=β/n, approximate the area on each slice with a trapezoid, sum.
  • Bisection for the inverse secondsToBeats. Binary-search for the β that makes beatsToSeconds(β) = t. This is safe because the warp map is strictly monotone, so exactly one root exists.

This regime is the reason every real DAW integrates tempo numerically rather than reaching for a formula: once tempo automation can be an arbitrary curve, a formula usually doesn't exist.

REGIME 4: CURVED TEMPO (ease in / ease out -- the one without a formula)

Now let tempo follow a POWER curve between start and end over L beats, with an "easing exponent" k that bends the shape:

BPM(b)=b0+(b1b0)(b/L)k,k>0

k = 1 is the linear ramp from REGIME 3 -- a straight line in tempo. k > 1 eases IN: tempo changes slowly at first, then rushes (concave-up when accelerating, concave-down when decelerating). 0 < k < 1 eases OUT: tempo changes quickly at first, then settles. This is the shape DAW tempo-automation lanes actually use.

Why we change tactics here The integral we want is still the same one:

t(β)=0β60/BPM(b)db=0β60/(b0+Δ(b/L)k)db

For k = 1 we just solved this with the "integral of 1/(linear)" rule. For k = 2 the integrand is 60 / (b0 + Δ * b^2 / L^2) -- a constant over a quadratic. Its antiderivative is an arctangent (or a log, depending on the sign of Δ), but the formulas get hairier and stop generalising. For general non-integer k -- which is exactly what you want from a smooth easing slider -- the antiderivative has NO ELEMENTARY CLOSED FORM. There is simply no combination of polynomials, exponentials, logarithms, and trig that evaluates this integral for arbitrary k.

So we stop trying to solve the integral symbolically and EVALUATE it numerically. The method:

TRAPEZOIDAL RULE . Slice the interval 0..β into n equal pieces of width h = β/n. On each slice, approximate the area under 60/BPM by a trapezoid with parallel sides 60/BPM(xi) and 60/BPM(xi+1). Summing across slices,

(h/2)(f(x0)+2f(x1)+...+2f(xn1)+f(xn))
The error shrinks like O(h^2). With n = 512 over a few seconds of
audio, the residual is far below anything an ear can hear.

And the inverse:

BISECTION . We need secondsToBeats(t). beatsToSeconds is STRICTLY INCREASING (because BPM(b) > 0 means 60/BPM is positive, so the integral is monotone), so for any target t there is exactly one β with beatsToSeconds(β) = t. Binary-search for it: keep an interval [lo, hi] known to bracket the answer, and halve it until the width is under tolerance. Monotonicity is what makes this safe -- without it, bisection could converge to the wrong root.

The round trip — BPM is the derivative

The piece that closes the calculus loop: a live BPM readout is the derivative of the warp map coming back around.

BPM(β)=60dβdt=60dt/dβ

Each map's bpmAt(β) IS that derivative, evaluated analytically where a formula exists. The test suite estimates dt/dβ numerically from beatsToSeconds at small h and confirms 60h/(t(β+h)t(β)) matches bpmAt(β) across all four regimes.

THE ROUND TRIP: BPM IS THE DERIVATIVE OF t(β)

We opened with dbeta/dt = BPM / 60 -- tempo is the RATE of beats with respect to audio time. Everything below INTEGRATES that rate to recover t(β), three closed-form regimes followed by a numerical one. The piece that closes the loop, and that every DAW relies on without saying so out loud, is the other direction: a live BPM readout is the DERIVATIVE coming back around.

BPM(β)=60d(β)/dt=60/(dt/d(β))(reciprocaloftheslope)

Each map's bpmAt(β) IS that derivative, evaluated analytically where a formula exists for it: a constant (regime 1), a piecewise constant (regime 2), the line b0 + sβ (regime 3), the power curve b0 + Δ(β/L)^k (regime 4). The test suite estimates dt/d(β) numerically from beatsToSeconds at small h and confirms 60/(dt/dbeta) agrees with bpmAt for every regime -- the explicit proof that the integral and the derivative are two views of the same object.