Skip to content

TimecodeEngine reads the ambiguous family enum instead of the exact rate enum (+42), supports only 3 rates #1

Description

@katzzero

Context

We use pt_api as a reference (the detailed specs and add_volume_node confirmed the 0x260a layout word-for-word — thank you for that work). While cross-checking session frame rates we found what looks like two related problems in TimecodeEngine. Happy to contribute the fix (a PR will follow referencing this issue), and we accept corrections if any layout differs in sessions you have.

Bug 1 — wrong field: family enum instead of exact enum

The loader reads the frame-rate enum from the first byte of the 0x204d payload (frame_block.items[0][0]), which is the family enum (data-relative +2). That enum is ambiguous: family 0x01 covers both 24 fps and 48 fps; family 0x09 covers both 23.976 fps and 47.952 fps. So a 48 fps session currently decodes as 24 fps, and a 47.952 fps session as 23.976 fps.

The exact enum lives at data-relative +42 (u32 LE, payload offset 40) with 19 unambiguous values: 0x01=24, 0x02=25, 0x03=29.97ND, 0x05=29.97DF, 0x07=30, 0x08=30DF, 0x09=23.976, 0x0a=50, 0x0b=59.94, 0x0c=59.94DF, 0x0d=60, 0x0e=60DF, 0x0f=47.952, 0x10=48, 0x11=100, 0x12=119.88, 0x13=119.88DF, 0x14=120, 0x15=120DF (0x04/0x06 reserved). There is no 23.976 DF; DF rates use ; as separator. Validated against multiple sessions authored in Pro Tools.

Bug 2 — incomplete table + hardcoded drop-frame constants

get_frame_rate() maps only three values and raises ValueError("Unsupported frame rate") for everything else (25, 30, 50, 59.94, 60, 100, 119.88, 120 and DF variants). Additionally, samples_to_timecode() hardcodes the nominal-30 drop-frame constants (17982/1798), so nominal-60/120 DF rates are broken. They generalise to decade = 600*N - 18, minute = 60*N - 2 for nominal N.

Suggested fix (PR to follow)

  1. Loader: read frame_block.items[0][40] (exact enum at data +42), requiring payload length ≥ 44.
  2. Replace the 3-value lookup with the full 19-entry exact table.
  3. _nominal_fps(): derive from the resolved rate (ceil(fps - eps)).
  4. Generalise the forward/reverse drop-frame paths with the N-based constants above.

No public API signature change — TimecodeEngine(sample_rate, frame_rate_enum) stays as is; callers just pass exact enums.

How to verify

  • All 19 enums resolve to the correct fps / drop-frame / nominal.
  • Round-trip timecode_to_samples(samples_to_timecode(x)) at 24, 25, 29.97ND/DF, 30, 30DF, 23.976, 50, 59.94ND/DF, 60, 60DF, 47.952, 48, 100, 119.88, 119.88DF, 120, 120DF.
  • Drop-frame label checks at high nominals: 00:01:00:00/00:01:00:01 rejected, 00:10:00:00 accepted.
  • Existing tests/test_timecode.py keeps passing; tests/test_loading.py / tests/test_save_offsets.py fixtures need updating to 44-byte payloads (that update rides with the PR).

All code/layout notes we share are available under your project's license (MIT) — no strings attached.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions