Repository navigation
chore(windows): pin the LTC and MTC wire formats as golden vectors (#728 M2 slice 1) - #857
Merged
Merged
Conversation
Red on purpose: the two drift guards write golden/ltc-wire-v1.json and golden/mtc-wire-v1.json on first run and then fail, so the generated contract is committed and reviewed rather than silently accepted in CI. The hand-computed pins alongside them pass already — the sync word, the 25 fps correction-bit placement, the 24 fps / 48 kHz midpoint tie, the quarter-frame nibbles and the Full Frame SysEx are all checked against SMPTE 12M / the MTC spec rather than read off the implementation.
26 LTC cases (16 frame words across all four rates, 10 PCM encodes) and 44 MTC cases (4 rate codes, 16 quarter-frame sequences, 16 Full Frames, 8 clamp cases). Turns the guards from the previous commit green. The PCM travels as run lengths — [[sign, sampleCount], …] — rather than 2000 floats per case: the run boundaries are exactly what the rounding decides, so nothing the contract needs is lost. The 24 fps / 48 kHz case shows the midpoint tie directly as the [1, 13], [-1, 12] pair.
…854) LtcFrame, LtcEncoder and MtcFrame, verified case-for-case against the golden vectors macOS emits (644 C# tests, up from 566). Two C# translation hazards the vectors exist to catch are handled in the mirrors rather than papered over in the tests: - LtcEncoder rounds with MidpointRounding.AwayFromZero. Swift's .rounded() is half-away-from-zero; .NET's default Math.Round is banker's rounding, and at 24 fps / 48 kHz halfBitSamples is exactly 12.5, so every odd slot boundary is a tie. The default would put every other run one sample early. - MtcFrame casts every shift back to byte. In C# the operands of << and >> promote to int, so piece 7 — where the rate bits sit next to the hour's high bit — is one missing cast away from a widened value. LTCBiphaseEncoder is deliberately outside the contract: it has no production caller, and LTCEncoder.samples inlines its own modulation against fractional slot boundaries that samplesPerHalfBit: Int cannot express.
The three rows the vectors pin now say so: the rate-dependent correction bit, the rounding rule (the 24 fps / 48 kHz midpoint tie is the reason LtcEncoder rounds away from zero), and the quarter-frame clamp.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pins the LTC and MTC wire formats as cross-platform golden vectors and mirrors them in
OnlyCue.Core, so the Windows port's byte- and sample-level output can never quietly drift from macOS. First slice of epic #728's M2 — pure logic only, no WinUI, fully developed and verified on a Mac.Closes #854
Motivation
M0/M1 pinned the document model, timecode, tempo grid, cue presentation, MA2 and OSC. What was still unpinned was the part where a translation bug is inaudible until showtime: the 80-bit SMPTE 12M word and the PCM it modulates into, and the MTC bytes that go on the wire. Both are places where C# and Swift differ by default rather than by mistake — see the two hazards below.
What Changed
golden/ltc-wire-v1.json— 26 cases. 16framewords (all four rates; the zero word, an interior value, the last frame of the day, both 30df counting boundaries, and two extra 25 fps values) and 10encodePCM cases across(rate, sampleRate, amplitude, startLevel).golden/mtc-wire-v1.json— 44 cases. 4 rate codes, 16 quarter-frame sequences (wholeF1 <data>messages × 8, so the status byte and piece ordering are pinned too), 16 Full Frame SysEx messages, and 8 clamp cases (piece = -1and8at every rate).OnlyCueTests/{LTC,MTC}WireGolden.swift+…GoldenVectorTests.swift— the generators and drift guards, modelled onTimecodeGoldenVectorTests.swift. macOS is the source of truth; a missing file bootstraps and fails, so a new contract is committed and reviewed rather than accepted silently in CI.windows/OnlyCue.Core/Ltc/{LtcFrame,LtcEncoder}.cs,windows/OnlyCue.Core/Midi/MtcFrame.cs— the C# mirrors.windows/OnlyCue.Core.Tests/{Ltc,Mtc}Wire{Vector,GoldenVectorTests}.cs— the verifiers plus independent hand-computed pins, so a wrong C# core still fails even if the golden file were ever regenerated from a wrong source.docs/architecture.md— the three rows the vectors pin now say so.The two hazards this exists to catch
1. Rounding is part of the LTC wire format. Slot
kspans[round(k·R) … round((k+1)·R))withR = sampleRate / (160·fps). At 24 fps / 48 kHzRis exactly 12.5, so every odd slot boundary is a midpoint tie. Swift's.rounded()is half-away-from-zero; .NET's defaultMath.Round(double)is banker's rounding and lands on the even neighbour.LtcEncodertherefore rounds with an explicitMidpointRounding.AwayFromZero. The tie is visible in the vectors as the[1, 13], [-1, 12]run pair.2. The 25 fps correction bit. The vectors assert bits 27 and 59 explicitly at every rate, in both directions — set and clear — so a port that parks the parity bit at one position fails rather than passing on the rates where the bit happens to be clear. (#853 fixed this on the Swift side first; this pins it.)
PCM travels as run lengths (
[[sign, sampleCount], …]) rather than 2000 floats per case: the run boundaries are exactly what the rounding decides, so nothing the contract needs is lost, andtest_runLengthEncoding_roundTripsTheSamplesproves the encoding is lossless for a ±amplitude signal.How to Verify
./scripts/dev/test.sh→ 1852 tests, 0 failures (was 1845).dotnet test windows/→ 644 passed, 0 failed (was 566; +78 = 70 golden cases + 8 hand pins).swiftlint --strict --quiet→ exit 0.def88d4), then turned green by committing the generated vectors (4ada810).Mutation testing
Each mutant was proven applied (
grep -n MUTANT) before the run and proven gone (grep -c MUTANT= 0) after.LtcEncoder:Math.Round(v, AwayFromZero)→Math.Round(v)LtcFrame.ParityBitIndex→ hardcoded27MtcFrame: drop the(byte)cast on the piece-7 shiftMtcFrame: piece-7 hour bit>> 4→>> 5LTCFrame: revert #853 (parity back to 27 for all rates)test_frame_at25fps_writesCorrectionAtBit59_notBit27and the newgolden/ltc-wire-v1.jsonguardOn mutant C. The spec listed it as a must-fail; it survives, and the honest reason is that it is equivalent, not that the vectors are weak. The mutated expression's value is
(rateBits << 1) | ((hours >> 4) & 1), which over the full reachable domain (4 rate codes × hours 0–23) ranges 0–7; the enclosingreturn (byte)((index << 4) | payload)cast then absorbs theintpromotion, so no input diverges. Verified exhaustively over that domain rather than argued. Mutant C2 mutates the same line behaviourally and is killed 13 times over, so the line is genuinely covered — the cast is defensive typing, not behaviour.On mutant D. As in #853,
test_frame_correctionBit_isSetExactlyWhenTheRestOfTheWordIsOdddoes not fail, because it derives its index from the same mutated function — it is a consistency test. The placement guarantee is carried by the named 25 fps test and, now, by the vectors.Out of Scope
LTCBiphaseEncoderis deliberately not in the contract. It has no production caller, andLTCEncoder.samplesinlines its own modulation against fractional slot boundaries that the primitive'ssamplesPerHalfBit: Intcannot express. Pinning it would pin dead code.LTCFrameStream,LTCScheduleandMTCSchedule— M2 slice 2.LTCDecoder,LTCAudioOutput,MTCPortSenderand everything else hardware-facing.OnlyCue.App.ViewModels+ the WinUI shell (M1e) — still blocked on a real Windows machine.OnlyCue verification (required — chore)
Spec link:
docs/superpowers/specs/2026-09-16-windows-m2-timecode-wire-formats.mdCloses: #854 (also updates parent Epic #728 task list)
CLAUDE.mdupdated if conventions changed — n/a;docs/architecture.mdupdated