Intro

vtc is built to have a simple, scriptable interface without sacrificing features or correctness.

Let’s walk through the assumptions vtc makes to offer a fast scripting API, and how to override them.

Timecode: A History

But first: what is timecode?

If you’re already familiar with timecode, it’s history, and it’s flavors, feel free to skip this section.

Back in the days of film, a running strip of numbers ran along the edge of the film stock to uniquely identify each frame, called keycode.

Keycode was essential to the film editing process. The raw negative of a film is irreplaceable: you loose quality each time you make a copy. Editing film is necessarily a destructive process, and often required multiple iterations. It would be just a tad nerve-wracking to take a pair of scissors and some glue to the one-of-a-kind film reels straight out of the camera on set, then running it over and over through a flatbed.

To avoid potential disaster, editors made their cut of the film using copies of the raw negative, called a work print, allowing the editor to work without fear of sinking a project from slicing, dicing, and wearing at the film.

When the edit was complete, it was necessary to know exactly where the edits had been made, so it could be recreated with the raw negative for finishing. A cut list would be written out, with the exact reels and keycodes for every cut, and would be used to make an exact duplicate of the editor’s work print with the mint condition raw negative.

In video and digital filmmaking, the same approach is used. Massive RAW files from a RED, ARRI, Sony, or other cinema camera are rendered down to more manageable files an Editor’s machine won’t choke on. Once the edit is complete, the raw files are re-assembled using a digital cutlist on a powerful machine for finishing out the film.

In film, we referenced keycode to know exactly what frame was being displayed on screen at any given time. In digital video, we reference the timecode of a given frame.

For a technical deep-dive into the many flavors of timecode, check out Frame.io’s excellent blogpost on the subject.

Supplied Framerates

vtc contains a number of pre-supplied framerates though the RATE namespace:

>>> import vtc
>>> vtc.RATE.F23_98
[23.98 NTSC]

Manual Framerate Creation

In order to make scripting more ergonomic, vtc has sane defaults for parsing Framerate values.

By default, a whole-frame framerate is left as-is

>>> import vtc
>>> vtc.Framerate(24)
[24]

For floats too:

>>> vtc.Framerate(24.0)
[24]

It can be forced to an NTSC timebase by explicitly setting ntsc=True.

>>> vtc.Framerate(24, ntsc=True)
[23.98 NTSC]

Non-whole-frame rates are interpreted as being NTSC by default:

>>> vtc.Framerate(23.98)
[23.98 NTSC]

Warning

NTSC Auto-detection

By default, non-whole framerates are rounded, multiplied by 1000, then put over a denominator of 1001 to make them proper NTSC framerates, regardless of their value.

23.98 will be converted to 24000/1001, but so will 23.5.

We can disable interpretation of non-whole framerates as NTSC by setting ntsc=False in order to define custom rates.

>>> import fractions
>>> vtc.Framerate(fractions.Fraction(3, 2), ntsc=False)
[1.5]

Note

Non-NTSC Restrictions

Using a float with ntsc=False will result in a ValueError. Floats are not precise, and without the ntsc flag, vtc cannot know exactly what framerate you want. A decimal.Decimal or fractions.Fraction value must be used.

Drop-frame must be set specifically:

>>> vtc.Framerate(29.97, dropframe=True)
[29.97 NTSC DF]

Note

Dropframe Restrictions

Dropframe timecode may only be used on framerates that are a multiple of 30000/1001 (29.97). The dropframe algorithm cannot be applied to any other timebase, as described in this article

Timecode Representations

Timecode represents a specific frame of a video, and can be represented a number of different ways. This section will give a brief overview of the representation attributes vtc.Timecode exposes, what they represent, and in what context you would expect to see them.

Timecode

property: vtc.Timecode.timecode()

what it is: timecode is used as a human-readable way to represent the id of a given frame. It is formatted to give a rough sense of where to find a frame: [HOURS]:[MINUTES]:[SECONDS]:[FRAME]. For more on timecode, see Frame.io’s excellent post on the subject.

where you see it: Timecode is ubiquitous in video editing, a small sample of places you might see timecode:

  • Source and Playback monitors in your favorite NLE.

  • Burned into the footage for dailies.

  • Cut lists like an EDL.

Frame Number

property: vtc.Timecode.frames()

what it is: frame number is the number of a frame if the timecode started at 00:00:00:00 and had been running until the current value. A timecode of ‘00:00:00:10’ has a frame number of 10. A timecode of ‘01:00:00:00’ has a frame number of 86400.

where you see it:

  • Frame-sequence files: ‘my_vfx_shot.0086400.exr’

  • FCP7XML cut lists:

    <timecode>
        <rate>
            <timebase>24</timebase>
            <ntsc>TRUE</ntsc>
        </rate>
        <string>01:00:00:00</string>
        <frame>86400</frame>  <!-- <====THIS LINE-->
        <displayformat>NDF</displayformat>
    </timecode>
    

Seconds

property: vtc.Timecode.frames()

what it is: the number of real-world seconds that have elapsed between 00:00:00:00 and the timecode value. With NTSC timecode, the timecode drifts from the real-world elapsed time.

where you see it: Anywhere real-world time needs to be calculated.

Runtime

property: vtc.Timecode.runtime()

what it is: the formatted version of seconds. It looks like timecode, but with a decimal seconds value instead of a frame number place.

where you see it: Anywhere real-world time is used.

  • FFMPEG commands:

    ffmpeg -ss 00:00:30.5 -i input.mov -t 00:00:10.25 output.mp4
    

Rational Time

property: vtc.Timecode.rational()

what it is: the number of real-world seconds that have elapsed between 00:00:00:00 and the timecode value, expressed as a fraction.

where you see it: In code that needs to do lossless calculations of playback time elapsed, but cannot rely on frame count, like adding two timecodes together with different framerates.

Adobe Premiere Pro Ticks

property: vtc.Timecode.premiere_ticks()

what it is: internally, Adobe Premiere Pro uses ticks to divide up a second, and keep track of how far into that second we are. There are 254016000000 ticks in a second, regardless of framerate in Premiere.

where you see it:

  • Premiere Pro Panel functions and scripts

  • FCP7XML cutlists generated from Premiere:

    <clipitem id="clipitem-1">
        ...
        <in>158</in>
        <out>1102</out>
        <pproTicksIn>1673944272000</pproTicksIn>
        <pproTicksOut>11675231568000</pproTicksOut>
        ...
    </clipitem>
    

Feet And Frames

what it is: On physical film, each foot contains a certain number of frames. For 35mm, 4-perf film (the most common type on Hollywood movies), this number is 16 frames per foot. Feet-And-Frames was often used in place of Keycode to quickly reference a frame in the edit.

where you see it: For the most part, feet + frames has died out as a reference, because digital media is not measured in feet. The most common place it is still used is Studio Sound Departments. Many Sound Mixers and Designers intuitively think in feet + frames, and it is often burned into the reference picture for them.

  • Sound turnover reference picture.

  • Sound turnover change lists.

Timecode Value Inferences

When creating a timecode or using operators, vtc.Timecode interprets other values based on their type. This allows fast scripting by being able to use shorthand for things like adding frames or seconds to a timecode without instantiating an entire new Timecode instance, like so:

>>> tc = vtc.Timecode("01:00:00:00", rate=vtc.RATE.F23_98)
>>> tc + "01:00:00:00"
[02:00:00:00 @ [23.98 NTSC]]
>>> tc + 12
[01:00:00:12 @ [23.98 NTSC]]

In the first addition example, the timecode string is interpreted as a timecode. In the second, our int value is interpreted as a frame count.

The below table details how types are interpreted by vtc when both instantiating new Timecode instances and doing operations.

Python Type

Interpreted As

string (‘HH:MM:SS:FF’)

Timecode

string (‘HH:MM:SS.FF’)

Runtime

string (‘FEET+FRAMES’)

Feet+Frames

int

Frame Number

fractions.Fraction

Seconds

decimal.Decimal

Seconds

float

Seconds

vtc.PremiereTicks

Adobe Premiere Pro Ticks

vtc.Timecode

value.rational as seconds

Timecode Arithmetic

The Timecode type supports the following operations:

  • Addition

  • Subtraction

  • Multiplication

  • Division

  • Floor Division

  • Divmod

  • Modulo

  • Absolute Value

  • Negation

When two Timecode values are involved in an operation together, the framerate from the one on the left side is used. The real-world times of the timecodes are added, then rounded to the nearest frame given the left-hand framerate.

>>> vtc.Timecode("01:00:00:00", rate=vtc.RATE.F24) + vtc.Timecode("01:00:00:00", rate=vtc.RATE.F23_98)
[02:00:03:14 @ [24]]

We might expect a result of 02:00:00:00 here, but because the real-time playback of “01:00:00:00” at 23.98 NTSC is ~01:00:03:14, we get the result above.

Timecode Rounding

Timecode is always rounded to the nearest complete frame when instantiating a new Timecode value. Partial frames are not accepted as this could result in accumulated drift from imprecise calculations.

>>> vtc.Timecode(fractions.Fraction(235, 240), rate=vtc.RATE.F24).rational
Fraction(1, 1)

This applies to adding two timecodes together as well:

>>> tc1 = vtc.Timecode(1, rate=vtc.RATE.F24)
>>> tc2 = vtc.Timecode(1, rate=vtc.RATE.F30)
>>>
>>> tc1.rational
Fraction(1, 24)
>>>
>>> tc2.rational
Fraction(1, 30)
>>>
>>> tc3 = tc1 + tc2
>>> tc3.rational
Fraction(1, 12)

We assume that an NLE is not going to allow cuts to fall on fractional frames, and adjust accordingly.

Timecode Comparison

Timecode comparison is done based on real-world elapsed time, not frame count:

>>> tc1 = vtc.Timecode("01:00:00:00", rate=vtc.RATE.F23_98)
>>> tc2 = vtc.Timecode("01:00:00:00", rate=vtc.RATE.F24)
>>>
>>> tc1 == tc2
False
>>> tc1 > tc2
True

This has implications for sorting:

>>> sorted([tc1, tc2])
[[01:00:00:00 @ [24]], [01:00:00:00 @ [23.98 NTSC]]]

Ranges

vtc offers the Range class for working with timecode ranges:

>>> tc1 = vtc.Timecode("01:00:00:00", rate=vtc.RATE.F23_98)
>>> tc2 = vtc.Timecode("02:00:00:00", rate=vtc.RATE.F23_98)
>>> tc_range = vtc.Range(tc1, tc2)
>>> tc_range
[01:00:00:00 - 02:00:00:00 @ [23.98 NTSC]]
>>> tc_range.tc_in
[01:00:00:00 @ [23.98 NTSC]]
>>> tc_range.tc_out
[02:00:00:00 @ [23.98 NTSC]]

Note

Framerates

A timecode Range can only be made between two timecodes with the same framerate. A ValueError will be raised if tc1 and tc2 ave differing framerates.

Note

Valid Ranges

A ranges in point must come before the outpoint. If the first tc passed to the range comes after the second tc, the values will be flipped into the correct order.

Get the length, in frames, of the range:

>>> len(tc_range)
86400

To check if some timecode falls within a range:

>>> "01:30:00:00" in tc_range
True

We can check if one range intersects another, and get that intersection:

>>> tc3 = vtc.Timecode("01:30:00:00", rate=vtc.RATE.F23_98)
>>> tc4 = vtc.Timecode("02:30:00:00", rate=vtc.RATE.F23_98)
>>> range_2 = vtc.Range(tc3, tc4)
>>> tc_range.intersection(range_2)
[01:30:00:00 - 02:00:00:00 @ [23.98 NTSC]]

We can also get the separation of two non-intersecting ranges:

>>> tc5 = vtc.Timecode("02:30:00:00", rate=vtc.RATE.F23_98)
>>> tc6 = vtc.Timecode("03:30:00:00", rate=vtc.RATE.F23_98)
>>> range_3 = vtc.Range(tc5, tc6)
>>> tc_range.separation(range_3)
[02:00:00:00 - 02:30:00:00 @ [23.98 NTSC]]

Note

Inclusive vs Exclusive Timecode Ranges

Ranges are exclusive, which means that the timecode at the end of the range does not actually fall within it.

If you are used to working in Final Cut or Premiere, this will seem familiar, the out point of a clip, segment, etc does not include the out point. In these programs the out point represents the timecode that the cut itself falls on, with the frame before being the final frame of the clip.

If you are coming from Avid, this may strike you as odd. In avid, the out point of a clip is the final actual frame of the clip. This is called an inclusive range, and is not what VTC does.

So why choose exclusive instead of inclusive? Two main reasons:

One is author preference. The author off this library primarily uses Premiere, and works primarily with FCP7XML’s, so therefore is primarily working with exclusive frame rates.

The other is mathematical. With an exclusive range, to get the number of frames it contains, you simply do = duration. With an inclusive range, you have to do tc_out - tc_in + 1 = duration, which is less intuitive and more prone to error.

Keep this in mind as you work. Some application, like AVID and Redline, use an inclusive frame range convention, and adjustments will need to be made accordingly.