01 · The integral
The whole repo lives under one equation. Tempo is the rate of beats with respect to audio time:
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:
The four regimes below are the same integral evaluated for four different shapes of
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
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:
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:
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
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
A line through the origin with slope
REGIME 1: CONSTANT TEMPO
constant rule learn more ↗
So:
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
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
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:
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
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
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:
People expect that a straight-line tempo gives a straight-line time map. It does NOT, and the integral shows exactly why. We need:
where
integral of 1/(linear) learn more ↗
(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=
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
Regime 4 — Curved tempo (the one without a formula)
Let tempo follow a power curve with an easing exponent
For general
- Trapezoidal rule for the forward direction
beatsToSeconds. Sliceinto pieces of width , approximate the area on each slice with a trapezoid, sum. - Bisection for the inverse
secondsToBeats. Binary-search for thethat 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:
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:
For k = 1 we just solved this with the "integral of 1/(linear)" rule. For k = 2 the integrand is 60 / (
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/
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
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.
Each map's bpmAt(β) IS that derivative, evaluated analytically where a formula exists. The test suite estimates beatsToSeconds at small bpmAt(β) across all four regimes.
THE ROUND TRIP: BPM IS THE DERIVATIVE OF
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
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 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.