Skip to content

1.0.5: interlaced video follows the shared field-per-grain convention - #23

Merged
LeeO86 merged 2 commits into
mainfrom
fix/interlaced
Oct 16, 2026
Merged

LeeO86 merged 2 commits into
mainfrom
fix/interlaced

Conversation

@LeeO86

@LeeO86 LeeO86 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Fixes interlaced handling (audit 2026-10-16, sections A, B and D) per the shared convention (MXL 218ddaa): an interlaced flow holds one field per grain at twice the frame rate; grain 2k = the first field in time of TAI frame k, 2k + 1 = the second; tff first field = even lines, bff = odd lines; ANC at the field rate with the video field's index. Spec: new SPECIFICATION §3.2.2.

Capture (SDI → MXL)

  • Frame index k from the frame's start (one period before its timestamp) at the frame rate; grain 2k gets the first field's lines, 2k + 1 the other lines, by row (v210, and 8-bit YUV after the expansion; v210a: fill lines). Before: one grain per frame at the field-rate index holding frame lines 0–539, odd indexes invalid.
  • Field order from the mode table: NTSC is interlaced_bff (DeckLink bmdLowerFieldFirst) in the flow definition and NMOS; PAL and 1080i stay tff. Before: tff hard-coded.
  • ANC flow of an interlaced mode declared at 2× the frame rate; field 1's packets at 2k, field 2's at 2k + 1 (field from the SMPTE line number; an empty grain for a field without packets).

Playout (MXL → SDI)

  • VideoReader reads an interlaced flow as frames: grains 2k and 2k + 1 woven with line parity (idx & 1) ^ bff; its rate, head, ring size and indexes are frames, so the read position, preroll, read offset, catch-up, slips and the audio per frame (1920 samples at 1080i50, was 960) all use the frame rate. The sync-group wait waits for the second field's time. A frame with an invalid field repeats (source_invalid). A re-created flow that switches progressive ↔ interlaced restarts the channel.
  • The output does not read ANC, so there is no ANC reader to change.

Other

  • Web UI MXL tab maps interlaced flows to HD1080i50/HD1080i5994/PAL/NTSC (it looked for HD1080i25, which does not exist).
  • Mock: odd lines' counter band is grey in interlaced modes (field marker); MOCK_PLAYBACK_FRAME_FILE dumps played frames for lab checks.
  • Version 1.0.5 (CMakeLists, version.hpp, deploy manifest), CHANGELOG, README.

Tests

  • Unit: field split/weave tff + bff (every line back in place), frame index from the frame start and field timestamps (1080i50, 1080i59.94), frame head from field head, ANC line → field, field order table, flow written field by field and read back woven (PAL tff, NTSC bff, real MXL domain), output channel playing an interlaced flow (fake card): frame rate, woven frame exact, 1920-sample audio blocks. 116/116 pass locally.
  • Mock lab (HD1080i50, CH0 capture with ANC + CH1 output of that flow): grains of 540 lines, both parities valid, grain 2k = even lines, ANC flow 50/1 with the video's indexes (packet at 2k, empty at 2k+1); played frame woven exactly. Own tff 1080i25 writer → output: every frame row in place, 25 frames/s, no repeats. HD1080p50: unchanged (1080-line grains, ANC 50/1 one packet per grain, same behaviour as 1.0.4).

Round 2 (4786562)

  • RFC 8331 F bits: interlaced ANC grain 2k = 0b10, 2k+1 = 0b11; progressive 0b00.
  • NTSC ANC line 0 (unknown) counts as field 1.
  • NMOS ANC flow grain_rate and ANC receiver caps at the field rate for interlaced modes (video keeps the frame rate). Lab: 1080i50 data flow 50/1, video 25/1.
  • VideoReader warns mxl_flow_def_unreadable instead of silently reading as progressive; an invalid first field returns the frame invalid at once.
  • Docs: empty ANC grain only when the other field has packets; grain counters count 2 per interlaced frame.
  • Tests: F bits, line 0, invalid first field, InputChannel interlaced capture with a fake card.

Not changed / found

  • Outputs at 29.97/59.94 (progressive and interlaced) read audio at frame index × floor(48000/rate) samples, which is off the TAI sample grid (1601 vs 1601.6 per frame), so their audio is likely silent. Pre-existing, not part of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01St7QwmonpoG9EEEqaLFXLi

LeeO86 and others added 2 commits October 16, 2026 09:17
Capture writes frame k (from the frame's start at the frame rate) as two
field grains, 2k with the first field's lines and 2k + 1 with the other
lines (v210 and 8-bit YUV); before, one grain per frame at the field-rate
index held frame lines 0-539 and every odd index stayed invalid. NTSC is
interlaced_bff in the flow definition and NMOS (was tff for every mode).

ANC of an interlaced input runs at the field rate (2x the frame rate):
field 1's packets at grain 2k, field 2's at 2k + 1, the video field's
index. Before, packets were written at the field index into a frame-rate
data flow.

Playout reads an interlaced flow as frames: grains 2k and 2k + 1 woven
with line parity (idx & 1) ^ bff into the DeckLink frame; read position,
preroll, offsets, catch-up, the sync wait and the audio per frame use the
frame rate (1920 samples at 1080i50, was 960). Before, one field per
output frame went into the top half and the read advanced at the field
rate.

The web UI maps interlaced flows to HD1080i50/HD1080i5994/PAL/NTSC. The
mock marks the odd lines' counter band grey in interlaced modes and can
dump played frames (MOCK_PLAYBACK_FRAME_FILE). Progressive is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01St7QwmonpoG9EEEqaLFXLi
…rnings

- RFC 8331 F bits: interlaced ANC grain 2k gets 0b10 (field 1), 2k + 1
  gets 0b11 (field 2); progressive keeps 0b00 (as mxl-test-player 1.2.3).
- ANC line -> field: NTSC line 0 (unknown) is field 1, not field 2.
- NMOS ANC flow grain_rate and the ANC receiver caps follow the MXL flow
  definition: the field rate for interlaced modes; video keeps the frame
  rate.
- VideoReader logs mxl_flow_def_unreadable when the flow definition cannot
  be read or parsed (the flow is then read as progressive), and returns a
  frame with an invalid first field without waiting for the second.
- Docs: ANC empty grains only when the other field has packets; grain
  counters count two grains per interlaced frame.
- Tests: F bits, line 0, invalid first field, and an InputChannel capturing
  PAL from a fake card (field grains, counters, ANC per field with F bits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01St7QwmonpoG9EEEqaLFXLi
@LeeO86
LeeO86 merged commit 3668151 into main Oct 16, 2026
1 check passed
LeeO86 added a commit that referenced this pull request Oct 16, 2026
The output's audio read position was frame index x floor(48000 / rate)
samples: 1601 instead of 1601.6 at 29.97, 800 instead of 800.8 at 59.94.
At today's TAI frame indexes that is 7.8 resp. 20.7 days behind the sample
index the sources write, so these outputs played silence for good.

The position is now a frame number; each frame's block runs from the 48 kHz
sample index of the frame's TAI start to that of the next frame
(mxlTimestampToIndex(48000, mxlIndexToTimestamp(frame rate, k))), giving the
1602/1601 and 801/800 cadence. Scratch buffers are reserved for the largest
block, and the read-offset cap uses it. Whole rates (25, 50, 24, 30, 60,
23.98) give exactly the same sample indexes as before.

Rebased on #23: its interlaced output test now requires sound for NTSC
too, and the NMOS ANC source's grain_rate uses the field rate like its
flow (ancRateOf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01St7QwmonpoG9EEEqaLFXLi
LeeO86 added a commit that referenced this pull request Oct 16, 2026
…rid (#24).

The output's audio read position was frame index x floor(48000 / rate)
samples: 1601 instead of 1601.6 at 29.97, 800 instead of 800.8 at 59.94.
At today's TAI frame indexes that is 7.8 resp. 20.7 days behind the sample
index the sources write, so these outputs played silence for good.

The position is now a frame number; each frame's block runs from the 48 kHz
sample index of the frame's TAI start to that of the next frame
(mxlTimestampToIndex(48000, mxlIndexToTimestamp(frame rate, k))), giving the
1602/1601 and 801/800 cadence. Scratch buffers are reserved for the largest
block, and the read-offset cap uses it. Whole rates (25, 50, 24, 30, 60,
23.98) give exactly the same sample indexes as before.

Rebased on #23: its interlaced output test now requires sound for NTSC
too, and the NMOS ANC source's grain_rate uses the field rate like its
flow (ancRateOf).


Claude-Session: https://claude.ai/code/session_01St7QwmonpoG9EEEqaLFXLi

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant