Skip to content

08 · The grid follows the file

Chapter 07 warped the file onto a rigid grid, and that direction has a price: varispeed shifts the pitch, and avoiding the shift means time-stretch DSP. This chapter pays nothing. Run the conform the other way — make the project's tempo map be the file's beat map — and the audio plays raw at rate 1.0, bit-identical, while the metronome, the bar ruler, and every MIDI clip follow the file's wobble instead. This is the other half of every DAW's warp workflow: Ableton's Set tempo from clip, Logic's adapt project tempo to recording.

It is also this tutorial's first advanced chapter: playback runs through @dawcore/transport, a production Web Audio transport whose TempoMap implements precisely the regimes chapter 01 derived — 'step' is the piecewise-constant rectangle sum, 'linear' is the (60/s)ln() closed form, 'curve' integrates with the trapezoidal rule. The chapters stop deriving and start proving: the tests check that chapter-01 math and the production TempoMap agree at every beat, to 109.

A transport schedules in ticks (chapter 07) against a tempo map (chapter 01). To make the grid follow the file, feed it one tempo event per file beat, holding each segment's BPM as a step:

event n:  tick = (n - 1) * PPQN,   bpm = segmentBpm(m[n], m[n+1])

The tick is chapter 07's anchor verbatim -- tick 0 is beat 1 -- and the bpm is chapter 05's instantaneous BPM, the per-gap rate the detectors already compute. Nothing new is derived; two existing layers are plugged into each other.

One number-system wrinkle remains, and it is the lead-in again. The transport anchors tick 0 at transport second 0, but the file's beat 1 sits at t_first > 0 in the audio -- and the file may open with PICKUP beats, the tail of a bar that began before the recording. The grid itself is non-negotiable: a DAW's grid is always FULL BARS from tick 0. So the placement must satisfy two constraints at once:

  1. the first DOWNBEAT lands exactly on a bar boundary (tick = k * beatsPerBar * PPQN), and
  2. the pickup beats fill the END of the bar before it.

With p pickup beats, the first bar boundary with room for them is

firstDownbeatTick = ceil(p * PPQN / ticksPerBar) * ticksPerBar
firstBeatTick     = firstDownbeatTick - p * PPQN

and the file's beat n then lives at firstBeatTick + (n-1) * PPQN -- chapter 07's anchor, shifted whole. The grid before the first beat (the empty lead-in bars the metronome counts through) runs at the first segment's tempo, and the CLIP is placed so the audio's beat-1 sample sounds exactly at firstBeatTick's second: any audio before t_first now PLAYS during the lead-in instead of being trimmed. With no pickup, firstBeatTick = 0 and this degenerates to the plain clip offset: trim t_first, beat 1 at tick 0.

And the bars themselves come from the FILE, not from an assumption. Real tracker output contains irregular bars -- a lone 5-beat bar in the middle of a 4/4 song is common -- so counting "every 4th beat is a downbeat" drifts one beat off at the first irregularity and stays wrong forever after. The declared beatInBar column is authoritative: each run of equal bar lengths becomes one meter entry (5 beats = one bar of 5/4, then back), each entry placed at its downbeat's tick. That is the meter layer of chapter 04, feeding the transport's MeterMap instead of barPositionOf.

grid conformity  learn more ↗

tempoMap.ticksToSeconds(firstBeatTick+(n1)PPQN)=clipStartSec+m[n].second

That rule is the whole chapter: the production transport's clock, evaluated at beat n's tick, must reproduce the beat tracker's timestamp shifted by where the clip sits -- through chapter-01 math on one side and @dawcore/transport's TempoMap on the other. The tests assert both, with and without pickups -- and assert the meter side too: every declared downbeat tick must satisfy MeterMap.isBarBoundary.

What the conversion actually is

Nothing new. One tempo event per file segment:

layersuppliesfrom chapter
event tick(beat1)PPQN — tick 0 anchors at beat 107
event bpmsegmentBpm(m[n], m[n+1]) — the instantaneous per-gap BPM01 / 05
event semantics'step' interpolation — hold until the next event01 (regime 2)

tempoEventsFromMarkers(markers, ppqn) is that conversion in its purest form — events anchored at tick 0 = beat 1, plus clipOffsetSec = markers[0].second to trim so the audio's beat-1 sample plays at transport second 0. gridPlanFromBeats(parsedBeats, ppqn) is the production-shaped version on top of it: it reads the .beats file's beatInBar column, detects pickups, and bar-aligns everything (next section).

The equivalence, proven three ways

The test suite sweeps every quarter-beat of a wobbly fixture through three independent clocks and demands agreement to 109:

  1. Chapter 01piecewiseConstantMap(markers).beatsToSeconds(β)
  2. Productionnew TempoMap(ppqn) fed our events, ticksToSeconds((β−1)·ppqn) + clipOffset
  3. Reference — this chapter's gridSecondForBeat, ten lines of pure arithmetic

The same residual check runs live in the demo's table: transport.tickToTime(tick) + clipOffset − beatTime, evaluated against the actual transport instance that is playing, shows 0.00e+0 down the column while conformed.

Full bars, pickups, and the lead-in

The recurring number-system lesson closes the loop here, under one non-negotiable constraint: a DAW's grid is always full bars — the first downbeat must land on a bar boundary. With p pickup beats:

firstDownbeatTick=pPPQNticksPerBarticksPerBar,firstBeatTick=firstDownbeatTickpPPQN

so the pickup fills the end of the bar before the first downbeat, the empty lead-in bars tick at the first segment's tempo, and the clip is placed so the audio's beat-1 sample sounds exactly at firstBeatTick's second — meaning the file's own lead-in audio plays during the count-in bars instead of being trimmed. With no pickup this degenerates to the plain clip offset (trim tfirst, beat 1 at tick 0). gridPlanFromBeats implements all of it, and the test suite checks the conformity rule in both shapes through the production TempoMap.

See and hear it

Open the standalone demo ↗ — the UI is a full editor — a bar ruler, grid, waveform, and playhead drawn over a production Web Audio transport. Load a .beats file (six bundled samples: synthetic wobbly and pickup fixtures, real tracker output for otherside — as tracked and repaired per the chapter 06 pipeline — scar tissue, and the genuinely mixed-meter bastard, 73 meter regions) and optionally its audio. The file is scheduled once and never touched; the conform toggle only swaps the tempo map. Rigid: the bar lines march evenly and the waveform's beats drift across them while the metronome fights the music. Conformed: the grid bends to the waveform and every beat sits on a bar line. The signal is identical both ways — compare with chapter 07's demo, where locking the beats changed the sound.

What this chapter does not cover

Meter changes. The grid here is fixed 4/4; mapping a file whose bar length changes mid-way onto the transport's MeterMap (via meterMapFromBeats from the shared parser) is the same plug-two-layers-together move and makes a good exercise. And as ever: no DSP — this chapter's entire point is that none was needed.