English | Espanol
SMPTE timecode arithmetic for video editors, with drop frame handled the way SMPTE ST 12-1 actually defines it.
Most timecode implementations get drop frame slightly wrong. The usual mistake
is to believe that drop frame throws away picture, or to accept 00:01:00;00
as a real position when that label does not exist. This tool treats the label
and the frame as two different things and keeps the arithmetic in exact
integers, so the numbers it prints match the numbers a deck or an NLE prints.
timecode add 01:00:00:00 00:02:30:15 --fps 29.97df
timecode sub 01:00:00:00 00:02:30:15 --fps 29.97df
timecode conv 01:00:00;00 --from 29.97df --to 23.976
timecode frames 01:00:00:00 --fps 24
timecode at 86400 --fps 24
timecode range 01:00:00:00 01:00:10:00 --fps 25
timecode drift 01:00:00;00 --fps 29.97df
timecode validate 01:01:00;01 --fps 29.97df
Supported rates: 23.976, 24, 25, 29.97, 29.97df, 30, 50,
59.94, 59.94df, 60.
Every subcommand takes --json and prints one JSON object instead of the
aligned block. --version prints the version.
A semicolon in a shell argument needs quoting, so write '01:00:00;00' or use
the colon form, which is accepted at every rate.
- Drop frame drops the label, not the frame. No picture is discarded. Two frame numbers are skipped at the start of every minute except minutes divisible by ten. 29.97df skips two, 59.94df skips four.
- One hour of 29.97df is 107892 frames, not 108000. One hour of 59.94df is 215784 frames.
00:01:00;02is frame 1800 and00:10:00;00is frame 17982.- Non-drop 29.97 is legal. It runs 3.6 seconds per hour behind wall clock time and the tool says so rather than pretending the problem does not exist.
- A semicolon separates the frames field for drop frame and a colon for non-drop. Both are accepted on input and the correct one comes back out.
- Negative results print with a leading minus sign. Sums past 24 hours wrap and report how many days came off.
When NTSC added colour in 1953 the new colour subcarrier had to share a
channel with the existing luminance and sound carriers without producing a
visible beat pattern on monochrome sets. The engineers solved it by slowing
the whole system by a factor of 1000/1001, which moved 30.00 frames per second
to 29.97 and put the subcarrier into an interleaved relationship with the
sound carrier that neither the eye nor the ear could pick out. Timecode, which
was standardised later, kept counting 30 labels every second even though only
29.97 frames were being recorded, so an hour of timecode took 3603.6 seconds
of real time and a one hour programme overran the clock by 3.6 seconds.
Drop frame timecode corrects the label count rather than the picture by
skipping the labels ;00 and ;01 at the start of every minute except every
tenth minute, which removes 108 labels per hour and leaves a residual error of
3.6 milliseconds per hour.
go install github.com/keivanmalhani/timecode/cmd/timecode@latest
The package can also be used directly:
import "github.com/keivanmalhani/timecode"
There are no dependencies outside the standard library.
Everything below is pasted from a real run.
$ timecode --version
timecode 0.1.0
$ timecode add 01:00:00:00 00:02:30:15 --fps 29.97df
result 01:02:30;15
frames 112403
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
a 01:00:00;00, frame 107892
b 00:02:30;15, frame 4511
real 3750.513433 s, 01:02:30.513433
Rolling over midnight:
$ timecode add 23:59:59;29 00:00:00;01 --fps 29.97df
result 00:00:00;00
frames 2589408
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
a 23:59:59;29, frame 2589407
b 00:00:00;01, frame 1
real 86399.913600 s, 23:59:59.913600
rollover the sum ran past 24:00:00:00 to 24:00:00;00, so 1 whole day came off
$ timecode sub 01:00:00;00 00:02:30;15 --fps 29.97df
result 00:57:29;15
frames 103381
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
a 01:00:00;00, frame 107892
b 00:02:30;15, frame 4511
real 3449.479366 s, 00:57:29.479366
Going negative:
$ timecode sub 00:00:01:00 00:00:02:00 --fps 25
result -00:00:01:00
frames -25
rate 25, 25 frames per second exactly
a 00:00:01:00, frame 25
b 00:00:02:00, frame 50
real -1.000000 s, -00:00:01.000000
note the result is before 00:00:00:00 and is printed with a minus sign;
the same position read inside one day is 23:59:59:00
$ timecode conv 01:00:00;00 --from 29.97df --to 23.976
source rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
source 01:00:00;00
source frames 107892
source real 3599.996400 s, 00:59:59.996400
target rate 23.976, 24000/1001 = 23.976024 frames per second, non drop
target 00:59:56:10
target frames 86314
target real 3600.013083 s, 01:00:00.013083
difference 0.016683 s
note the frame count was rounded to the nearest whole frame at
23.976, so the target lands 0.016683 s from the source instant
The same instant relabelled between drop and non-drop at the same real rate,
which is why an hour of non-drop 29.97 shows up as 01:00:03;18 in drop
frame:
$ timecode conv 01:00:00:00 --from 29.97 --to 29.97df
source rate 29.97, 30000/1001 = 29.970030 frames per second, non drop
source 01:00:00:00
source frames 108000
source real 3603.600000 s, 01:00:03.600000
target rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
target 01:00:03;18
target frames 108000
target real 3603.600000 s, 01:00:03.600000
difference 0.000000 s
note the two rates land on the same instant, so nothing was rounded
$ timecode frames 01:00:00:00 --fps 24
frames 86400
timecode 01:00:00:00
rate 24, 24 frames per second exactly
labels 86400 counted at 24 per second
real 3600.000000 s, 01:00:00.000000
$ timecode frames 00:01:00;02 --fps 29.97df
frames 1800
timecode 00:01:00;02
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
labels 1802 counted at 30 per second
dropped 2 labels skipped before this point
real 60.060000 s, 00:01:00.060000
$ timecode at 86400 --fps 24
timecode 01:00:00:00
frames 86400
rate 24, 24 frames per second exactly
labels 86400 counted at 24 per second
real 3600.000000 s, 01:00:00.000000
$ timecode at 107892 --fps 29.97df
timecode 01:00:00;00
frames 107892
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
labels 108000 counted at 30 per second
dropped 108 labels skipped before this point
real 3599.996400 s, 00:59:59.996400
$ timecode range 01:00:00:00 01:00:10:00 --fps 25
frames 250
inclusive 251 frames when both ends are kept
seconds 10.000000
duration 00:00:10:00
real 00:00:10.000000
rate 25, 25 frames per second exactly
from 01:00:00:00, frame 90000
to 01:00:10:00, frame 90250
$ timecode drift 01:00:00;00 --fps 29.97df
timecode 01:00:00;00
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
labels ((1*60 + 0)*60 + 0)*30 + 0 = 108000 labels counted at 30 per second
dropped 2 labels * (60 minutes - 6 minutes divisible by ten) = 108 labels never used
frames 108000 - 108 = 107892 frames
label time 108000 / 30 = 3600.000000 s
real time 107892 * 1001 / 30000 = 3599.996400 s
drift 3600.000000 - 3599.996400 = 0.003600 s
meaning at this point the timecode label is 0.003600 s ahead of the
wall clock
per hour 0.003600 s of drift for every hour of labels
per day 0.086400 s of drift for every day of labels
why NTSC colour television slowed the whole number rate by a
factor of 1000/1001 so that the colour subcarrier would not
beat visibly against the sound carrier, which turned 30 frames
per second into 30000/1001 frames per second. Timecode kept
counting 30 labels every second even though fewer frames were
being recorded, so an hour of labels took 3603.6 seconds of
real time. Drop frame corrects the label count rather than the
picture, skipping two labels at the start of every minute
except every tenth minute and removing 108 labels from every
hour. That correction overshoots by a small amount, which is
why an hour of 29.97df is 3.6 milliseconds short of an hour on
the clock.
The same point at non-drop 29.97, which is legal and drifts 3.6 seconds per hour:
$ timecode drift 01:00:00:00 --fps 29.97
timecode 01:00:00:00
rate 29.97, 30000/1001 = 29.970030 frames per second, non drop
labels ((1*60 + 0)*60 + 0)*30 + 0 = 108000 labels counted at 30 per second
dropped none, this rate does not use drop frame labelling
frames 108000 labels
label time 108000 / 30 = 3600.000000 s
real time 108000 * 1001 / 30000 = 3603.600000 s
drift 3600.000000 - 3603.600000 = -3.600000 s
meaning at this point the timecode label is 3.600000 s behind the wall
clock
per hour -3.600000 s of drift for every hour of labels
per day -86.400000 s of drift for every day of labels
why NTSC colour television slowed the whole number rate by a
factor of 1000/1001 so that the colour subcarrier would not
beat visibly against the sound carrier, which turned 30 frames
per second into 30000/1001 frames per second. Timecode still
counts 30 labels every second, so the label count falls behind
the clock by 3.6 seconds for every hour of labels. Non drop
29.97 is legal and is the usual choice for work that never has
to match a broadcast clock, because the label count then
matches the frame count exactly. Use 29.97df when the running
time on screen has to agree with the clock.
At an exact rate there is nothing to explain:
$ timecode drift 01:00:00:00 --fps 25
timecode 01:00:00:00
rate 25, 25 frames per second exactly
labels ((1*60 + 0)*60 + 0)*25 + 0 = 90000 labels counted at 25 per second
dropped none, this rate does not use drop frame labelling
frames 90000 labels
label time 90000 / 25 = 3600.000000 s
real time 90000 * 1 / 25 = 3600.000000 s
drift 3600.000000 - 3600.000000 = 0.000000 s
meaning at this point the timecode label and the wall clock agree
exactly
per hour 0.000000 s of drift for every hour of labels
per day 0.000000 s of drift for every day of labels
why 25 is a whole number of frames per second, so one second of
labels is one second on the clock and the two never separate.
There is no drift to correct and no drop frame form of this
rate exists.
validate exits 1 when the label does not exist:
$ timecode validate 01:01:00;01 --fps 29.97df
valid no
input 01:01:00;01
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
reason frames 00 and 01 do not exist at 01:01:00 because drop frame
skips two labels at the start of every minute except every
tenth minute; the first legal frame here is 02
$ echo $?
1
Drop frame only skips labels at second 00, and only in minutes that are not
divisible by ten. 01:00:01;01 is one second and one frame into minute 60,
which is a tenth minute anyway, so it is a real position and is accepted:
$ timecode validate 01:00:01;01 --fps 29.97df
valid yes
input 01:00:01;01
timecode 01:00:01;01
frames 107923
rate 29.97df, 30000/1001 = 29.970030 frames per second, drop frame, 2 labels skipped per dropping minute
Field range errors are reported the same way:
$ timecode validate 01:00:00:60 --fps 25
valid no
input 01:00:00:60
rate 25, 25 frames per second exactly
reason frames must be 00 to 24 because 25 counts 25 labels per second
$ timecode add 01:00:00;00 00:02:30;15 --fps 29.97df --json
{
"command": "add",
"rate": "29.97df",
"a": {
"timecode": "01:00:00;00",
"frames": 107892
},
"b": {
"timecode": "00:02:30;15",
"frames": 4511
},
"result": "01:02:30;15",
"frames": 112403,
"negative": false,
"rollover_days": 0,
"unwrapped": "01:02:30;15",
"real_seconds": 3750.513433,
"real_duration": "01:02:30.513433"
}
$ timecode range 01:00:00;00 01:00:10;00 --fps 29.97df --json
{
"command": "range",
"rate": "29.97df",
"from": {
"timecode": "01:00:00;00",
"frames": 107892
},
"to": {
"timecode": "01:00:10;00",
"frames": 108192
},
"frames": 300,
"frames_inclusive": 301,
"seconds": 10.010000,
"duration": "00:00:10;00",
"real_duration": "00:00:10.010000"
}
| status | meaning |
|---|---|
| 0 | the command succeeded |
| 1 | validate was given a timecode that does not exist |
| 2 | bad usage, bad rate, or bad timecode |
Nothing here is held as a floating point number. Frame counts and label counts
are integers, and real elapsed time is carried as the fraction
frames * den / num until the last step, where it becomes nanoseconds. That
is why a 29.97df hour comes out as 3599.996400 seconds with no residue. The
drift figure is computed as a single fraction,
labelIndex/nominal - frames*den/num, for the same reason.
The hours field is limited to 00 to 23, which is what SMPTE defines. A
duration longer than a day is carried as a frame count rather than as a
timecode string.
- It does not read or write timecode tracks in media files. There is no container parsing here at all.
- It does not read or write EDL, AAF, XML or any other edit list format.
- It does not do pulldown. Frame level cadence patterns such as 3:2 are a different problem from label arithmetic.
- It does not know about user bits. It does not decode or generate LTC or VITC waveforms either.
- It does not do audio sample counts or sample rate conversion.
- It does not support arbitrary or user-defined frame rates. The ten rates listed above are the ones that exist in practice, and adding open-ended rates would mean guessing at drop frame rules that no standard defines.
- It does not convert by preserving the frame count when the rates differ.
convconverts by real elapsed time and rounds to the nearest frame, and it tells you how far the rounding moved the result. - It does not accept sloppy input. Every field has to be two digits with the hours between 00 and 23, and a drop frame label that SMPTE skips is refused rather than quietly corrected.
go build ./...
go vet ./...
gofmt -l .
go test ./... -count=1
The test suite is table driven and covers 315 cases, including a walk over every frame of a whole day at 29.97df and 59.94df. That walk prints each frame count as a timecode and reads it back, which also proves the formatter never emits a label that drop frame skips, because the parser refuses those.
MIT. See LICENSE.
