Skip to content

chore(windows): pin the LTC and MTC wire formats as golden vectors (#728 M2 slice 1) - #857

Merged
one-soft-engineer merged 4 commits into
devfrom
issues/854
Sep 16, 2026
Merged

one-soft-engineer merged 4 commits into
devfrom
issues/854

Conversation

@one-soft-engineer

Copy link
Copy Markdown
Collaborator

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. 16 frame words (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 10 encode PCM cases across (rate, sampleRate, amplitude, startLevel).
  • golden/mtc-wire-v1.json — 44 cases. 4 rate codes, 16 quarter-frame sequences (whole F1 <data> messages × 8, so the status byte and piece ordering are pinned too), 16 Full Frame SysEx messages, and 8 clamp cases (piece = -1 and 8 at every rate).
  • OnlyCueTests/{LTC,MTC}WireGolden.swift + …GoldenVectorTests.swift — the generators and drift guards, modelled on TimecodeGoldenVectorTests.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 k spans [round(k·R) … round((k+1)·R)) with R = sampleRate / (160·fps). At 24 fps / 48 kHz R is exactly 12.5, so every odd slot boundary is a midpoint tie. Swift's .rounded() is half-away-from-zero; .NET's default Math.Round(double) is banker's rounding and lands on the even neighbour. LtcEncoder therefore rounds with an explicit MidpointRounding.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, and test_runLengthEncoding_roundTripsTheSamples proves 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.
  • Red-first: the two drift guards were committed failing (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.

# Mutant Result
A LtcEncoder: Math.Round(v, AwayFromZero) → Math.Round(v) Killed — 6 encode cases fail (24@48000 ×3, 24/25/30@44100)
B LtcFrame.ParityBitIndex → hardcoded 27 Killed — all five 25 fps frame cases + the independent hand pin
C MtcFrame: drop the (byte) cast on the piece-7 shift Survived — equivalent mutant (see below)
C2 MtcFrame: piece-7 hour bit >> 4 → >> 5 Killed — 13 failures incl. the hand pin
D Swift LTCFrame: revert #853 (parity back to 27 for all rates) Killed — test_frame_at25fps_writesCorrectionAtBit59_notBit27 and the new golden/ltc-wire-v1.json guard

On 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 enclosing return (byte)((index << 4) | payload) cast then absorbs the int promotion, 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_isSetExactlyWhenTheRestOfTheWordIsOdd does 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

  • LTCBiphaseEncoder is deliberately not in the contract. It has no production caller, and LTCEncoder.samples inlines its own modulation against fractional slot boundaries that the primitive's samplesPerHalfBit: Int cannot express. Pinning it would pin dead code.
  • LTCFrameStream, LTCSchedule and MTCSchedule — M2 slice 2.
  • LTCDecoder, LTCAudioOutput, MTCPortSender and 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.md
Closes: #854 (also updates parent Epic #728 task list)

  • Tooling verified locally (commands run, artifacts produced, no errors reported)
  • No behavioral surface in this PR — macOS behaviour is unchanged; the Swift side gains only tests, and the C# side is new code with no caller yet
  • Spec / CLAUDE.md updated if conventions changed — n/a; docs/architecture.md updated
  • CI green

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.
@one-soft-engineer
one-soft-engineer merged commit 4c085ed into dev Sep 16, 2026
2 checks passed
@one-soft-engineer
one-soft-engineer deleted the issues/854 branch September 16, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(windows): pin the LTC and MTC wire formats as golden vectors (#728 M2 slice 1)

2 participants