Broadcast playout · Karhu TV
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. Between 6 and 20 August 2026 the reference unit produced 61,088,850 frames in one unbroken 339-hour run, with none late on the SDI output.
What we actually make
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 — with one open defect, RET-122, named in the register below: the head of a looping region airs twice per loop, and displaces what follows.
Which is why, by design, losing house sync would not cost you your time base — a design we have not yet tested, because no house reference has been on this bench. 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 from a measured figure — to within 1 % at the median over the twelve-hour runs of July 2026 (individual runs −8 %/+19 %), and to 14 % over the 339-hour run of August, where the sheds arrived sooner than the model said because the drift number we publish is a run-cumulative mean that lags the crystal (median observed ÷ predicted 0.863 over 213 intervals, re-derived 21 Aug 2026). Genlock is something the card can take and we do not depend on: the design rule from the start was that the clock must not rely on it, and the reference unit runs free — through the 339-hour soak that ended on 20 August 2026 the card reported no late and, after start-up, no dropped frames of its own, and the engine's own late-frame count is carried in the panel above while a soak is running 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 — first driven from one process on 5 Sep 2026, on the second unit, bars only, not yet soaked. A source faster than the house rate airs slow (0.834×, RET-128) and is flagged, not refused. Runs on Ubuntu. Binary-only, under NDA — the timing core is the company, so it isn't published.
Evidence
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.
| Measurement | Result | How it was established |
|---|---|---|
| Late engine 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, which has since concluded, see the row below |
| Late engine frames, 14-day campaign — concluded | 2 | 2 across the full fourteen days, and zero late frames on the SDI output — two counters, and the card reported none of its own. The campaign ran to its frozen end and the engine was then stopped deliberately, at 2026-08-20T23:16:43 UTC, after 339 h unbroken. Both events: 11 Aug, 03:25 UTC (567 µs) and 15 Aug, 08:50 UTC (570 µs). The panel had the first by 03:33 — eight minutes later, unattended, because that panel was generated from the engine's own counters every fifteen minutes rather than written by hand. We did not notice the second for two days. The panel published it within minutes, as it is built to; nobody was reading the panel. That is worth saying plainly, because the automation and the attention are different claims and only the first one held. 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. The second event does not change that verdict; the first had already decided 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. Re-derived over the whole campaign after it ended, the two are members of a final population of 109 excursions of 380 µs or more — not the nine this row reported through 12 Aug, and not the 67 it reported through 17 Aug while the run was still going. They are all in the same direction (the clock thread always wakes late, never early), and 106 of the 109 sit in a tight band of 380–450 µs, median 413. This corrects what we published here before. On nine samples we said the 500 µs line that trips this counter sat inside the cluster; on 109 it does not. The two late frames stand 116 µs clear of the highest of the other 107, with nothing in between — so they are not simply the members that happened to cross an arbitrary line, and the threshold is not what made them count. That is a worse result than the one it replaces, in the sense that it removes an easy explanation, and it is the one the data supports. 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 typically 25 s apart (median) 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: the ~113 s recurrence (measured 112.68 s), and it is now much harder to argue with than it was on nine samples. The 109 arrived in 36 bursts of one to five, and every gap inside a burst is 112 or 113 s, or exactly twice that where a beat is missed — 73 gaps, no other value, none. A metronome that regular is not local load, and the magnitudes are indistinguishable between the bursts that fall in working hours and those at 02:00 and 05:00 local with nobody logged in (median 412 against 413 µs). Re-derived after the campaign ended, and it did not hold: the observation that the original nine all fell inside a long MXF decode was established on those nine, and the full population broke it — events land inside items as short as six seconds, and in more than one format, so “long MXF” was a property of the first nine rather than of the class. There is a grouping that does separate the population cleanly, and it is about how an item is handled on its way to the output rather than what the item is; that is still an association and it still does not name a cause. Cause not determined. We publish it undetermined rather than attach the tidiest available story to it — and this row has now had to correct itself twice, which is what publishing an open question costs compared with waiting for a closed one |
| 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 reads whatever decoder priming cost a run — the panel above carries the figure whenever a soak is running — 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. A run has now completed, and that bar is met: across the 14-day run the counter read 71 — all of it decoder priming, inside the first eight seconds — and then did not move again for 339 hours, in all 20,363 samples of it. The cell stays empty until the fix that produced that number has run somewhere other than the bench that found the bug, because the value it would publish is a zero and a zero is exactly what this row was wrong about the first time. Earning it back needs one more thing than a good result, and note that this row stays empty while the campaign row above it has already failed, which is the point of scoring them separately |
| Clock phase, typical (mean magnitude) | 45 ns | mean of |phase| per tick, one 4,552 s window, 2 Aug 2026 — the panel above reports this same quantity whenever a soak is running |
| 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 14 of the 20 runs between the fsync fix of 28 Jul and 18 Aug 2026. Every run from 19 Aug to 5 Sep 2026 exceeded it, the worst at 904,629 ns. Every run that exceeded it, through 18 Aug 2026: 39 µs, 198 µs, 331 µs, 434 µs, 570 µ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 produced 109 excursions of 380 µs or more, in both attended and unattended hours and at indistinguishable magnitudes, so local load was a property of those first four rather than the cause of the class. The entry above moved 567 → 570 µs on 15 Aug: it is that one run's latched worst tick, and 570 µs is where it finished. Exactly two of the 109 exceeded 500 µs, which is the threshold the late-frame counter itself uses — so the two counters agree without being wired together. Cause open. Host load was recorded alongside throughout, and from 12 Aug so was every interrupt the clock core took |
| 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 moves the median to 1,920 ns and the factor to ~450× — which is why the exclusion is stated rather than silent, and saying “no overlap” without saying so would be a quiet cherry-pick |
| As-run records checked for tampering | ≥ 120,483,920 | re-verified 21 Aug 2026 over the whole archive after the 14-day run: 47 day files, 120,483,920 records, 13 chain segments, 0 false positives, six forgery vectors closed. One broken boundary, 20→21 June 2026, known and pre-dating that run — it is a real finding and it is why the archive reads INVALID rather than clean. 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. Two further caveats, found on 21 Aug 2026 when the fortnight's logs were re-read: the instrument is a run-cumulative mean rather than an instantaneous rate, so it lags, and after 339 hours it understated the true value by about 16 %; and the true instantaneous drift moved between −2.2 and −4.2 ppm across that run, tracking case temperature at roughly −0.34 ppm/°C |
| 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. It moved: over the 339-hour run of August 2026 the same fit reads 0.863 across 213 shed intervals, because the drift figure it divides by is a run-cumulative mean that lags the crystal over a run that long. Corrected for that lag it reads 1.0025 |
| Longest continuous engine run | 339 h | 6–20 Aug 2026 — a final figure, not a floor: the run ended at 2026-08-20T23:16:43Z when the engine was stopped deliberately at the close of the campaign. Derived three independent ways that agree to within twenty seconds — one part in 61,000: the service manager (systemd) reports no restart across 2026-08-06T19:53:26Z–2026-08-20T23:16:43Z (339.3881 h), the engine’s own frame counter reached 61,088,850 at 50 fps (339.3825 h, read 14 s before the stop), and the 1 Hz diagnostic line spans 339.3861 h. Rounded down to whole hours |
The campaign missed its own criterion on 11 August, and a failed test deserves more time than a passing one would have got. When the run ended we re-read the fortnight from eight independent directions — the clock's own record, the compliance log, the host's counters and the archive — and most of the value is in what that ruled out.
The two late frames turned out not to be two events. They are the largest members of a population the instruments had been recording all along and nobody had counted, because the counter that trips at half a millisecond only ever saw the two that crossed it. They are all in one direction, they are all close to one size, the clock is back inside its ordinary range within the following second every time, and every one of them is independently visible in the as-run log — which is worth knowing about a compliance log, because it was not built to be a timing instrument.
What that buys: it is a repeatable mechanism rather than noise; it is bounded — under 3 % of one frame at its worst over the 339-hour run of August 2026, 4.5 % on the 228-hour run that ended 4 Sep 2026 — and none of it reached the SDI output; and it is not caused by the things it would be convenient to blame. Around twenty candidate causes have now been tested and rejected, several of them ours — and on 21 August the last two that were still standing went the same way. What is left is not a shorter list of the same kind of suspect, and the next step is still a measurement rather than a patch.
The same logs carry both halves of this run: the junctions that were exact and the wakes that were not. We still have not found the cause, and this section will not carry a story in place of one. What changed is that the question got small enough to answer, on instruments that were already running.
The register
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
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
We measured the engine-side component over 200 trials on 30 Jul 2026. 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 — the card buffer measured 8 frames, 160 ms, unchanged across 1,824 samples over 30 h to 1 Aug 2026. Scheduled-event accuracy on the wire is likewise unmeasured (engine-side, +22–31 ms). 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.
The card's crystal ran about 3.7 ppm slow against TAI over 26 Jul–1 Aug 2026, 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 predicted from the measured drift to within 1 % at the median over July 2026's twelve-hour runs (individual runs −8 %/+19 %) — and 14 % over the 339-hour August run, median observed ÷ predicted 0.863 across 213 shed intervals, re-derived 21 Aug 2026. The card itself reported zero late and zero dropped frames after start-up, across the whole 339-hour run. 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.
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 as of 21 Aug 2026 is 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; it read unlocked, and has done in every one of 25,876 samples to 21 Aug 2026 — including all 339 hours of the soak that ended on 20 Aug 2026 — since it began publishing — 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 built and has already run on air.
We have a full BS.1770 loudness meter in the codebase — and on the SDI product it is wired to nothing an operator can see. As built it reads decoded source audio, upstream of the output mix, so even wired it could not 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
The as-run is a tamper-evident hash chain and we stand behind it: 120,483,920 records checked as of 21 August 2026, no false positives, six forgery vectors closed, one known broken boundary (20→21 June 2026, before that run). 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. That is not hypothetical: RET-122, an open defect, airs the head of a looping region twice per loop on the reference unit today, and this log cannot show it. 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.
Mixed source rates into one output rate are supported, normal and proven on hardware; the engine binds one output rate per box today, and no 1001-family (NTSC) frame has yet been on the wire from it. This row is about one specific edge, so here is the whole map rather than a caveat that sounds bigger than it is.
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 (120 kHz once 23.976, which we also admit, is on the grid — ruled 18 Aug 2026): 1,200 ticks per 50 Hz frame, 1,001 per 59.94 — twelve hundred times today's grid resolution, but only 2.2× its wake rate, because you visit a grid point only where a frame boundary falls: 109.9 wakes a second against 50 today, measured 7 Aug 2026. (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 finer master grid is planned — 120 kHz — 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 120 kHz master tick, or independent cadence sources per channel. Ask if you need it; the answer is a scope, not a no.
Not yet earned
Our internal threshold for saying “reliable” is 90 days continuous. The longest run we could evidence from the as-run chain as of 21 August 2026 is 339 hours — just over fourteen days. That figure is dated on purpose: runs get longer, and a number stated as current would be wrong the next time one does. It said 39 hours until 21 August, which was true when written on 1 August and had been beaten twice before anyone re-derived it. What does not move is the argument — 339 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.
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 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.
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.
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.
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.
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.
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. Its frame-rate arithmetic and handoff ring are Rytmi's — copied verbatim from a dated engine snapshot, each with a provenance record naming its origin; its TAI sourcing and precision sleep are new code built to the same design, and the record says which is which 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.