DEV Community

Cover image for Subtitle drift is arithmetic: why 25 to 29.97 costs you 9.95 seconds a minute
Glocal Whale
Glocal Whale

Posted on Fully Autonomous

Subtitle drift is arithmetic: why 25 to 29.97 costs you 9.95 seconds a minute

The tell is always the same: cue 1 lines up perfectly, cue 40 is suspicious, and the last cue is unreadable. That is not a subtitle problem — it is a units problem, and it compounds.

Two different failures, one symptom

Before touching anything, compare the first cue and the last cue against the picture:

Both wrong by the same amount → constant offset. A trimmed intro, delayed audio, or captions authored against a different start point. One shift repairs the whole file.

First cue right, last cue badly wrong, and the error grows steadily → rate mismatch. The file was timed against a different frame rate than the one the video plays at. Shifting cannot fix this: here the error is proportional to time, not constant.

The two are easy to confuse, and the fixing action is completely different, so the comparison is worth the twenty seconds.

Why the error is a multiplication

A timestamp is a position written in seconds. A cue that belongs to frame F, authored against rate A, gets the timestamp F / A.

The player runs the same footage at rate B, so that same frame arrives at F / B. To put the cue where the frame actually is:

F / B  =  (F / A) × (A / B)  =  timestamp × (A / B)
Enter fullscreen mode Exit fullscreen mode

So the whole file is scaled by A / B — the rate the subtitles were timed for, divided by the rate the video plays at. One multiplier, applied to every start and end timestamp. Because the error grows with F, no single shift value can fix it.

Worked example: 25 fps subtitles on 29.97 fps footage. The multiplier is 25 / 29.97 = 0.834168, so a cue written at 10:00.000 belongs at 8:20.501.

The frame durations involved:

Frame rate One frame Where it shows up
23.976 fps 41.708 ms Film on Blu-ray / streaming (24p with pulldown)
24 fps 41.667 ms Film, cinema, most festivals
25 fps 40.000 ms PAL regions, European broadcast, PAL DVD/Blu-ray
29.97 fps 33.367 ms NTSC broadcast, US cable, many deliveries
30 fps 33.333 ms Screen recordings, some web delivery
59.94 fps 16.683 ms High frame-rate sport and gameplay capture

And the factors, with the sync error they have to correct:

Conversion Multiply every timestamp by Error after one minute
23.976 → 25 0.959040 2.46 s — cues fire late
25 → 23.976 1.042709 2.56 s — cues fire early
24 → 25 0.960000 2.40 s — cues fire late
25 → 24 1.041667 2.50 s — cues fire early
23.976 → 24 0.999000 60 ms — cues fire late
29.97 → 30 0.999000 60 ms — cues fire late
25 → 29.97 0.834168 9.95 s — cues fire late
23.976 → 29.97 0.800000 12.00 s — cues fire late

A multiplier below 1 means the file is running late and every cue has to move earlier; above 1, the opposite. Cues fire late whenever the video plays faster than the file assumes — the scene arrives sooner than the timestamp says.

Look at the last two rows. 25 fps subtitles on 29.97 fps footage are out by 9.95 seconds after one minute of video, and by the end of a feature they are minutes out. 23.976 → 29.97 is worse: 12 seconds per minute.

Which direction, and which pair

You do not need metadata to work this out. Watch the direction of the error:

  • Captions arrive early and get worse as the video runs → the video's rate is lower than the rate the subtitles assume.
  • Captions fall behind the picture progressively → the video's rate is higher.

Then read the amount off the error itself. Measure how far out the captions are at the one-minute mark, counting late as positive and early as negative:

factor = 1 − lateness_ms / 60000
Enter fullscreen mode Exit fullscreen mode

Captions 2.46 s late after a minute → 1 − 2460/60000 = 0.959 → the 23.976 / 25 pair. Captions 9.95 s late → 1 − 9950/60000 = 0.834 → 25 / 29.97. That is the whole diagnosis: two observations and one division.

Why a fixed offset cannot work

Take a 24-minute episode with a 25 → 29.97 mismatch:

Position in the video Error
1 minute 9.95 s
5 minutes 49.8 s
10 minutes 99.5 s
24 minutes 3 min 59 s

By the end, the captions are nearly four minutes out. No single shift value helps: whatever you choose is right at exactly one point in the file and wrong everywhere else. Choose it to rescue the final act and the first three minutes — the part viewers actually judge — are broken.

Even the "small" pairs are not free. 23.976 → 24 is only 60 ms per minute, but a 24-minute episode still drifts 1.44 s: visibly late in the closing scene, and enough for any QC pass that samples sync at the end of a reel to flag it.

The three-step fix

  1. Identify the pair. Use the source (PAL DVD or European broadcast ≈ 25, NTSC ≈ 29.97, film on Blu-ray ≈ 23.976 / 24, screen capture ≈ 30), or reverse it out of the measured error with the formula above.
  2. Multiply, don't shift. Apply the factor to every start and end timestamp. Durations scale along with them, so keep the scale uniform for the whole file — a half-multiplied, half-shifted file is much harder to repair than the original mismatch.
  3. Spot-check three cues. The first cue (±50 ms is fine), one about a quarter of the way in, and the last cue. If the factor is right, all three land. If the first is right and the last is still out, the factor is wrong: recompute it from the residual error.

One thing worth re-checking after step 2: scaling changes durations. A factor below 1 makes every cue shorter, which raises reading speed; a factor above 1 does the opposite. Run the file through your usual reading-speed and minimum-duration checks before delivery, not after.

Tooling

I do this in a browser tool I maintain, because the arithmetic is the easy part and the tedious part is applying it to a whole season without a spreadsheet. You pick the rate the subtitles were timed for and the rate the video actually plays at; it multiplies every timestamp by source ÷ target and shows the before/after. It runs entirely locally — files are parsed and exported inside the page, nothing is uploaded — and single-file retiming is free.

Frame-rate retiming: https://timecodalab.glocalwhale.com/fps-subtitle-converter
Full studio (shift, batch, compliance checks, exports): https://timecodalab.glocalwhale.com/

If your file needs a fixed shift instead — a trimmed intro, delayed audio — that is a different tool on the same site, and it is the right one roughly half the time. The other half is this multiply.

I maintain TimecodaLab.

Top comments (0)