View Source Vtc.Framestamp (vtc v0.17.5)

Identifies a particular frame in a media stream.

New Framestamp values are created by with_seconds/3 and with_frames/2.

Vtc's philosophy of working with Timecode is defined by two major conceits:

  1. A frame identifier is incomplete without a framerate. More here.

  2. All frame identifiers commonly used in Video production boil down to either the real-world seconds-since-midnight that frame occurred, OR a sequential index number. More here.

what-is-a-framestamp

What is a framestamp?

Framestamps are a way to identify a media stream frame without requiring any additional information. On a technical level, a framestamp is comprised of:

  • The real-world time that a frame occurred at, as represented by a rational value, measured in seconds since SMPTE timecode "midnight".

  • The framerate of the media the framestamp was generated for, as represented by a rational frames-per-second value.

  • Any associated metadata about the source representation the framestamp was parsed from, such as SMPTE NTSC non-drop timecode.

A fully-formed framestamp for 01:00:00:00 at 23.98 NTSC would be 18018/5 @ 24000/1001 NTSC non-drop.

why-prefer-seconds

Why prefer seconds?

SMPTE timecode is the canonical way frames are identified in professional video workflows. As a human-readable data type, timecode strings are great! You can easily locate, compare, and add timecode strings at-a-glance.

Why then, does Vtc come up with a new representation?

Well, SMPTE timecode strings are not as great for computers. Let's take a quick look at what we want from a good frame identifier:

  • Uniquely identifies a frame in a specific video stream.

  • Sortable by real-world occurrence.

  • Easily added/subtracted to each other.

  • All of the above, in mixed-framerate contexts.

The last point is key, timecode is great... if all of your media is running at the same framerate. For instance, when syncing media streams between two devices -- one running at 24fps, and one running at 48fps -- 01:00:00:13 and 01:00:00:26 are equivalent values, as they were captured at the same point in time, and should be synced together. Timecode is an expression of frame index more than frame seconds, and as such, cannot be lexically sorted in mixed-rate settings. Further, a computer cannot add "01:30:00:00" to "01:00:00:00" without converting it to some sort of numerical value.

Many programs convert timecode directly to an integer frame number for arithmetic and comparison operations where each frame on the clock is issued a continuous index, with 0 as 00:00:00:00. Frame numbers, though, have the same issue with mixed-rate values as timecode; 26 at 48 frames-per-second represents the same real-world time as 13 at 24 frames-per-seconds, and preserving that equality is important for operations like jam-syncing.

So that leaves us with real-world seconds. Convert timecode values -- even ones captured in mixed rates -- to seconds, then add and sort to your heart's content.

why-rational-numbers

Why rational numbers?

We'll avoid a deep-dive over why we use a rational value over a float or decimal, but you can read more on that choice here.

The short version is that many common SMPTE-specified framerates are defined as irrational numbers. For instance, 23.98 NTSC is defined as 24000/1001 frames-per-second.

In order to avoid off-by-one errors when using seconds, we need to avoid resolving values like 1001/24000 -- the value for frame 1 at 23.98 NTSC -- into any sort of decimal representation, since 1001/24000 is an irrational value and cannot be cleanly represented as a decimal. It's digits ride off into the sunset.

why-include-framerate

Why include framerate?

SMPTE timecode does not include a framerate in it's specification for frame identifiers, i.e 01:00:00:00. So why does Vtc?

Lets say that we are working with a given video file, and you are handed the timecode 01:00:00:12. What frame does that belong to?

Without a framerate, you cannot know. If we are talking about 23.98 NTSC media, it belongs to frame 86,400, but if we are talking about 59.94 NTSC NDF, frame then it belongs to frame 216,000, and if we are talking about 59.94 NTSC DF media then it belongs to frame 215,784.

What about the other direction? We need to calculate the SMPTE timecode for frame 48, which we previously parsed from a timecode. Well if it was originally parsed using 23.98 NTSC footage, then it is TC 00:00:02:00, but if it is 59.94 NTSC then it is TC 00:00:00:48. Framerate is implicitly required for a SMPTE timecode to be comprehensible.

The story is the same with seconds. How many seconds does 01:00:00:00 represent? At 23.98 NTSC, it represents 18018/5 seconds, but at 24fps true it represents 3600/1 seconds.

We cannot know what frame a seconds value represents, or what seconds value a frame represents, without knowing that scalar value's associated framerate. It's like having a timestamp without a timezone. Even in systems where all timestamps are converted to UTC, we often keep the timezone information around because it's just too useful in mixed-timezone settings, and you can't be sure what a given timestamp represents in a vacuum if you don't have the associated timezone.

Framerate -- especially in mixed rate settings, which Vtc considers a first-class use case -- is required to sensibly execute many operations, like casting in an out of SMPTE Timecode, adding two timecodes together, etc.

For this reason we package the framerate of our media stream together with the scalar value that represents a frame in that stream, and take the onus of transporting these two values together off of the caller.

struct-fields

Struct Fields

  • seconds: The real-world seconds elapsed since 'midnight' as a rational value.

  • rate: the Framerate of the Framestamp.

parsing-seconds-t-or-frames-t

Parsing: Seconds.t() or Frames.t()

Parsing functions pre-append with_ to their name. When you give a value to a parsing function, it is the same value that would be returned by the equivalent unit conversion. So a value passed to with_frames is the same value frames would return:

iex> {:ok, framestamp} = Framestamp.with_frames(24, Rates.f23_98())
iex> inspect(framestamp)
"<00:00:01:00 <23.98 NTSC>>"
iex> Framestamp.frames(framestamp)
24

The Framestamp module only has two basic construction / parsing methods: with_seconds and with_frames.

At first blush, this may seem... odd. Where is with_timecode/2? Or with_premiere_ticks/2? We can render these formats, so why isn't there a parser for them? Well there is, sort of: the two functions above.

Vtc's second major conceit is that all of the various ways of representing a video frame's timestamp boil down to EITHER:

  • a) A representation of an index number for that frame

OR

  • b) A representation of the real-world seconds the frame occurred at.

SMPTE timecode is really a human-readable way to represent a frame number. Same with film feet+frames.

Premiere Ticks, on the other hand, represents a real-world seconds value, as broken down in 1/254_016_000_000ths of a second.

Instead of polluting the module's namespace with a range of constructors, Vtc declares a Frames protocol for types that represent a frame count, and a Seconds protocol for types that represent a time-scalar.

All framestamp representations eventually get funneled through one of these protocols. For instance, when the String implementation of the protocol detects a SMPTE timecode string, it wraps the value in a SMPTETimecodeStr struct which handles converting that string to a frame number thorough implementing the Frames protocol. That frame number is then taken by with_frames and converted to a rational seconds value.

Going through protocols allows callers to define their own types that work with Vtc's parsing functions directly.

sorting-support

Sorting Support

Framestamp implements compare/2, and as such, can be used wherever the standard library calls for a Sorter module. Let's see it in action:

iex> stamp_01 = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> stamp_02 = Framestamp.with_frames!("02:00:00:00", Rates.f23_98())
iex>
iex> sorted = Enum.sort([stamp_02, stamp_01], Framestamp)
iex> inspect(sorted)
"[<01:00:00:00 <23.98 NTSC>>, <02:00:00:00 <23.98 NTSC>>]"
iex> sorted = Enum.sort([stamp_01, stamp_02], {:desc, Framestamp})
iex> inspect(sorted)
"[<02:00:00:00 <23.98 NTSC>>, <01:00:00:00 <23.98 NTSC>>]"
iex> max = Enum.max([stamp_02, stamp_01], Framestamp)
iex> inspect(max)
"<02:00:00:00 <23.98 NTSC>>"
iex> min = Enum.min([stamp_02, stamp_01], Framestamp)
iex> inspect(min)
"<01:00:00:00 <23.98 NTSC>>"
iex> data_01 = %{id: 2, tc: stamp_01}
iex> data_02 = %{id: 1, tc: stamp_02}
iex> sorted = Enum.sort_by([data_02, data_01], & &1.tc, Framestamp)
iex> inspect(sorted)
"[%{id: 2, tc: <01:00:00:00 <23.98 NTSC>>}, %{id: 1, tc: <02:00:00:00 <23.98 NTSC>>}]"

arithmetic-autocasting

Arithmetic Autocasting

For operators that take two Framestamp values, like add/3 or compare/2, as long as one argument is a Framestamp value, a or b May be any value that implements the Frames protocol, such as a timecode string, and will be assumed to be the same framerate as the other.

Production code

Autocasting exists to support quick scratch scripts and we suggest that it not be relied upon in production application code.

If parsing the value fails during casting, the function raises a Vtc.Framestamp.ParseError.

using-as-an-ecto-type

Using as an Ecto Type

See PgFramestamp for information on how to use Framerate in your postgres database as a native type.

Link to this section Summary

Types

Describes which side to inherit the framerate from in mixed-rate arithmetic.

Type returned by with_seconds/3 and with_frames/3.

Valid values for rounding options.

t()

Parse

Return a Framestamp for SMPTE timecode 24:00:00:00 at rate.

As smpte_midnight/1, but raises on error.

Returns a new Framestamp with a frames/2 return value equal to the frames arg.

As Framestamp.with_frames/3, but raises on error.

Returns a new Framestamp with a :seconds field value equal to the seconds arg.

Manipulate

Rebases framestamp to a new framerate.

As rebase/2, but raises on error.

Wrap value to the nearest valid TOD (time-of-day) timecode.

As smpte_wrap_tod/1, but raises on error.

Compare

Compare the values of a and b.

Returns true if a is equal to b.

Returns true if a is greater than b.

Returns true if a is greater than or equal to b.

Returns true if a is less than b.

Returns true if a is less than or equal to b.

Arithmetic

Returns the absolute value of tc.

Add two framestamps.

Divides dividend by divisor.

Divides the total frame count of dividend by divisor and returns both a quotient and a remainder.

Evaluates Framestamp mathematical expressions in a do block.

As the kernel -/1 function.

Scales a by b.

Divides the total frame count of dividend by devisor, and returns the remainder.

Subtract b from a.

Convert

Returns the number of physical film feet and frames framestamp represents if shot on film.

Returns the number of frames that would have elapsed between 00:00:00:00 and Framestamp.

Returns the number of elapsed ticks framestamp represents in Adobe Premiere Pro.

Runtime Returns the true, real-world runtime of framestamp in HH:MM:SS.FFFFFFFFF format.

Returns the the formatted SMPTE timecode for a Framestamp.

The individual sections of a SMPTE timecode string as i64 values.

Ecto Migrations

The database type for PgFramestamp.

Changeset Validations

Adds all constraints created by PgFramestamp.Migrations.create_constraints/3 to changeset to be added as changeset errors rather than raised.

Functions

Callback implementation for Ecto.Type.embed_as/1.

Callback implementation for Ecto.Type.equal?/2.

Link to this section Types

@type inherit_opt() :: :left | :right | false

Describes which side to inherit the framerate from in mixed-rate arithmetic.

@type parse_result() ::
  {:ok, t()}
  | {:error,
     Vtc.Framestamp.ParseError.t()
     | %ArgumentError{__exception__: true, message: term()}}

Type returned by with_seconds/3 and with_frames/3.

@type round() :: :closest | :floor | :ceil | :trunc | :off

Valid values for rounding options.

  • :closest: Round the to the closet whole frame. Rounds away from zero when value is equidistant from two whole-frames.

  • :floor: Always round down to the closest whole-frame. Negative numbers round away from zero

  • :ciel: Always round up to the closest whole-frame. Negative numbers round towards zero.

  • :trunc: Always round towards zero to the closest whole frame. Negative numbers round up and positive numbers round down.

  • :off: Do not round. Will always raise if result would represent a non-whole-frame value.

@type t() :: %Vtc.Framestamp{rate: Vtc.Framerate.t(), seconds: Ratio.t()}

Framestamp type.

Link to this section Parse

@spec smpte_midnight(Vtc.Framerate.t()) ::
  {:ok, t()} | {:error, Vtc.Framerate.InvalidSMPTEValueError.t()}

Return a Framestamp for SMPTE timecode 24:00:00:00 at rate.

Since all non-drop timecodes share the same midnight seconds value, and all drop timecodes similarly share the same midnight value, this function is significantly more performant than using the regular parsing functions.

Will return error if rate is not a drop, non-drop, or whole-frame SMPTE timecode.

examples

Examples

iex> {:ok, framestamp} = Framestamp.smpte_midnight(Rates.f23_98())
iex> inspect(framestamp)
"<24:00:00:00 <23.98 NTSC>>"
@spec smpte_midnight!(Vtc.Framerate.t()) :: t()

As smpte_midnight/1, but raises on error.

raises

Raises

Link to this function

with_frames(frames, rate)

View Source
@spec with_frames(Vtc.Source.Frames.t(), Vtc.Framerate.t()) :: parse_result()

Returns a new Framestamp with a frames/2 return value equal to the frames arg.

arguments

Arguments

  • frames: A value which can be represented as a frame number / frame count. Must implement the Frames protocol.

  • rate: Frame-per-second playback value of the framestamp.

options

Options

  • round: How to round the result with regards to whole-frames. Default: :closest.

examples

Examples

Accepts SMPTE timecode strings...

iex> result = Framestamp.with_frames("01:00:00:00", Rates.f23_98())
iex> inspect(result)
"{:ok, <01:00:00:00 <23.98 NTSC>>}"

... feet+frames strings...

iex> result = Framestamp.with_frames("5400+00", Rates.f23_98())
iex> inspect(result)
"{:ok, <01:00:00:00 <23.98 NTSC>>}"

By default, feet+frames is interpreted as 35mm, 4perf film. You can use the FeetAndFrames struct to parse other film formats:

iex> alias Vtc.Source.Frames.FeetAndFrames
iex>
iex> {:ok, feet_and_frames} = FeetAndFrames.from_string("5400+00", film_format: :ff16mm)
iex>
iex> result = Framestamp.with_frames(feet_and_frames, Rates.f23_98())
iex> inspect(result)
"{:ok, <01:15:00:00 <23.98 NTSC>>}"

... integers...

iex> result = Framestamp.with_frames(86_400, Rates.f23_98())
iex> inspect(result)
"{:ok, <01:00:00:00 <23.98 NTSC>>}"

... and integer strings.

iex> result = Framestamp.with_frames("86400", Rates.f23_98())
iex> inspect(result)
"{:ok, <01:00:00:00 <23.98 NTSC>>}"
Link to this function

with_frames!(frames, rate)

View Source
@spec with_frames!(Vtc.Source.Frames.t(), Vtc.Framerate.t()) :: t()

As Framestamp.with_frames/3, but raises on error.

Link to this function

with_seconds(seconds, rate, opts \\ [])

View Source
@spec with_seconds(
  Vtc.Source.Seconds.t(),
  Vtc.Framerate.t(),
  opts :: [{:round, round() | :off}]
) :: parse_result()

Returns a new Framestamp with a :seconds field value equal to the seconds arg.

arguments

Arguments

  • seconds: A value which can be represented as a number of real-world seconds. Must implement the Seconds protocol.

  • rate: Frame-per-second playback value of the framestamp.

options

Options

  • round: How to round the result with regards to whole-frames. If set to :off, will return an error if the provided seconds value does not exactly represent a whole-number frame count. Default: :closest.

examples

Examples

Accepts runtime strings...

iex> result = Framestamp.with_seconds("01:00:00.5", Rates.f23_98())
iex> inspect(result)
"{:ok, <00:59:56:22 <23.98 NTSC>>}"

... floats...

iex> result = Framestamp.with_seconds(3600.5, Rates.f23_98())
iex> inspect(result)
"{:ok, <00:59:56:22 <23.98 NTSC>>}"

... integers...

iex> result = Framestamp.with_seconds(3600, Rates.f23_98())
iex> inspect(result)
"{:ok, <00:59:56:10 <23.98 NTSC>>}"

... integer strings...

iex> result = Framestamp.with_seconds("3600", Rates.f23_98())
iex> inspect(result)
"{:ok, <00:59:56:10 <23.98 NTSC>>}"

... and float strings.

iex> result = Framestamp.with_seconds("3600.5", Rates.f23_98())
iex> inspect(result)
"{:ok, <00:59:56:22 <23.98 NTSC>>}"

premiere-ticks

Premiere Ticks

The Vtc.Source.Seconds.PremiereTicks struck implements the Seconds protocol and can be used to parse the format. This struct is not a general-purpose Module for the unit, and only exists to hint to the parsing function how it should be processed:

iex> alias Vtc.Source.Seconds.PremiereTicks
iex>
iex> input = %PremiereTicks{in: 254_016_000_000}
iex>
iex> result = Framestamp.with_seconds!(input, Rates.f23_98())
iex> inspect(result)
"<00:00:01:00 <23.98 NTSC>>"
Link to this function

with_seconds!(seconds, rate, opts \\ [])

View Source
@spec with_seconds!(
  Vtc.Source.Seconds.t(),
  Vtc.Framerate.t(),
  opts :: [{:round, round() | :off}]
) :: t()

As with_seconds/3, but raises on error.

Link to this section Manipulate

Link to this function

rebase(framestamp, rate)

View Source
@spec rebase(t(), Vtc.Framerate.t()) :: parse_result()

Rebases framestamp to a new framerate.

The real-world seconds are recalculated using the same frame count as if they were being played back at new_rate instead of framestamp.rate.

examples

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> {:ok, rebased} = Framestamp.rebase(framestamp, Rates.f47_95())
iex> inspect(rebased)
"<00:30:00:00 <47.95 NTSC>>"
Link to this function

rebase!(framestamp, new_rate)

View Source
@spec rebase!(t(), Vtc.Framerate.t()) :: t()

As rebase/2, but raises on error.

@spec smpte_wrap_tod(t()) ::
  {:ok, t()} | {:error, Vtc.Framerate.InvalidSMPTEValueError.t()}

Wrap value to the nearest valid TOD (time-of-day) timecode.

Framestamps with a SMPTE timecode of less than 00:00:00:00 will have 24:00:00:00 recursively added until they are positive.

Framestamps with a SMPTE timecode of greater than or equal to 24:00:00:00 will have 24:00:00:00 subtracted until they are less than 24:00:00:00.

Returns error if value.rate is not NTSC or whole-frame. Time-of-day timecode is not defined for non-SMPTE framerates.

examples

Examples

iex> {:ok, stamp} = Framestamp.with_frames("01:30:21:17", Rates.f23_98())
iex> {:ok, stamp} = Framestamp.smpte_wrap_tod(stamp)
iex> inspect(stamp)
"<01:30:21:17 <23.98 NTSC>>"
iex> {:ok, stamp} = Framestamp.with_frames("24:30:21:17", Rates.f23_98())
iex> {:ok, stamp} = Framestamp.smpte_wrap_tod(stamp)
iex> inspect(stamp)
"<00:30:21:17 <23.98 NTSC>>"
iex> {:ok, stamp} = Framestamp.with_frames("-01:00:00:00", Rates.f23_98())
iex> {:ok, stamp} = Framestamp.smpte_wrap_tod(stamp)
iex> inspect(stamp)
"<23:00:00:00 <23.98 NTSC>>"
iex> {:ok, stamp} = Framestamp.with_frames("24:00:00:00", Rates.f23_98())
iex> {:ok, stamp} = Framestamp.smpte_wrap_tod(stamp)
iex> inspect(stamp)
"<00:00:00:00 <23.98 NTSC>>"
@spec smpte_wrap_tod!(t()) :: t()

As smpte_wrap_tod/1, but raises on error.

raises

Raises

Link to this section Compare

@spec compare(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  :lt | :eq | :gt

Compare the values of a and b.

Compatible with Enum.sort/2. For more on sorting non-builtin values, see the Elixir documentation.

auto-casts Frames values. See eq?/2 for more information on how equality is determined.

examples

Examples

Using two framestamps parsed from SMPTE timecode, 01:00:00:00 NTSC is greater than 01:00:00:00 true because it represents more real-world time.

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:00", Rates.f24())
iex> :gt = Framestamp.compare(a, b)

Using a framestamp and a bare string:

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> :eq = Framestamp.compare(framestamp, "01:00:00:00")
@spec eq?(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  boolean()

Returns true if a is equal to b.

auto-casts Frames values.

examples

Examples

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> true = Framestamp.eq?(a, b)

Framestamps with the same string timecode representation, but different real-world seconds values, are not equal:

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:00", Rates.f24())
iex> false = Framestamp.eq?(a, b)

But Framestamps with the different SMPTE timecode string representation, but the same real-world seconds values, are equal:

iex> a = Framestamp.with_frames!("01:00:00:12", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:24", Rates.f47_95())
iex> true = Framestamp.eq?(a, b)
@spec gt?(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  boolean()

Returns true if a is greater than b.

auto-casts Frames values. See eq?/2 for more information on how equality is determined.

@spec gte?(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  boolean()

Returns true if a is greater than or equal to b.

auto-casts Frames values. See eq?/2 for more information on how equality is determined.

@spec lt?(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  boolean()

Returns true if a is less than b.

auto-casts Frames values. See eq?/2 for more information on how equality is determined.

examples

Examples

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("02:00:00:00", Rates.f23_98())
iex> true = Framestamp.lt?(a, b)
iex> false = Framestamp.lt?(b, a)
@spec lte?(a :: t() | Vtc.Source.Frames.t(), b :: t() | Vtc.Source.Frames.t()) ::
  boolean()

Returns true if a is less than or equal to b.

auto-casts Frames values. See eq?/2 for more information on how equality is determined.

Link to this section Arithmetic

@spec abs(t()) :: t()

Returns the absolute value of tc.

examples

Examples

iex> stamp = Framestamp.with_frames!("-01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.abs(stamp)
iex> inspect(result)
"<01:00:00:00 <23.98 NTSC>>"
iex> stamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.abs(stamp)
iex> inspect(result)
"<01:00:00:00 <23.98 NTSC>>"
@spec add(
  a :: t() | Vtc.Source.Frames.t(),
  b :: t() | Vtc.Source.Frames.t(),
  opts :: [inherit_rate: inherit_opt(), round: round()]
) :: t()

Add two framestamps.

Uses the real-world seconds representation. When the rates of a and b are not equal, the result will inherit the framerate of a and be rounded to the seconds representation of the nearest whole-frame at that rate.

auto-casts Frames values.

options

Options

  • inherit_rate: Which side to inherit the framerate from in mixed-rate calculations. If false, this function will raise if a.rate does not match b.rate. Default: false.

  • round: How to round the result with respect to whole-frames when mixing framerates. Default: :closest.

examples

Examples

Two framestamps running at the same rate:

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:30:21:17", Rates.f23_98())
iex>
iex> result = Framestamp.add(a, b)
iex> inspect(result)
"<02:30:21:17 <23.98 NTSC>>"

Two framestamps running at different rates:

iex> a = Framestamp.with_frames!("01:00:00:02", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:00:00:02", Rates.f47_95())
iex>
iex> result = Framestamp.add(a, b, inherit_rate: :left)
iex> inspect(result)
"<01:00:00:03 <23.98 NTSC>>"
iex>
iex> result = Framestamp.add(a, b, inherit_rate: :right)
iex> inspect(result)
"<01:00:00:06 <47.95 NTSC>>"

If :inherit_rate is not set...

iex> a = Framestamp.with_frames!("01:00:00:02", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:00:00:02", Rates.f47_95())
iex> Framestamp.add(a, b)
** (Vtc.Framestamp.MixedRateArithmeticError) attempted `Framestamp.add(a, b)` where `a.rate` does not match `b.rate`. try `:inherit_rate` option to `:left` or `:right`. alternatively, do your calculation in seconds, then cast back to `Framestamp` with the appropriate framerate using `with_seconds/3`

Using a framestamps and a bare string:

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.add(a, "01:30:21:17")
iex> inspect(result)
"<02:30:21:17 <23.98 NTSC>>"
Link to this function

div(dividend, divisor, opts \\ [])

View Source
@spec div(
  dividend :: t(),
  divisor :: Ratio.t() | number(),
  opts :: [{:round, round()}]
) :: t()

Divides dividend by divisor.

The result will inherit the framerate of dividend and rounded to the nearest whole-frame based on the :round option.

options

Options

  • round: How to round the result with respect to whole-frame values. Defaults to :trunc to match divmod and the expected meaning of div to mean integer division in elixir.

examples

Examples

iex> dividend = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.div(dividend, 2)
iex> inspect(result)
"<00:30:00:00 <23.98 NTSC>>"

iex> dividend = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.div(dividend, 0.5)
iex> inspect(result)
"<02:00:00:00 <23.98 NTSC>>"
Link to this function

divrem(dividend, divisor, opts \\ [])

View Source
@spec divrem(
  dividend :: t(),
  divisor :: Ratio.t() | number(),
  opts :: [round_frames: round(), round_remainder: round()]
) :: {t(), t()}

Divides the total frame count of dividend by divisor and returns both a quotient and a remainder.

The quotient returned is equivalent to Framestamp.div/3 with the :round option set to :trunc.

options

Options

  • round_frames: How to round the frame count before doing the divrem operation. Default: :closest.

  • round_remainder: How to round the remainder frames when a non-whole frame would be the result. Default: :closest.

examples

Examples

iex> dividend = Framestamp.with_frames!("01:00:00:01", Rates.f23_98())
iex>
iex> result = Framestamp.divrem(dividend, 4)
iex> inspect(result)
"{<00:15:00:00 <23.98 NTSC>>, <00:00:00:01 <23.98 NTSC>>}"
Link to this macro

eval(opts \\ [], body)

View Source (macro)
@spec eval(
  [at: Vtc.Framerate.t() | number() | Ratio.t(), ntsc: Vtc.Framerate.ntsc()],
  Macro.input()
) ::
  Macro.t()

Evaluates Framestamp mathematical expressions in a do block.

Any code captured within this macro can use Kernel operators to work with Framestamp values instead of module functions like add/2.

options

Options

  • at: The Framerate to cast non-Framestamp values to. If this value is not set, then at least one value in each operation must be a Framestamp. This value can be any value accepted by Framerate.new/2.

  • ntsc: The ntsc value to use when creating a new Framerate with at. Not needed if at is a Framerate value.

examples

Examples

Use eval to do some quick math. The block captures variables from the outer scope, but contains the expression within its own scope, just like an if or with statement.

iex> require Framestamp
iex>
iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:30:00:00", Rates.f23_98())
iex> c = Framestamp.with_frames!("00:15:00:00", Rates.f23_98())
iex>
iex> result =
iex>   Framestamp.eval do
iex>     a + b * 2 - c
iex>   end
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"

Or if you want to do it in one line:

iex> require Framestamp
iex>
iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:30:00:00", Rates.f23_98())
iex> c = Framestamp.with_frames!("00:15:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.eval(a + b * 2 - c)
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"

Just like the regular Framestamp functions, only one value in an arithmetic expression needs to be a Framestamp value. In the case above, since multiplication happens first, that's b:

iex> b = Framestamp.with_frames!("00:30:00:00", Rates.f23_98())
iex>
iex> result =
iex>   Framestamp.eval do
iex>     "01:00:00:00" + b * 2 - "00:15:00:00"
iex>   end
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"

You can supply a default framerate if you just want to do some quick calculations. This framerate is inherited by every value that implements the Frames protocol in the block, including integers:

iex> result =
iex>   Framestamp.eval at: Rates.f23_98() do
iex>     "01:00:00:00" + "00:30:00:00" * 2 - "00:15:00:00"
iex>   end
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"

You can use any value that can be parsed by Framerate.new/2.

iex> result =
iex>   Framestamp.eval at: 23.98 do
iex>     "01:00:00:00" + "00:30:00:00" * 2 - "00:15:00:00"
iex>   end
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"

ntsc: :non_drop, coerce_ntsc?: true is assumed by default, but you can set a different value with the :ntsc option:

iex> result =
iex>   Framestamp.eval at: 24, ntsc: nil do
iex>     "01:00:00:00" + "00:30:00:00" * 2 - "00:15:00:00"
iex>   end
iex>
iex> inspect(result)
"<01:45:00:00 <24.0 fps>>"
@spec minus(t()) :: t()

As the kernel -/1 function.

  • Makes a positive tc value negative.
  • Makes a negative tc value positive.

examples

Examples

iex> stamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.minus(stamp)
iex> inspect(result)
"<-01:00:00:00 <23.98 NTSC>>"
iex> stamp = Framestamp.with_frames!("-01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.minus(stamp)
iex> inspect(result)
"<01:00:00:00 <23.98 NTSC>>"
@spec mult(
  a :: t(),
  b :: Ratio.t() | number(),
  opts :: [{:round, round()}]
) :: t()

Scales a by b.

The result will inherit the framerate of a and be rounded to the seconds representation of the nearest whole-frame based on the :round option.

options

Options

  • round: How to round the result with respect to whole-frame values. Defaults to :closest.

examples

Examples

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.mult(a, 2)
iex> inspect(result)
"<02:00:00:00 <23.98 NTSC>>"

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.mult(a, 0.5)
iex> inspect(result)
"<00:30:00:00 <23.98 NTSC>>"
Link to this function

rem(dividend, divisor, opts \\ [])

View Source
@spec rem(
  dividend :: t(),
  divisor :: Ratio.t() | number(),
  opts :: [round_frames: round(), round_remainder: round()]
) :: t()

Divides the total frame count of dividend by devisor, and returns the remainder.

The quotient is truncated before the remainder is calculated.

options

Options

  • round_frames: How to round the frame count before doing the rem operation. Default: :closest.

  • round_remainder: How to round the remainder frames when a non-whole frame would be the result. Default: :closest.

examples

Examples

iex> dividend = Framestamp.with_frames!("01:00:00:01", Rates.f23_98())
iex>
iex> result = Framestamp.rem(dividend, 4)
iex> inspect(result)
"<00:00:00:01 <23.98 NTSC>>"
@spec sub(
  a :: t() | Vtc.Source.Frames.t(),
  b :: t() | Vtc.Source.Frames.t(),
  opts :: [inherit_rate: inherit_opt(), round: round()]
) :: t()

Subtract b from a.

Uses their real-world seconds representation. When the rates of a and b are not equal, the result will inherit the framerate of a and be rounded to the seconds representation of the nearest whole-frame at that rate.

auto-casts Frames values.

options

Options

  • inherit_rate: Which side to inherit the framerate from in mixed-rate calculations. If false, this function will raise if a.rate does not match b.rate. Default: false.

  • round: How to round the result with respect to whole-frames when mixing framerates. Default: :closest.

examples

Examples

Two framestamps running at the same rate:

iex> a = Framestamp.with_frames!("01:30:21:17", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.sub(a, b)
iex> inspect(result)
"<00:30:21:17 <23.98 NTSC>>"

Two framestamps running at different rates:

iex> a = Framestamp.with_frames!("01:00:00:02", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:00:00:02", Rates.f47_95())
iex>
iex> result = Framestamp.sub(a, b, inherit_rate: :left)
iex> inspect(result)
"<01:00:00:01 <23.98 NTSC>>"
iex>
iex> result = Framestamp.sub(a, b, inherit_rate: :right)
iex> inspect(result)
"<01:00:00:02 <47.95 NTSC>>"

If :inherit_rate is not set...

iex> a = Framestamp.with_frames!("01:00:00:02", Rates.f23_98())
iex> b = Framestamp.with_frames!("00:00:00:02", Rates.f47_95())
iex> Framestamp.sub(a, b)
** (Vtc.Framestamp.MixedRateArithmeticError) attempted `Framestamp.sub(a, b)` where `a.rate` does not match `b.rate`. try `:inherit_rate` option to `:left` or `:right`. alternatively, do your calculation in seconds, then cast back to `Framestamp` with the appropriate framerate using `with_seconds/3`

When b is greater than a, the result is negative:

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("02:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.sub(a, b)
iex> inspect(result)
"<-01:00:00:00 <23.98 NTSC>>"

Using a framestamps and a bare string:

iex> a = Framestamp.with_frames!("01:30:21:17", Rates.f23_98())
iex>
iex> result = Framestamp.sub(a, "01:00:00:00")
iex> inspect(result)
"<00:30:21:17 <23.98 NTSC>>"

Link to this section Convert

Link to this function

feet_and_frames(framestamp, opts \\ [])

View Source
@spec feet_and_frames(t(), opts :: [film_format: Vtc.FilmFormat.t(), round: round()]) ::
  Vtc.Source.Frames.FeetAndFrames.t()

Returns the number of physical film feet and frames framestamp represents if shot on film.

Ex: '5400+13'.

options

Options

  • round: How to round the internal frame count before conversion. Default: :closest.

  • film_format: The film format to use when doing the calculation. For more on film formats, see Vtc.FilmFormat. Default: :ff35mm_4perf, by far the most common format used to shoot Hollywood movies.

what-it-is

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

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.

  • Telecine.
  • Sound turnover reference picture.
  • Sound turnover change lists.

For more information on individual film formats, see the Vtc.FilmFormat module.

examples

Examples

Defaults to 35mm, 4perf:

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.feet_and_frames(framestamp)
iex> inspect(result)
"<5400+00 :ff35mm_4perf>"

Use String.Chars to convert the resulting struct to a traditional F=F string:

iex> alias Vtc.Source.Frames.FeetAndFrames
iex>
iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.feet_and_frames(framestamp)
iex> String.Chars.to_string(result)
"5400+00"

Outputting as a different film format:

examples-1

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.feet_and_frames(framestamp, film_format: :ff16mm)
iex> inspect(result)
"<4320+00 :ff16mm>"
Link to this function

frames(framestamp, opts \\ [])

View Source
@spec frames(t(), opts :: [{:round, round()}]) :: integer()

Returns the number of frames that would have elapsed between 00:00:00:00 and Framestamp.

options

Options

  • round: How to round the resulting frame number.

what-it-is

What it is

Frame number / frames count is the number of a frame if the SMPTE timecode started at 00:00:00:00 and had been running until the current value. A SMPTE timecode of '00:00:00:10' has a frame number of 10. A SMPTE timecode of '01:00:00:00' has a frame number of 86400.

where-you-see-it

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>

examples

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> Framestamp.frames(framestamp)
86400
Link to this function

premiere_ticks(framestamp, opts \\ [])

View Source
@spec premiere_ticks(t(), opts :: [{:round, round()}]) :: integer()

Returns the number of elapsed ticks framestamp represents in Adobe Premiere Pro.

options

Options

  • round: How to round the resulting ticks.

what-it-is

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

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>

examples

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> Framestamp.premiere_ticks(framestamp)
915372057600000
Link to this function

runtime(framestamp, opts \\ [])

View Source
@spec runtime(t(), precision: non_neg_integer(), trim_zeros?: boolean()) :: String.t()

Runtime Returns the true, real-world runtime of framestamp in HH:MM:SS.FFFFFFFFF format.

Trailing zeroes are trimmed from the end of the return value. If the entire fractal seconds value would be trimmed, '.0' is used.

options

Options

  • precision: The number of places to round to. Extra trailing 0's will still be trimmed. Default: 9.

  • trim_zeros?: Whether to trim trailing zeroes. Default: true.

what-it-is

What it is

The human-readable version of seconds. It looks like timecode, but with a decimal seconds value instead of a frame number place.

where-you-see-it

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

note

Note

The true runtime will often diverge from the hours, minutes, and seconds value of the SMPTE timecode representation when dealing with non-whole-frame framerates. Even drop-frame timecode does not continuously adhere 1:1 to the actual runtime. For instance, <01:00:00;00 <29.97 NTSC DF>> has a true runtime of '00:59:59.9964', and <01:00:00:00 <23.98 NTSC>> has a true runtime of '01:00:03.6'

examples

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> Framestamp.runtime(framestamp)
"01:00:03.6"
Link to this function

smpte_timecode(framestamp, opts \\ [])

View Source
@spec smpte_timecode(t(), opts :: [{:round, round()}]) :: String.t()

Returns the the formatted SMPTE timecode for a Framestamp.

Ex: 01:00:00:00. Drop frame timecode will be rendered with a ';' separator before the frames field.

options

Options

  • round: How to round the resulting frames field.

what-it-is

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

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.

examples

Examples

iex> framestamp = Framestamp.with_frames!(86_400, Rates.f23_98())
iex> Framestamp.smpte_timecode(framestamp)
"01:00:00:00"
Link to this function

smpte_timecode_sections(framestamp, opts \\ [])

View Source
@spec smpte_timecode_sections(t(), opts :: [{:round, round()}]) ::
  Vtc.SMPTETimecode.Sections.t()

The individual sections of a SMPTE timecode string as i64 values.

examples

Examples

iex> framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex>
iex> result = Framestamp.smpte_timecode_sections(framestamp)
iex> inspect(result)
"%Vtc.SMPTETimecode.Sections{negative?: false, hours: 1, minutes: 0, seconds: 0, frames: 0}"

Link to this section Ecto Migrations

@spec type() :: atom()

The database type for PgFramestamp.

Can be used in migrations as the fields type.

Link to this section Changeset Validations

Link to this function

validate_constraints(changeset, field, opts \\ [])

View Source
@spec validate_constraints(
  Ecto.Changeset.t(data),
  atom(),
  [Vtc.Ecto.Postgres.PgFramestamp.Migrations.constraint_opt()]
) :: Ecto.Changeset.t(data)
when data: any()

Adds all constraints created by PgFramestamp.Migrations.create_constraints/3 to changeset to be added as changeset errors rather than raised.

options

Options

Pass the same options that were passed to PgFramestamp.Migrations.create_constraints/3

Link to this section Functions

Callback implementation for Ecto.Type.embed_as/1.

Callback implementation for Ecto.Type.equal?/2.