Skip to content

Label AAC granules in the sample rate's timescale - #5

Merged
ustacode merged 1 commit into
mainfrom
feature/encoder-granule-timescale
Aug 2, 2026
Merged

Label AAC granules in the sample rate's timescale#5
ustacode merged 1 commit into
mainfrom
feature/encoder-granule-timescale

Conversation

@ustacode

@ustacode ustacode commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The bug

AudioEncoderRingBuffer.createOutputBuffer built each granule's timestamp offset like this:

let offsetTimeStamp = CMTime(
    value: CMTimeValue(workingIndex),          // a FRAME COUNT
    timescale: workingBufferPresentationTimeStamp.timescale   // the incoming PTS's scale
)

workingIndex counts frames, so the only timescale that converts it to a duration is the sample rate. Inheriting the incoming timestamp's scale is correct only when the producer stamps in sample time — which AVCaptureAudioDataOutput does, which is why this never surfaced.

A host-clock producer stamps at timescale 1e9. There, an offset of 1024 frames is 1.024 microseconds instead of 21.3 ms. Every granule carved from one appended buffer landed on essentially the same timestamp, then the timeline jumped by the whole buffer. MpegTsWriter drives the PCR from these labels and RtmpStream computes FLV deltas from them, so they reach the wire.

(CMTimeAdd does not rescue this: LCM(48000, 1e9) = 3e9 overflows Int32, so the sum keeps the host timescale.)

Also fixed

The final createOutputBuffer of an append re-arms latestPresentationTimeStamp before discovering there is nothing left to copy. That stale value then labelled the next granule with the previous buffer's timestamp. Reset on append — but only when outputIndex == 0, because a granule spanning two appends must keep the timestamp of the buffer that started it. That guard is why the third test exists.

Verification

Reverted the fix and re-ran: the new tests fail with exactly the predicted values.

✘ Each append labels its granule with its own timestamp
    (b.first?.seconds → 1.000001024) == 2.0
✘ Granules from one host-clock-stamped buffer are spaced by real time
    (abs(delta - step) → 0.021332309333332983) < 0.0001

40 tests in 10 suites pass with it. Scripts/check-containment.sh clean.

Upstream

This is vendored Moblin code and the bug is upstream's. Worth sending back — flagging rather than doing, since that is the repo owner's call.

🤖 Generated with Claude Code

The encoder's ring buffer built each granule's timestamp offset in the
INCOMING timestamp's timescale. `workingIndex` counts frames, so the only
timescale that turns it into a duration is the sample rate; inheriting
the timestamp's scale is correct only when a producer happens to stamp in
sample time. Capture audio does, which is why this never surfaced.

A host-clock producer does not. At timescale 1e9, an offset of 1024
frames comes out as 1.024 MICROseconds instead of 21.3 ms, so every
granule carved out of one appended buffer landed on essentially the same
timestamp and the timeline then jumped by the whole buffer. Downstream
uses those labels verbatim — MpegTsWriter drives the PCR from them and
RtmpStream computes FLV deltas from them.

Also closes a staleness: the final `createOutputBuffer` of an append
re-arms `latestPresentationTimeStamp` before discovering there is nothing
left to copy, so the next granule was labelled with the PREVIOUS buffer's
timestamp. Reset on append — but only when `outputIndex` is zero, since a
granule spanning two appends must keep the timestamp of the buffer that
started it.

Tests fail without the fix with exactly the predicted values: a granule
labelled 1.000001024 instead of 2.0, and granule spacing off by 21.33 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ustacode
ustacode merged commit a38eeff into main Aug 2, 2026
1 check passed
@ustacode
ustacode deleted the feature/encoder-granule-timescale branch August 2, 2026 20:42
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