Broadcast playout · Karhu TV

At 1080p50, a frame is 20 milliseconds. Our clock error is measured in nanoseconds.

Karhu TV builds Rytmi — a software playout engine that turns a stock PC and a Blackmagic DeckLink card into a TAI-disciplined SDI playout chain, with a tamper-evident as-run log built into the timing core rather than bolted on afterwards. The free opx-* tools below are the instruments we wrote to prove it correct. By 2 August 2026 the reference unit had delivered 7,656,700 frames in its then-current soak with none late.

0 ns one frame · 1080p50 · 20,000,000 ns
SOAK LIVE

What we actually make

Rytmi is a playout engine, in software. Syke is its clock.

There are two conventional places to take playout timing from, and both leave the card's own clock unmeasured. Pace from the host and you assume the SDI card agrees with it; lock to house sync and you assume the reference will always be there. Either way nobody is watching the crystal in the card, and it is a few parts per million off — ours by about −3.7 ppm through 1 Aug 2026, which is one slipped frame every hour and a half or so if nothing corrects for it.

Syke paces from TAI — a monotonic timescale with no leap seconds — and measures the card's drift continuously rather than assuming it away. Rytmi's timing authority is Syke's, and Syke's is TAI. Every schedule item airs against an absolute instant, not an offset from whenever the process happened to start.

Which is why losing house sync doesn't cost you your time base. Take the second of those approaches and the reference is your timing authority, so when it goes you are left on an uncorrected crystal. Syke's authority is TAI — and Rytmi's is Syke's — so the card's rate error is a measured quantity rather than an assumption; we predict its consequences to within 1 %. Genlock is something we accept, not something we depend on: the design rule from the start was that the clock must not rely on it, and the reference unit runs free — with the card reporting no late or dropped frames of its own, and the engine's own late-frame count published live in the panel above and explained, when it is not zero, in the table below.

The as-run log is a hash chain. Each record commits to the one before it, blocks are sealed and fsynced, and rotated day files are verifiable across boundaries. Compliance isn't a report generated later from something that might have been edited — it's structural.

And it is software, not an appliance. The machine behind every figure on this page is a stock Intel desktop and a DeckLink card any broadcast dealer stocks. The nanoseconds come from measurement, not from custom silicon — the engine treats every clock in the box as something to verify rather than trust, which is why it holds time on hardware you can buy, replace, and keep spares for. When a card fails, you swap a card — not a channel appliance.

One price, everything enabled, bought once. There is no feature matrix, no per-channel tier, no per-output unlock and no annual renewal: the licence is perpetual and the software you install is the whole software. We price this way because the alternative — charging separately for the things a playout system already has to do — makes the vendor's incentives point away from the operator's. It also means the number we quote is the number, which is not universally true in this market.

Four independent SDI outputs per card. Runs on Ubuntu. Binary-only, under NDA — the timing core is the company, so it isn't published. Everything below it is.

Evidence

Every number here has a log behind it

Nothing on this page is a target, a projection, or a figure from a datasheet. Each row is something we measured on hardware and can show you the capture for.

MeasurementResultHow it was established
Late frames, pre-registered null test 0 adjudicated 30 Jul 2026. 35.8 h — three consecutive epochs spanning two scheduled restarts; 1 Hz sampling, 99.98 % coverage, criteria frozen before any data existed. That was a different and shorter test; for the 14-day campaign running now, see the row below
Late frames, 14-day campaign — in progress 1 1 as at 12 Aug 2026, on a campaign still running; the live panel above carries the current figure, for the current run. The late frame itself was 11 Aug, 03:25 UTC, and the panel had it by 03:33 — eight minutes later, unattended, because that panel is generated from the engine's own counters every fifteen minutes rather than written by hand. The number was public a day before this explanation was, which is the way round we want it. The criterion for this campaign was frozen before it started and allows zero, so the campaign has failed its own test — and that is the row, rather than the clean days either side of it. What the counter actually means: on one tick out of the fifty in one second, the clock thread woke 567 µs after the instant it was scheduled for. The frame was still built and the card's output buffer absorbed it, so nothing reached the SDI output late, missing or out of order. This is an internal timing miss, not a dropped frame, and we would rather explain the difference than let the word do work it hasn't earned. Through 12 Aug it is one of nine such excursions, 380–567 µs, and the other eight fell just under the 500 µs line this counter trips at — which is itself worth knowing, because the threshold sits inside the cluster rather than clear of it. Ruled out, each by a measurement that could have said otherwise: the time daemon's own steering — we read TAI from the kernel and NTP disciplines that clock, so a correction applied to it was a live candidate and had to be excluded by measurement rather than by assertion — and the as-run block seal, thermal, and any engine stall or restart. Not ruled out, and not ruled in: clip junctions — they run every 25 s on this schedule, so proximity to one carries no information either way, and saying we had excluded them would be dressing an untestable up as a result. Ruled in: they repeat every 112.5 s within a single item, and all nine fall inside a long MXF decode — though that file had played about 150 times by 12 Aug and produced them on three of those, so it is a correlation and not an explanation. Cause not determined. We publish it undetermined rather than attach the tidiest available story to it
Frame underruns, after start-up priming withdrawn 4 Aug 2026, and the empty cell is the point. This row read 0, from the 30 Jul null test. On 4 Aug we found the instrument wanting: it published a starvation run-length that reset the moment a frame arrived, so a recovered underrun — nearly all of them — was structurally invisible to it. That 0 was not so much false as unearned; the counter could not have reported anything else. It is cumulative now and stands at whatever decoder priming cost that run — the live panel above carries the figure — so the bar has moved: what would earn this row back is a completed run in which the counter does not grow after start-up. As at 12 Aug 2026 the running campaign had gone five days with no post-priming growth, so the row is on course to earn its value back when a run completes. Until one does, the honest value is no value
Clock phase, typical (mean magnitude) 45 ns mean of |phase| per tick, one 4,552 s window, 2 Aug 2026 — the live panel above reports the current value of this same quantity
Clock phase, median of the per-second peak 175 ns a different quantity from the row above: worst tick in each second, then the median of those; p99 1,060 ns. Same 4,552 s window, 2 Aug 2026
Worst single tick, per run 944–3,091 ns the typical band, on most runs since the fsync fix of 28 Jul 2026. Every run that exceeded it, through 12 Aug 2026: 39 µs, 198 µs, 331 µs, 434 µs, 567 µs, 1.21 ms, 1.35 ms, 4.93 ms and 23.45 ms — nine, and this list was checked against the log by re-derivation on 12 Aug after its previous version claimed “every” while missing one (198 µs, an eight-minute run during the 4 Aug deploy restarts). Two further runs on 28 Jul itself exceeded the band before the fix landed at 22:52 UTC that day and are the previous era’s numbers, not this row’s. That last one is longer than a frame, and on a page whose first line is that a frame is 20 milliseconds it is the number you should want from us: it happened on 4 Aug, it came with two starved decoder frames rather than a clock fault — the phase figure is the consequence of a repeated frame, not its cause — and it is the reason the underrun instrument two rows up was rebuilt and that row now shows no value at all. Until 12 Aug this row listed only the first four and said local build load was the common thread. Both were wrong: the list was nine days stale and omitted the two largest, and the 14-day campaign has since produced nine excursions of 380–567 µs on an idle machine with nobody logged in, so local load was a property of those first four rather than the cause of the class. Cause open. Host load is recorded alongside, and since 12 Aug so is every interrupt the clock core takes
Effect of moving one fsync off the clock thread 578× the fix deployed 28 Jul 2026. Median worst tick per run, 865,037 → 1,496 ns; 9 runs before, 7 after, no overlap between the two sets. Runs were a uniform 12 h at the time, which is what makes them comparable; that cadence has since been retired. Observational, not a controlled A/B — the controlled test on that fix measured late frames, not phase. Restricted to full-length runs on both sides — the figure is a latched maximum, so mixing in short ones flattered it to 693× — and the after-set excludes one further full-length run, 2 Aug, whose 434 µs peak is the build-load excursion the “Worst single tick” row lists; leaving it in changes the median not at all and the 578× barely, but saying “no overlap” without saying so would be a quiet cherry-pick
As-run records checked for tampering ≥ 44,300,073 as of 28 Jul 2026; 0 false positives, six forgery vectors closed. Cumulative, so this is a floor too
Card crystal error vs TAI −3.7 ppm median over 26 Jul–1 Aug 2026, and dated because it is not a constant: the crystal is temperature-sensitive, and this is measured against CLOCK_TAI, which NTP disciplines — so a clock correction is arithmetically indistinguishable from crystal drift in it
Time-to-slip prediction from that drift 1.007× observed ÷ predicted, median over 14 runs to 31 Jul 2026. A model fit, so it moves as runs accumulate — what is claimed is that the model tracks, not that this digit is fixed
Longest continuous engine run ≥ 39 h 19–21 Jul 2026, from the as-run chain rather than from memory. A floor, not a current reading — it only ever grows, and the live panel above carries the run in progress

The register

What we don't claim, why, and what would earn it

In April we decided internally not to ship HEVC. The website went on advertising it for three more months. Nobody caught it, because nothing was watching — and a claim nobody is watching is indistinguishable from a lie to the customer who relies on it.

This is what we keep instead of that habit. Every row is a thing we could plausibly say and don't. It is on the public site rather than in an internal file, because a register that lives where customers can't read it is the same failure with better manners.

Licensing

HEVC / H.265 support

Out of scope on patent-licensing grounds since April 2026. Not claimed, not tested, not marketed.

Would earn it: an MPEG-LA licence and a hardware test pass. Neither is planned.

Measured — but only as far as our own instruments reach

Glass-to-glass latency

We've measured the engine-side component over 200 trials. It is a lower bound: it stops at the point the frame enters the handoff queue, and the card pipeline beyond that point adds at least 60 ms this number does not include. Quoting it as glass-to-glass would understate our own latency by an unknown margin.

Would earn it: a capture rig on the SDI output measuring the round trip end to end.

“Never drops a frame”

The card's crystal runs about 3.7 ppm slow against TAI, so the engine produces marginally faster than the card consumes. A surplus frame is discarded from the handoff queue roughly every 90 minutes at the drift measured through 1 Aug 2026 — by design, and at a rate we predict from the live drift to within 1 %. The card itself reports zero late and zero dropped frames after start-up. Whether a discarded frame is visible on the wire is engine-side accounting, not a wire measurement.

Would earn it: the same capture rig. Until then we report what our counters see and say which side of the card they sit on.

Holdover through a reference failure

The architecture is built for it and the free-running case is evidenced — we run without a house reference and the drift compensation is measured, not assumed. What we have not exercised is genlock at all: no valid house reference has yet been on this bench, so locked operation and the transition across a reference failure are both unmeasured. The card's genlock-source setting exists in our configuration and is currently read by nothing, and the telemetry that would let us watch the reference state has been written and went on air on 2 Aug 2026; as at 12 Aug 2026 it reads unlocked, which is the correct answer for a bench with no reference. So we can tell you the box runs correctly free. We cannot yet tell you what it does locked, or what the first second after a reference failure looks like.

Would earn it: a reference on the bench, lock observed, and then pulling that reference on a live channel and measuring the edge. The telemetry needed to watch it is already on air.

Loudness monitoring on air

We ship a full BS.1770 loudness meter — and it does not run on the SDI path. It reads decoded source audio, upstream of the output mix, so it structurally cannot see a conform error. A number from it would describe the file, not the transmission.

Would earn it: metering the output bed itself, on its own thread. Designed; not built.

Structural, as the system stands today

Item-level granularity in the as-run log

The as-run is a tamper-evident hash chain and we stand behind it: 44.3 million records checked, no false positives, six forgery vectors closed. It records one entry per frame, which makes it exact about what played and when, to the frame. What it does not carry is an item boundary — so two consecutive plays of the same file read as one continuous run of double the length. Integrity is proven; item-level granularity is not, and we would rather tell you which is which than let the first imply the second.

Would earn it: an item-boundary record. The obstacle is that our record format is frozen — adding a field would invalidate every archive written to date — so it needs a versioned format change, done deliberately.

Cross-family rates on one card while genlocked

Mixed frame rates are supported and normal — this row is about one specific edge, so here is the whole map rather than a caveat that sounds bigger than it is.

Sync mode Same family — 50 + 25, or 59.94 + 29.97 Cross family — 50 + 59.94
Genlocked to house reference By design — decimated from one tick. Lock itself is untested here; see below. No. One REF IN, one PLL, four sub-devices. Physical.
Free-running on internal sync Yes — and this is the mode the clock was designed for, not a fallback. Not barred by the hardware — barred by us. See below.

The genlocked no is a physical fact and no software fixes it. The genlocked by design is exactly that — no house reference has been on this bench yet, the same gap the holdover row above names, so that cell is intent rather than measurement until a reference has been on the bench. The free-running cell is our limit, not the card's: below the master clock the pipeline is already per-channel, and the card derives both families from one crystal happily. What is single is our master tick, and one tick has to divide exactly into every rate it serves. 59.94 is not 60 — it is 60000⁄1001, and 1001 shares no factor with 60000, so nothing cancels. The smallest tick that lands on both is 60 kHz: 1,200 ticks per 50 Hz frame, 1,001 per 59.94 — twelve hundred times today's rate. (Were the second rate an exact 60, it would be 300 Hz and this row would not exist; the whole cost is the NTSC 1001.) That is an engineering choice of ours, not a law of physics, and it is a choice we mean to reverse: the 60 kHz master clock is planned, and this row is written to be deleted. We would rather say that than let a decision of ours read as a hardware limit.

Would earn it: genlocked cross-family — a second card with its own reference. Free-running cross-family — the 60 kHz master tick, or independent cadence sources per channel. Ask if you need it; the answer is a schedule, not a no.

Not yet earned

Uptime and reliability figures

Our internal threshold for saying “reliable” is 90 days continuous. The longest run we could evidence from the as-run chain as of 1 August 2026 was 39 hours. That figure is dated on purpose: runs get longer, and a number stated as current would be wrong the next time one does. What does not move is the argument — 39 hours is not 90 days, and until it is we do not use the word.

Would earn it: 90 days. There is no shortcut and we won't paraphrase our way to one.

ATSC A/85 loudness conformance

A/85 assesses the anchor element — dialogue — not integrated programme loudness, which is what our meter measures. For dialogue-sparse content the two differ materially. The 2026 revision also requires true peak to be measured before encoding, which a delivered file no longer permits. Our tool therefore returns INCONCLUSIVE for A/85, and it is impossible for it to return a pass: the refusal is enforced in code, before any number is compared.

Would earn it: dialogue-gated measurement. A different algorithm, not a threshold change.

If a claim you need isn't here and isn't on the evidence table, ask — the answer will be a measurement, a date, or “we don't know yet”.

Tools

The instruments, and what else runs on the clock

The opx-* tools exist because we needed them to prove Rytmi correct. They're free, binary-only, Linux and Windows. Published checksums; --version on every one, because that's the first thing a support conversation asks. The last card is neither an instrument nor finished — it is here because it shares Rytmi's timing code, and we would rather show it early than announce it late.

opx-probev0.2 · available

What is this file? Container, codec, exact rational frame rate, audio config, MXF sidecar. And --house pre-flights it against your delivery spec, exiting non-zero if it won't conform.

Linux x86-64Windows x86-64All releases & checksums

opx-driftv0.2.1 · available

Is its timeline sane? PTS drift and A/V sync from exact rational arithmetic — 29.97 is 30000/1001, and rounding it is how drift gets designed in.

Linux x86-64Windows x86-64All releases & checksums

opx-loudnessin progress

Is it deliverable? EBU R128 and Free TV OP-59, with the thresholds read from the issuing bodies' own documents rather than a summary. Says INCONCLUSIVE where it can't honestly say pass.

opx-verifyin progress

Is the compliance chain intact? Verifies a hash-chained as-run archive across day-file boundaries and restarts. A single file can't check what it continues.

Karhu Playerin development · not released

A media player for people who have to trust the timecode. Built for broadcast use rather than adapted to it, with EBU R128 loudness metering in the engine rather than bolted on. It shares Rytmi's timing crate — TAI time sourcing, exact rational frame arithmetic and the precision sleep are the same code, not a reimplementation, and every ported item carries a provenance record naming its origin in the engine.

Work in progress and not available — no download, no date. It is listed because it exists and because a player that says 29.97 when the file says 30000/1001 is the problem it was written to stop being.