Skip to content

Make the source's presentation window first-class: elst-aware reads, sync-frame trims, matching text-track edit list - #20

Open
cclements wants to merge 9 commits into
Mbucari:masterfrom
cclements:feat/presentation-window
Open

Make the source's presentation window first-class: elst-aware reads, sync-frame trims, matching text-track edit list#20
cclements wants to merge 9 commits into
Mbucari:masterfrom
cclements:feat/presentation-window

Conversation

@cclements

Copy link
Copy Markdown
Contributor

This is the follow-up we discussed in #19, covering both cases you raised plus the
text-track caveat.

What changes

1. Inputs with an edit list are read in presentation time (your case 2).
ElstBox exposes the single-edit form we write (SingleEdit), and Mpeg4File gains
PresentationStartSample / PresentedDurationSamples / PresentedDuration. The chunk
readers now map their start/end bounds into each track's own media timeline via that
track's elst (AddTrack does the mapping per-track, since an audio track with preroll
and a text track present different media). Re-converting a split part no longer reads
the preroll as content, and ConvertToMultiMp4aAsync maps chapter offsets the same way,
so re-splitting a part is also correct.

2. Trimmed lossless output starts at a sync frame with an exact window (your case 1 —
the branding-strip path). The preroll queue from #19 is hoisted into a shared
SyncPrerollQueue, and ConvertToMp4aAsync with user chapters now behaves exactly like
a split part: output opens at the most recent sync frame at or before the trim start,
and a single elst presents exactly the requested window. For USAC the reader over-dispatches
a bounded 2 s lookback (sync truth only exists post-decrypt — same encryption constraint
as #18) and the filter selects the sync run. An untrimmed conversion is byte-identical
to 3.1.0 output (verified, see below) — no elst, no behavior change.

3. The chapter text track stays consistent with the trimmed audio (your caveat).
One nuance found while implementing: chapter samples are written on the presentation
timeline (first chapter at presentation zero), so an edit list literally matching the
audio one (same media_time) would double-shift the markers. What was actually broken is
that SetDuration copied the audio media duration (preroll included) onto the text
track. The writer now gives the text track the presented durations and an identity edit
(media_time 0, same segment_duration), so both tracks present the same window. Verified
against Apple's own chapter API (details below) — happy to switch to a different mapping
if you had something else in mind.

Out of scope, as discussed: the decoder-side policy (decode from sync, discard PCM before
the window) belongs to AAXClean.Codecs — I'd like to do that as the companion PR next.
The public surface it needs (presentation properties, per-track dispatch mapping) is in
this PR.

Compatibility notes: source-compatible, but AAXClean.Codecs needs a rebuild against
this version — IChunkReader.AddTrack gained an optional parameter and the
MultipartFilterBase constructor gained an optional mediaTimeOffset (defaulted, so
existing subclasses compile unchanged; the Codecs multipart filters should pass their
input's offset in the companion PR). Until the companion lands, Codecs conversions of
elst-bearing inputs behave as before for USAC (the 2 s lookback re-covers the preroll
the bounds mapping would have skipped) and slightly better for AAC-LC (the trailing
out-of-window tail is no longer read). Mp4File.Duration deliberately stays
media-based to avoid a blast radius through Libation; PresentedDuration is the new
elst-aware property — happy to flip the default if you'd rather.

Verification (real Audible media, Apple decoder)

  • No-regression: full untrimmed remux of a USAC title (10h53m) and an AAC-LC title
    (13h08m) against v3.1.0 builds — outputs byte-identical except the 4 bytes of
    mdhd creation/modification timestamps (UtcNow, differs every run).
  • Split round-trip (case 2): split a USAC title into 3 parts, re-remuxed part 2:
    identical sample count, identical stss, identical elst (segDur=35077051,
    mediaTime=4738), identical audio payload MD5, Apple seek probes 5/5 incl. t=0.
    Before this PR the round-trip dropped the window and the preroll became audible.
  • Trim, USAC (case 1): ConvertToMp4aAsync with a 2.5s–3600s chapter window:
    output opens on a sync run (elst mediaTime=28330), AVAsset duration exactly 3597.5 s,
    seek probes 5/5, and decoded PCM at trim+10 s is bit-identical (max abs diff 0.0)
    to source+12.5 s — the window offset is sample-exact.
  • Trim, AAC-LC control: stss correctly absent (all-sync), elst mediaTime=853 — exactly
    round(2.5s·22050) mod 1024, the mid-frame remainder — probes 5/5. One measured,
    honest caveat: cross-correlation shows Apple's AudioFile layer ignores the elst for
    AAC-LC (constant −853-sample lag, i.e. 38.7 ms early — same layer behavior we measured
    in Write sync sample tables: derive stss for USAC (ISO/IEC 23003-3 §H.1), propagate source sync info — fixes seeking in Apple players #18, where even media_time+2112 changed nothing), while the AVAsset layer (Books,
    QuickTime) honors it exactly (asset duration 3597.500). Pre-PR LC trims started on the
    same frame with no declared window, so this is strictly better or equal at every
    layer; container-side we can't do more than declare the window. USAC has no such
    caveat: cross-correlation lag 0, PCM bit-identical.
  • Re-split of an elst-bearing part (split part 2 into two sub-parts): presented
    durations sum to the parent's exactly (17538525 + 17538526 = 35077051), seek probes
    pass, and PCM is bit-identical to the parent on both sides of the sub-boundary.
    Testing this caught a real bug in an earlier revision of this branch: with early
    dispatch, the multipart filter opened part 1 at the first dispatched frame and then
    dropped the pre-window frames after it, leaving a hole in the part's bitstream — the
    fix defers each part's open until the first frame overlapping its chapter window
    (the same policy the single-file path uses), and the fresh-source split battery was
    re-run afterward: outputs hash-identical to before the fix.
  • Text track: on the trimmed output, AVAsset.loadChapterMetadataGroups reports the
    chapter at start 0.0 with duration 3597.5 — markers land on the presented timeline;
    tkhd/mdhd/mvhd durations all consistent (structural check in the thread if you want it).
  • Unit tests: 13/13 (existing stss/elst suites + new SyncPrerollQueue tests; the
    MultipartFilterBase refactor is behavior-preserving and the split battery above ran
    through it).

🤖 Generated with Claude Code

cclements and others added 9 commits July 31, 2026 20:52
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l sync lookback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… window

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With lookback dispatch (or an edit-list input whose window starts mid-media)
the first dispatched frame precedes the first chapter's window. Opening the
part there wrote the sync frame and then dropped the pre-window frames after
it, leaving a hole in the part's bitstream. Deferring the open to the first
frame overlapping the window makes the part's media the contiguous run from
the preroll's sync frame — the same policy the single-file path uses.

Also round PresentedDurationSamples instead of truncating, for sources whose
mvhd and mdhd timescales differ.

Co-Authored-By: Claude Fable 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