Examples¶
The specification says what is true. This page shows what that looks like, in numbers and in messages.
What on this page is normative
Some of these are quoted directly from the normative text (Common Time Core, commontime/1, version 1.2a) and are marked as such. The rest are implementation examples: they show what one existing implementation does, not what the protocol requires. Where they differ, the specification wins.
The accepted vector¶
Normative, §7. This is the specification's own test vector, chosen under N5 so that an incorrect implementation produces a visibly incorrect answer.
ping t0 = 1789232400000
pong t1 = 1789232401000 response received at 1789232400200
→ offset = t1 − (t0 + rtt/2) = +900 ms
session anchor = 1789232400000, bpm = 118, beatsPerBar = 4, ppq = 960
local clock 1789232460000
serverNow = 1789232460900 elapsed = 60900
tick = 60900 × 118 × 960 / 60000 = 114,979
An uncorrected implementation — one that ignores the offset between clocks — reads the same instant as 113,280. The difference is 1,699 ticks, about 0.9 seconds. That is a beat and a half late, and it is audible.
That is the point of the vector. §7 also records a rejected one: a 40 ms offset at 118 BPM is two percent of a bar, so the corrected and uncorrected answers round to the same number — and a faulty implementation passes.
The five operations¶
Normative, §4. The op field of ct.cmd takes these five values. param
is the only extension point, and its namespace belongs to the application.
op |
What it does | value |
rampTicks |
|---|---|---|---|
start |
slot begins playing its material | — | yes |
stop |
slot stops playing | — | yes |
gain |
slot level | decibels | yes |
param |
application-defined parameter | name + value | yes |
material |
assigns material to the slot | identifier | no |
material is an instantaneous assignment and takes no ramp. rampTicks = 0
means the local minimum: a client must not produce a discontinuity, but the
mechanism is unspecified. Greater than 0 means the value is reached over
atTick → atTick + rampTicks, linear in the parameter's declared unit (N7).
A complete session¶
Implementation example, not normative. This flow was executed, not written: it is the traffic one existing implementation produced over WebSocket (N11), from the start to the first sound.
client → {"type":"ct.hello","v":"commontime/1"}
→ client {"type":"ct.hello","v":"commontime/1"}
→ client {"type":"ct.session","id":"kellari-2026-09-13","anchorEpochMs":1789313361186,
"bpm":118,"beatsPerBar":4,"ppq":960}
client → {"type":"ct.ping","t0":1789313361226}
→ client {"type":"ct.pong","t0":1789313361226,"t1":1789313361227}
→ client {"type":"ct.load","id":1,
"material":"sha256:3f8a2b1c9d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8",
"lengthTicks":15360}
client → {"type":"ct.ready","ref":1,"readyAtTick":157}
→ client {"type":"ct.cmd","id":1,"atTick":15360,"slot":0,"op":"material",
"value":"sha256:3f8a…d7e8","rampTicks":0}
→ client {"type":"ct.cmd","id":2,"atTick":15360,"slot":0,"op":"start",
"value":null,"rampTicks":960}
→ client {"type":"ct.cmd","id":3,"atTick":30720,"slot":0,"op":"gain",
"value":-6,"rampTicks":3840}
Four things worth reading out of it:
ct.load does not assign material to anything. It is a content-addressed
fetch with no slot. Assignment is slot state and therefore a ct.cmd whose op
is material (H21, since version 1.1). A flow that goes from ct.load straight
to start leaves the slot empty, and V3 makes that silence.
Commands are scheduled into the future. atTick 15,360 is four bars, or
8.14 s from the anchor. That is the normal way to use the protocol, not a
special case.
readyAtTick is the client's own estimate, not an echo of the request (N3).
A client that cannot meet the request answers ct.refuse.
ct.cmd is never answered and never acknowledged (N3). A late command is
applied anyway, and the lateness is reported in the late list of ct.state as
{ id, lateTicks }.
If you copy this flow
The value of v is now decided (H30), and it is commontime/1. It is
a compatibility token, not a document version: it does not change with
version 1.3. Do not copy a specification version number into it.
sha256:3f8a…d7e8 is an abbreviation for reading, not a valid
identifier. N13 requires exactly 64 hex characters in lower case, and the
abbreviated form does not parse — a validator rejects it. The full value is
on the ct.load line above.
anchorEpochMs is the clock at the time of the run, not a constant. A
copier gets a session whose anchor is in the past. That is in fact useful —
N10's phase and late commands show up immediately — but it is not a number
to quote as given.
A late command¶
Implementation example, not normative. N3 says a late command is still
applied, and that the lateness is reported in the late list of ct.state.
One executed ct.state shows three different magnitudes at once — and the
magnitude tells you which case it is:
{"type":"ct.state","offsetMs":0,"minRttMs":0,"jitterMs":1,"minLeadTicks":567,
"late":[{"id":1,"lateTicks":113480},
{"id":2,"lateTicks":113480},
{"id":3,"lateTicks":2},
{"id":4,"lateTicks":20002}],
"degraded":false}
id |
lateTicks |
What it is |
|---|---|---|
| 3 | 2 | atTick was "now". Late by the wire alone — 2 ticks is 1.06 ms at 118 BPM, because one millisecond is 1.888 ticks. |
| 4 | 20,002 | A command from the past, deliberately 20,000 ticks. The other 2 is the same wire delay as id 3. |
| 1, 2 | 113,480 | The session's opening commands at atTick 0, with the anchor about 60 s ago. |
Single digits are the network; five digits are history. That is a practical rule you can read straight off the number.
And from the same run, the proof that the commands were applied rather than
discarded: slot 0's gain is −6 dB (command id 3), param cutoff is 800
(command id 4, the one 20,000 ticks late), and the slot is sounding.
A caveat about the magnitude
This run is a loopback, so minRttMs is 0 and id 3's two ticks are a
floor. A local network adds a fraction of a millisecond, so the figure
stays in single digits.
Two commands competing for one slot¶
Normative rule (N8), the example illustrates it. The highest atTick wins;
ties are broken by the higher id.
Slot 0 receives two gain commands, both at atTick 30,720. id 3 with value
−6 and a ramp of 3,840, id 4 with value −12 and no ramp. Same atTick, so the
higher id wins:
{
"type": "ct.snapshot",
"atTick": 40000,
"cmds": [
{"id":1,"atTick":15360,"slot":0,"op":"material","value":"sha256:3f8a…d7e8","rampTicks":0},
{"id":2,"atTick":15360,"slot":0,"op":"start","value":null,"rampTicks":960},
{"id":4,"atTick":30720,"slot":0,"op":"gain","value":-12,"rampTicks":0}
]
}
Three things to point at:
- The loser (
id3) is not there at all. A snapshot is the set of winning commands, not a history. - The winner is the command entire, not a value. The snapshot does not say "gain is now −12 dB". A snapshot must not carry evaluated values (N8).
- The
startcommand's ownatTickof 15,360 survives, and it is the whole reason a late-joining client plays the right point in the material. N10 measures phase from the winningstartcommand'satTick— and that number exists nowhere else but in that command.
The specification measured what its absence would cost: without it a late joiner would be off by 6,976 ticks, 3.7 seconds of an 8.1-second loop (H19).
A client joining mid-loop¶
Implementation example, not normative. This is N10's phase as concrete numbers — the thing a hand-written example cannot prove. The joiner arrives during the seventh iteration:
{"type":"ct.snapshot","atTick":113289,
"cmds":[
{"id":1,"atTick":0, "slot":0,"op":"material","value":"sha256:3f8a…d7e8","rampTicks":0},
{"id":2,"atTick":5760,"slot":0,"op":"start", "value":null,"rampTicks":0},
{"id":3,"atTick":9600,"slot":0,"op":"gain", "value":-6, "rampTicks":0}]}
N10 by hand, with the joiner's own numbers:
T (the joiner's tick now) 113 600
startAtTick 5 760 ← the winning start command's own atTick,
NOT the snapshot's atTick 113 289
lengthTicks 15 360
T − startAtTick 107 840
107 840 mod 15 360 320 ← the phase, and it is not zero
iteration 7
Two lines are worth reading twice. startAtTick is 5,760 and not 113,289 —
the snapshot's own timestamp is no good for measuring phase. And the phase is
320, not 0 — the joiner does not start from the top of the material. Both
were bugs before version 1.1, and both cost a measured 6,976 ticks.
Why N1 binds the wire and not the read head¶
From the same instant the implementation computed a phase of 320.9600 ticks,
not 320. The difference is exactly exactTick − floor(exactTick) — the floor
and nothing else. And that is deliberate:
| Phase | Sample in the material | |
|---|---|---|
| Control plane, integer tick (N1) | 320 | 8,135.6 |
| Read head, fractional tick | 320.9600 | 8,160.0 |
The difference is 24.4 samples, or 0.5 ms — and it is exactly the floor:
0.96 × 25.42 samples per tick is 24.4. The same remainder shows up in both
units, and nothing else separates these two rows.
One tick is 25.42 samples at 48 kHz, so a read head quantised to whole ticks would step audibly.
And one implementation choice that follows straight from V2. This implementation stretches the material to the loop's exact duration rather than playing it at unity rate and cutting the end. The file's deviation then becomes a constant detune — 1.215 ppm here — which resets on every iteration, because V2 requires each iteration's onset to be derived from session time and not from the end of the previous one. Unity rate would instead leave a sub-sample gap on every iteration, and that is a click rather than a detune. N1 requires integers on the control plane — it is a promise that two implementations agree about time, not a requirement about how audio is rendered.
H36 · running this example exposed a gap
The joiner's first ct.state carried late: [] — even though it had just
received three commands whose atTick is 113,600 ticks in the past.
Two normative sentences point in different directions. N3: lateness
must be reported in the late list. N8: ct.snapshot is not a
special case but a set of ct.cmd payloads. Every command in a snapshot
has an atTick in the past by definition — that is the whole reason a
snapshot exists. Which sentence wins is nowhere.
The consequence if implementations differ: under one reading, every
joiner's first ct.state is a burst with one element per slot and
parameter — which means a joining client looks identical in telemetry to
a client that has just lost the network. And joins happen exactly when
telemetry is being read alongside a recording.
The implementer recorded it as F19; the specification numbered it
H36 on 13 September 2026. It is open and has not been decided.
Nor is divergence claimed: one implementation's behaviour is known, the
other's is not.
What in these flows is the implementation's choice
With the same caveat as the value of v — none of these are dictated by
the specification:
minLeadTicks567 is the required lead,ceil(300 ms × 1.888). Item H34 is open, and worse than a gap: §4 gives the field no meaning at all, and the non-normative Appendix A says "observed" where this implementation reports required. The 300 ms is the implementation's own.jitterMs1 — the specification does not say how jitter is computed. Here it is the highest minus the lowest RTT over the last 64 samples.offsetMs0 — N6 gives the formula, but what happens to the half millisecond is the implementation's choice (an unnumbered finding).- The order of
cmds[]— N8 says "a set", so the order is free. Here it is slot,atTick,id. - The anchor and the session id are application layer, not protocol.
The sample-count check¶
Normative, N4. On ct.load a client must verify the material's sample count
against the material's own sample rate, not its own output rate:
samples = lengthTicks × 60 × sampleRate / (ppq × bpm)
For a four-bar loop (lengthTicks 15,360) in a 118 BPM session:
| Sample rate | Required sample count |
|---|---|
| 48,000 Hz | 390,508.4746 |
| 44,100 Hz | 358,779.6610 |
Neither is an integer — and neither is any bar count I tried at this tempo. That is exactly why the computation must be done in exact rational arithmetic and the check passes within ±1 sample. Floating point and rounding would give different answers in different implementations, and the whole point of N4 is that the verdict is identical in every one.
Why ±1 sample, and not looser or tighter. A file's deviation from the exact value does not sit at the end of the file; it shows up in the read head's position, and it grows linearly with the phase. This loop's file is −0.4746 samples off exact:
| Phase | Through the loop | Read-head deviation |
|---|---|---|
| 0 | 0 % | ±0.0000 |
| 3,840 | 25 % | −0.1186 |
| 7,680 | 50 % | −0.2373 |
| 15,360 | 100 % | −0.4746 |
The deviation is zero at the start of the loop and reaches the file's own deviation at its end. A ±1 sample gate therefore bounds the positional error at one sample — and only at the loop's last sample. The gate is exactly as tight as the error it admits, not looser.
Whether the material is musically at the session tempo is not a protocol check. Material at the wrong tempo sounds wrong in every client in the same way, so synchronisation holds.
The material pipeline in practice¶
Application layer — not the protocol
Everything in this section is one application's choices. §5 of the specification puts retrieval of the bytes outside its scope, and the protocol does not know what material is musically — no roles, no instruments, no genre. A conforming implementation owes none of this anything. It is here because it shows what one real material source looks like.
What the pipeline produces: Ogg Opus, 48,000 Hz, stereo, 96 kbit/s VBR,
20 ms frames. One file per role, 3–6 roles per track, typically 16 bars at
118 BPM — 32.54 s and 330–460 kB per role. Materials are loops, not one-shots,
which is what the protocol requires (N9): a slot's material loops from start
until stop, and one-shot playback does not exist.
The digest is computed over the finished .ogg bytes, not over the source
and not before encoding, and the whole sha256:<hex> including the prefix goes
into the material field.
lengthTicks is derived from bars, not from the file¶
This is where material and protocol meet, and the direction is the opposite of what one might expect:
lengthTicks = bars × beatsPerBar × ppq = 16 × 4 × 960 = 61 440
samples = lengthTicks × 60 × sampleRate / (ppq × bpm)
= 61 440 × 60 × 48 000 / (960 × 118)
= 92 160 000 / 59
= 1 562 033.8983… → 1 562 034
The file is not measured and the length is not derived from it. The length is computed from bars and the file is cut to fit: the pipeline generates something overlong, detects the tempo, stretches by at most 5 %, picks a musical cut point and trims exactly the computed sample count. The seam is crossfaded from the surplus, so the fade does not change the length.
Note what the exact value is: 92,160,000 / 59. The denominator is 59, so the
number cannot be an integer at any bar count at this tempo. Rounding happens
only at the end, and N4's gate accepts ±1 sample. Here the deviation is 0.1017
samples.
Roles and slots¶
The protocol knows only numbered slots. The mapping from role to slot is an application convention, not the protocol's:
| Slot | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Role | DRUMS | BASS | SEQ | CHORD | LEAD | VOX |
A missing role is silence in its own slot, not an error — which is exactly what V3 requires.
Three things that went wrong, all measured¶
Opus does not preserve peak. An encoded file can exceed the source's true peak by roughly 0.4–0.6 dB. If the peak is measured from the wav before encoding, tracks reach playback over zero. The peak must be measured from the decoded Opus. This is structurally the same mistake as N14: the correct figure only exists after the conversion, and the figure measured before it looks entirely plausible.
Granule position is not duration. The last granule of an Ogg Opus file
includes the pre-skip, so the correct duration is
(last granule − pre-skip) / 48000. Without the subtraction every loop is a few
milliseconds too long — and since V2 forbids concatenating iterations the error
does not accumulate in playback, but it makes a file that N4's check rejects.
The sum of six stems is not one stem. Six tracks each normalised to −14 LUFS summed to +10.2 dBFS and clipped 1.8 % of samples. The sum is now normalised with one common gain, which preserves the relative levels between tracks.
Open: no real multi-client session has been run
On the material side only a single-deck run has been measured. Publishing
is three HTTP calls and playback starts with one, which reads a track's
arrangement and sends start commands at each section's first bar — 64
bars, at most three tracks at once. But several clients have not been run
together over a real network, so there is no figure for it here.
The same loop, at different tempi¶
This is what a fractional bar position does not give. Loop length in ticks does not depend on tempo at all; only its duration in seconds does.
| Tempo | 16 bars in ticks | Duration |
|---|---|---|
| 100 BPM | 61 440 | 38.4000 s |
| 118 BPM | 61 440 | 32.5424 s |
| 120 BPM | 61 440 | 32.0000 s |
| 140 BPM | 61 440 | 27.4286 s |
61 440 is exactly 16 × ppq × beatsPerBar = 16 × 960 × 4. It is an integer
at every tempo, in every implementation, with no rounding — which is why two
loops can be checked for equality by comparing two integers.
From a bar to a tick¶
The session is in 4/4 and ppq is 960, so one bar is
ticksPerBar = 960 × 4 = 3 840 ticks.
bar 25, beat 1 = 25 × 3 840 = 96 000 ticks
With the anchor at 13 September 2026, 00:00:00 UTC and a tempo of 118 BPM, tick
96 000 falls at 00:00:50.847 UTC. A client never receives that wall clock
time over the wire: it receives the tick 96000 and derives the instant from
the anchor itself.
N14 in practice¶
A tick may be negative, because a session may be announced before its anchor. Three instants before the anchor:
| Instant | Tick | Bar / beat | Truncating division would give |
|---|---|---|---|
| anchor − 1 ms | −2 | −1 / 4 | −1 ✗ |
| anchor − 500 ms | −944 | −1 / 4 | −944 |
| anchor − 60 000 ms | −113 280 | −30 / 3 | −113 280 |
The first row is the whole of N14 in a single number. One millisecond before the
anchor the correct tick is −2, but JavaScript's / — and the native
operator of most other languages — truncates toward zero and gives −1.
The other two rows reveal nothing: their remainder happens to be zero, so truncation and flooring agree. Which is why the bug does not show up in a test that tries round numbers — exactly what N5 says about choosing test vectors.
What an example cannot show¶
Two items are still open, and neither can be shown as an example without the example deciding the matter on the specification's behalf.
The joining client's message order. Not specified. The two existing
implementations differ (item H32): one sends a joiner ct.hello →
ct.session → ct.snapshot and no ct.load messages at all; the other sends a
ct.load for every material before the snapshot. The latter has a good reason: without
ct.load the client has no lengthTicks, so it cannot compute the N10
position, and the slot stays silent permanently. The implementations
interoperate: H32 passes when run. This is an open item, not an
incompatibility.
Lateness of commands applied from a snapshot. N3 requires lateness to be
reported in late[]; N8 says a snapshot's commands are processed as commands
are, and their atTick is in the past by definition. Which sentence wins is not
in the text. The item is H36, open — see above.
The value of v was the third item on this list, and it is now decided.
H30 was settled on 13 September 2026: the value is commontime/1 as a
compatibility token. See Status for the reasoning.
Items like these are why a third implementation is the most valuable thing anyone can do with this specification.