Packages
vtc
0.7.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.2
0.16.1
0.16.0
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.10
0.10.9
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.3
0.5.2
0.5.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
A SMPTE timecode library for Elixir
Current section
Files
Jump to
Current section
Files
lib/timcode.ex
defmodule Vtc.Timecode do
@moduledoc """
Represents the frame at a particular time in a video.
New Timecode values are created with the `with_seconds/3` and `with_frames/2`, and
other function prefaced by `with_*`.
## Struct Fields
- `seconds`: The real-world seconds elapsed since 01:00:00:00 as a rational value.
(Note: The Ratio module automatically will coerce itself to an integer whenever
possible, so this value may be an integer when exactly a whole-second value).
- `rate`: the Framerate of the timecode.
## Sorting Support
`Timecode` implements `compare/2`, and as such, can be used wherever the standard
library calls for a `Sorter` module. Let's see it in action:
```elixir
iex> tc_01 = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> tc_02 = Timecode.with_frames!("02:00:00:00", Rates.f23_98())
iex>
iex>
iex> Enum.sort([tc_02, tc_01], Timecode) |> inspect()
"[<01:00:00:00 <23.98 NTSC>>, <02:00:00:00 <23.98 NTSC>>]"
iex>
iex>
iex> Enum.sort([tc_01, tc_02], {:desc, Timecode}) |> inspect()
"[<02:00:00:00 <23.98 NTSC>>, <01:00:00:00 <23.98 NTSC>>]"
iex>
iex>
iex> Enum.max([tc_02, tc_01], Timecode) |> inspect()
"<02:00:00:00 <23.98 NTSC>>"
iex>
iex>
iex> Enum.min([tc_02, tc_01], Timecode) |> inspect()
"<01:00:00:00 <23.98 NTSC>>"
iex>
iex>
iex> data_01 = %{id: 2, tc: tc_01}
iex> data_02 = %{id: 1, tc: tc_02}
iex> Enum.sort_by([data_02, data_01], &(&1.tc), Timecode) |> inspect()
"[%{id: 2, tc: <01:00:00:00 <23.98 NTSC>>}, %{id: 1, tc: <02:00:00:00 <23.98 NTSC>>}]"
```
"""
import Kernel, except: [div: 2, rem: 2, abs: 1]
alias Vtc.FilmFormat
alias Vtc.Framerate
alias Vtc.Source.Frames
alias Vtc.Source.Frames.FeetAndFrames
alias Vtc.Source.Frames.TimecodeStr
alias Vtc.Source.Seconds
alias Vtc.Source.Seconds.PremiereTicks
alias Vtc.Source.Seconds.RuntimeStr
alias Vtc.Timecode
alias Vtc.Timecode.Eval
alias Vtc.Timecode.ParseError
alias Vtc.Timecode.Sections
alias Vtc.Utils.Rational
@enforce_keys [:seconds, :rate]
defstruct [:seconds, :rate]
@typedoc """
`Timecode` type.
"""
@type t() :: %__MODULE__{
seconds: Ratio.t(),
rate: Framerate.t()
}
@typedoc """
Valid values for rounding options.
- `:closest`: Round the to the closet whole frame.
- `:floor`: Always round down to the closest whole-frame.
- `:ciel`: Always round up to the closest whole-frame.
"""
@type round() :: :closest | :floor | :ceil
@typedoc """
As `round/0`, but includes `:off` option to disable rounding entirely. Not all
functions exposed by this module make logical sense without some form of rouding, so
`:off` will not be accepted by all functions.
"""
@type maybe_round() :: round() | :off
@typedoc """
Type returned by `with_seconds/3` and `with_frames/3`.
"""
@type parse_result() :: {:ok, t()} | {:error, ParseError.t() | %ArgumentError{}}
@doc """
Returns a new `Timecode` with a `Timecode.seconds` field value equal to the
`seconds` arg.
## 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 timecode.
## Options
- `round`: How to round the result with regards to whole-frames.
## Examples
Accetps runtime strings...
```elixir
iex> Timecode.with_seconds("01:00:00.5", Rates.f23_98) |> inspect()
"{:ok, <00:59:56:22 <23.98 NTSC>>}"
```
... floats...
```elixir
iex> Timecode.with_seconds(3600.5, Rates.f23_98) |> inspect()
"{:ok, <00:59:56:22 <23.98 NTSC>>}"
```
... integers...
```elixir
iex> Timecode.with_seconds(3600, Rates.f23_98) |> inspect()
"{:ok, <00:59:56:10 <23.98 NTSC>>}"
```
... integer Strings...
```elixir
iex> Timecode.with_seconds("3600", Rates.f23_98) |> inspect()
"{:ok, <00:59:56:10 <23.98 NTSC>>}"
```
... and float strings.
```elixir
iex> Timecode.with_seconds("3600.5", Rates.f23_98) |> inspect()
"{:ok, <00:59:56:22 <23.98 NTSC>>}"
```
## Premiere Ticks
The `Vtc.Source.Seconds.PremiereTicks` struck implements the `Vtc.Source.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:
```elixir
iex> alias Vtc.Source.Seconds.PremiereTicks
iex>
iex> input = %PremiereTicks{in: 254_016_000_000}
iex> Timecode.with_seconds!(input, Rates.f23_98()) |> inspect()
"<00:00:01:00 <23.98 NTSC>>"
```
"""
@spec with_seconds(Seconds.t(), Framerate.t(), opts :: [round: maybe_round()]) :: parse_result()
def with_seconds(seconds, rate, opts \\ []) do
round = Keyword.get(opts, :round, :closest)
with {:ok, seconds} <- Seconds.seconds(seconds, rate) do
# If the vaue doesn't cleany divide into the framerate then we need to round to the
# nearest frame.
seconds = with_seconds_round_to_frame(seconds, rate, round)
{:ok, %__MODULE__{seconds: seconds, rate: rate}}
end
end
# Rounds seconds value to the nearest whole-frame.
@spec with_seconds_round_to_frame(Ratio.t(), Framerate.t(), maybe_round()) :: Ratio.t()
def with_seconds_round_to_frame(seconds, _, :off), do: seconds
def with_seconds_round_to_frame(seconds, rate, round) do
case Ratio.div(seconds, rate.playback) do
%Ratio{denominator: 1} ->
seconds
%Ratio{} ->
rate.playback
|> Ratio.mult(seconds)
|> Rational.round(round)
|> Ratio.new()
|> Ratio.div(rate.playback)
end
end
@doc """
As `with_seconds/3`, but raises on error.
"""
@spec with_seconds!(Seconds.t(), Framerate.t(), opts :: [round: maybe_round()]) :: t()
def with_seconds!(seconds, rate, opts \\ []) do
seconds
|> with_seconds(rate, opts)
|> handle_raise_function()
end
@doc """
Returns a new `Timecode` with a `frames/1` return value equal to the `frames` arg.
## 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 timecode.
## Options
- `round`: How to round the result with regards to whole-frames.
## Examples
Accepts timecode strings...
```elixir
iex> Timecode.with_frames("01:00:00:00", Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC>>}"
```
... feet+frames strings...
```elixir
iex> Timecode.with_frames("5400+00", Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC>>}"
```
... integers...
```elixir
iex> Timecode.with_frames(86400, Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC>>}"
```
... and integer strings.
```elixir
iex> Timecode.with_frames("86400", Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC>>}"
```
"""
@spec with_frames(Frames.t(), Framerate.t()) :: parse_result()
def with_frames(frames, rate) do
case frames do
%Ratio{} -> raise "HERE"
val -> val
end
with {:ok, frames} <- Frames.frames(frames, rate) do
frames
|> Ratio.new()
|> Ratio.div(rate.playback)
|> with_seconds(rate)
end
end
@doc """
As `Timecode.with_frames/3`, but raises on error.
"""
@spec with_frames!(Frames.t(), Framerate.t()) :: t()
def with_frames!(frames, rate) do
frames
|> with_frames(rate)
|> handle_raise_function()
end
@doc """
Rebases the timecode 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 `timecode.rate`.
## Examples
```elixir
iex> timecode = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> {:ok, rebased} = Timecode.rebase(timecode, Rates.f47_95())
iex> inspect(rebased)
"<00:30:00:00 <47.95 NTSC>>"
```
"""
@spec rebase(t(), Framerate.t()) :: parse_result()
def rebase(%{rate: rate} = timecode, rate), do: {:ok, timecode}
def rebase(timecode, new_rate), do: timecode |> frames() |> with_frames(new_rate)
@doc """
As `rebase/2`, but raises on error.
"""
@spec rebase!(t(), Framerate.t()) :: t()
def rebase!(timecode, new_rate), do: timecode |> rebase(new_rate) |> handle_raise_function()
@doc """
Returns whether `a` is greater than, equal to, or less than `b` in terms of real-world
seconds. Compatible with `Enum.sort/2`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
> #### Sorting with Timecodes {: .info}
>
> This function can be used anyware the standard library expexts a `sorter`. See
> top-level `Timecode` docs for more information.
## Examples
Using two timecodes, `01:00:00:00` NTSC is greater than `01:00:00:00` true because it
represents more real-world time.
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("01:00:00:00", Rates.f24())
iex> :gt = Timecode.compare(a, b)
```
Using a timcode and a bare string:
```elixir
iex> timecode = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> :eq = Timecode.compare(timecode, "01:00:00:00")
```
"""
@spec compare(a :: t() | Frames.t(), b :: t() | Frames.t()) :: :lt | :eq | :gt
def compare(a, b) do
{a, b} = cast_op_args(a, b)
Ratio.compare(a.seconds, b.seconds)
end
@doc """
Returns `true` if `a` is eqaul to `b`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
"""
@spec eq?(a :: t() | Frames.t(), b :: t() | Frames.t()) :: boolean()
def eq?(a, b), do: compare(a, b) == :eq
@doc """
Returns `true` if `a` is less than `b`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
## Examples
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("02:00:00:00", Rates.f23_98())
iex> true = Timecode.lt?(a, b)
iex> false = Timecode.lt?(b, a)
```
"""
@spec lt?(a :: t() | Frames.t(), b :: t() | Frames.t()) :: boolean()
def lt?(a, b), do: compare(a, b) == :lt
@doc """
Returns `true` if `a` is less than or equal to `b`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
"""
@spec lte?(a :: t() | Frames.t(), b :: t() | Frames.t()) :: boolean()
def lte?(a, b), do: compare(a, b) in [:lt, :eq]
@doc """
Returns `true` if `a` is greater than `b`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
"""
@spec gt?(a :: t() | Frames.t(), b :: t() | Frames.t()) :: boolean()
def gt?(a, b), do: compare(a, b) == :gt
@doc """
Returns `true` if `a` is greater than or eqaul to `b`.
> #### Argument Autocasting {: .info}
>
> As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will
> #### Raises {: .error}
>
> Raises if there is an error parsing a `Frames` value from either argument.
"""
@spec gte?(a :: t() | Frames.t(), b :: t() | Frames.t()) :: boolean()
def gte?(a, b), do: compare(a, b) in [:gt, :eq]
@doc """
Adds two timecodoes together using their real-world seconds representation. When the
rates of `a` and `b` are not equal, the result will inheret the framerat of `a` and
be rounded to the seconds representation of the nearest whole-frame at that rate.
As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will raise
if there is an error parsing a `Frames` value.
## Options
- `round`: How to round the result with respect to whole-frames when mixing
framerates. Default: `:closest`.
## Examples
Two timecodes running at the same rate:
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("01:30:21:17", Rates.f23_98())
iex> Timecode.add(a, b) |> inspect()
"<02:30:21:17 <23.98 NTSC>>"
```
Two timecodes running at different rates:
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("00:00:00:02", Rates.f47_95())
iex> Timecode.add(a, b) |> inspect()
"<01:00:00:01 <23.98 NTSC>>"
```
Using a timcode and a bare string:
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.add(a, "01:30:21:17") |> inspect()
"<02:30:21:17 <23.98 NTSC>>"
```
"""
@spec add(a :: t() | Frames.t(), b :: t() | Frames.t(), opts :: [round: maybe_round()]) :: t()
def add(a, b, opts \\ []) do
{a, b} = cast_op_args(a, b)
a.seconds |> Ratio.add(b.seconds) |> with_seconds!(a.rate, opts)
end
@doc """
Subtracts two timecodoes together using their real-world seconds representation. When
the rates of `a` and `b` are not equal, the result will inheret the framerat of `a`
and be rounded to the seconds representation of the nearest whole-frame at that rate.
As long as one argument is a `Timecode` 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. This is mostly to support quick scripting. Will raise
if there is an error parsing a `Frames` value.
## Options
- `round`: How to round the result with respect to whole-frames when mixing
framerates. Default: `:closest`.
## Examples
Two timecodes running at the same rate:
```elixir
iex> a = Timecode.with_frames!("01:30:21:17", Rates.f23_98())
iex> b = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.sub(a, b) |> inspect()
"<00:30:21:17 <23.98 NTSC>>"
```
When `b` is greater than `a`, the result is negative:
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("02:00:00:00", Rates.f23_98())
iex> Timecode.sub(a, b) |> inspect()
"<-01:00:00:00 <23.98 NTSC>>"
```
Two timecodes running at different rates:
```elixir
iex> a = Timecode.with_frames!("01:00:00:02", Rates.f23_98())
iex> b = Timecode.with_frames!("00:00:00:02", Rates.f47_95())
iex> Timecode.sub(a, b) |> inspect()
"<01:00:00:01 <23.98 NTSC>>"
```
Using a timcode and a bare string:
```elixir
iex> a = Timecode.with_frames!("01:30:21:17", Rates.f23_98())
iex> Timecode.sub(a, "01:00:00:00") |> inspect()
"<00:30:21:17 <23.98 NTSC>>"
```
"""
@spec sub(a :: t(), b :: t() | Frames.t(), opts :: [round: maybe_round()]) :: t()
def sub(a, b, opts \\ []) do
{a, b} = cast_op_args(a, b)
a.seconds |> Ratio.sub(b.seconds) |> with_seconds!(a.rate, opts)
end
# Casts args for ops with two timecodes as long as at least one argument is a
# timecode. The non-timecode argument inherents the framerate of the timecode
# argument.
@spec cast_op_args(t() | Frames.t(), t() | Frames.t()) :: {t(), t()}
defp cast_op_args(%__MODULE__{} = a, %__MODULE__{} = b), do: {a, b}
defp cast_op_args(%__MODULE__{} = a, b), do: {a, with_frames!(b, a.rate)}
defp cast_op_args(a, %__MODULE__{} = b), do: {with_frames!(a, b.rate), b}
@doc """
Scales `a` by `b`. The result will inheret the framerat of `a` and be rounded to the
seconds representation of the nearest whole-frame based on the `:round` option.
## Options
- `round`: How to round the result with respect to whole-frame values. Defaults to
`:closest`.
## Examples
```elixir
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.mult(a, 2) |> inspect()
"<02:00:00:00 <23.98 NTSC>>"
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.mult(a, 0.5) |> inspect()
"<00:30:00:00 <23.98 NTSC>>"
```
"""
@spec mult(a :: t(), b :: Ratio.t() | number(), opts :: [round: maybe_round()]) :: t()
def mult(a, b, opts \\ []), do: a.seconds |> Ratio.mult(Ratio.new(b)) |> with_seconds!(a.rate, opts)
@doc """
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
- `round`: How to round the result with respect to whole-frame values. Defaults to
`:floor` to match `divmod` and the expected meaning of `div` to mean integer
division in elixir.
## Examples
```elixir
iex> dividend = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.div(dividend, 2) |> inspect()
"<00:30:00:00 <23.98 NTSC>>"
iex> dividend = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.div(dividend, 0.5) |> inspect()
"<02:00:00:00 <23.98 NTSC>>"
```
"""
@spec div(
dividend :: t(),
divisor :: Ratio.t() | number(),
opts :: [round: maybe_round()]
) :: t()
def div(dividend, divisor, opts \\ []) do
opts = Keyword.put_new(opts, :round, :floor)
dividend.seconds |> Ratio.div(Ratio.new(divisor)) |> with_seconds!(dividend.rate, opts)
end
@doc """
Divides the total frame count of `dividend` by `divisor` and returns both a quotient
and a remainder as Timecode values.
The quotient returned is equivalent to `Timecode.div/3` with the `:round` option set
to `:floor`.
## 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
```elixir
iex> dividend = Timecode.with_frames!("01:00:00:01", Rates.f23_98())
iex> Timecode.divrem(dividend, 4) |> inspect()
"{<00:15:00:00 <23.98 NTSC>>, <00:00:00:01 <23.98 NTSC>>}"
```
"""
@spec divrem(
dividend :: t(),
divisor :: Ratio.t() | number(),
opts :: [round_frames: round(), round_remainder: round()]
) :: {t(), t()}
def divrem(dividend, divisor, opts \\ []) do
round_frames = Keyword.get(opts, :round_frames, :closest)
round_remainder = Keyword.get(opts, :round_remainder, :closest)
with :ok <- ensure_round_enabled(round_frames, "round_frames"),
:ok <- ensure_round_enabled(round_remainder, "round_remainder") do
%{rate: rate} = dividend
{quotient, remainder} =
dividend
|> frames(round: round_frames)
|> Ratio.new()
|> Rational.divrem(Ratio.new(divisor))
remainder = Rational.round(remainder, round_remainder)
{with_frames!(quotient, rate), with_frames!(remainder, rate)}
end
end
@doc """
Devides the total frame count of `dividend` by `devisor`, rounds the quotient down,
and returns the remainder rounded to the nearest frame.
## 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.
## Examples
```elixir
iex> dividend = Timecode.with_frames!("01:00:00:01", Rates.f23_98())
iex> Timecode.rem(dividend, 4) |> inspect()
"<00:00:00:01 <23.98 NTSC>>"
```
"""
@spec rem(
dividend :: t(),
divisor :: Ratio.t() | number(),
opts :: [round_frames: round(), round_remainder: round()]
) :: t()
def rem(dividend, divisor, opts \\ []), do: dividend |> divrem(Ratio.new(divisor), opts) |> elem(1)
@doc """
As the kernel `-/1` function.
- Makes a positive `tc` value negative.
- Makes a negative `tc` value positive.
## Examples
```elixir
iex> tc = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.minus(tc) |> inspect()
"<-01:00:00:00 <23.98 NTSC>>"
```
```elixir
iex> tc = Timecode.with_frames!("-01:00:00:00", Rates.f23_98())
iex> Timecode.minus(tc) |> inspect()
"<01:00:00:00 <23.98 NTSC>>"
```
"""
@spec minus(t()) :: t()
def minus(tc), do: %{tc | seconds: Ratio.minus(tc.seconds)}
@doc """
Returns the absolute value of `tc`.
## Examples
```elixir
iex> tc = Timecode.with_frames!("-01:00:00:00", Rates.f23_98())
iex> Timecode.abs(tc) |> inspect()
"<01:00:00:00 <23.98 NTSC>>"
```
```elixir
iex> tc = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> Timecode.abs(tc) |> inspect()
"<01:00:00:00 <23.98 NTSC>>"
```
"""
@spec abs(t()) :: t()
def abs(tc), do: %{tc | seconds: Ratio.abs(tc.seconds)}
@doc """
Evalute timecode mathematical expression in a `do` block. Any code captured within
this macro can use Kernel operators to work with timecode values instead of module
functions like `add/2`.
## Options
- `at`: The Framerate to cast non-timecode values to. If this value is not set, then
at least one value in each operation must be a `Timecode`. 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
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.
```elixir
iex> require Timecode
iex>
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("00:30:00:00", Rates.f23_98())
iex> c = Timecode.with_frames!("00:15:00:00", Rates.f23_98())
iex>
iex> result = Timecode.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:
```elixir
iex> require Timecode
iex>
iex> a = Timecode.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Timecode.with_frames!("00:30:00:00", Rates.f23_98())
iex> c = Timecode.with_frames!("00:15:00:00", Rates.f23_98())
iex>
iex> result = Timecode.eval(a + b * 2 - c)
iex>
iex> inspect(result)
"<01:45:00:00 <23.98 NTSC>>"
```
Just like the regular `Timecode` functions, only one value in an arithmatic expression
needs to be a `Timecode` value. In the case above, since multiplication happens first,
that's `b`:
```elixir
iex> b = Timecode.with_frames!("00:30:00:00", Rates.f23_98())
iex>
iex> result = Timecode.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:
```elixir
iex> result = Timecode.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`.
```elixir
iex> result = Timecode.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` is assumed by default, but you can set a different value with the
`:ntsc` option:
```elixir
iex> result = Timecode.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 eval([at: Framerate.t() | number() | Ratio.t(), ntsc: Framerate.ntsc()], Macro.input()) :: Macro.t()
defmacro eval(opts \\ [], body), do: Eval.eval(opts, body)
@doc """
Returns the number of frames that would have elapsed between 00:00:00:00 and this
timecode.
## Options
- `round`: How to round the resulting frame number.
## What it is
Frame number / frames count 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:
```xml
<timecode>
<rate>
<timebase>24</timebase>
<ntsc>TRUE</ntsc>
</rate>
<string>01:00:00:00</string>
<frame>86400</frame> <!-- <====THIS LINE-->
<displayformat>NDF</displayformat>
</timecode>
```
"""
@spec frames(t(), opts :: [round: round()]) :: integer()
def frames(timecode, opts \\ []) do
round = Keyword.get(opts, :round, :closest)
with :ok <- ensure_round_enabled(round) do
timecode.seconds
|> Ratio.mult(timecode.rate.playback)
|> Rational.round(round)
end
end
@doc """
The individual sections of a timecode string as i64 values.
"""
@spec sections(t(), opts :: [round: round()]) :: Sections.t()
def sections(timecode, opts \\ []) do
round = Keyword.get(opts, :round, :closest)
with :ok <- ensure_round_enabled(round) do
Sections.from_timecode(timecode, opts)
end
end
@doc """
Returns the the formatted SMPTE timecode: (ex: 01:00:00:00). Drop frame timecode will
be rendered with a ';' sperator before the frames field.
## Options
- `round`: How to round the resulting frames field.
## 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](https://blog.frame.io/2017/07/17/timecode-and-frame-rates/) 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.
"""
@spec timecode(t(), opts :: [round: round()]) :: String.t()
def timecode(timecode, opts \\ []), do: timecode |> TimecodeStr.from_timecode(opts) |> then(& &1.in)
@doc """
Runtime Returns the true, real-world runtime of the timecode in HH:MM:SS.FFFFFFFFF
format.
Arguments
- `precision`: The number of places to round to. Extra trailing 0's will still be
trimmed.
## 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:
```shell
ffmpeg -ss 00:00:30.5 -i input.mov -t 00:00:10.25 output.mp4
```
## Note
The true runtime will often diverge from the hours, minutes, and seconds
value of the 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'
"""
@spec runtime(t(), integer()) :: String.t()
def runtime(timecode, precision \\ 9), do: timecode |> RuntimeStr.from_timecode(precision) |> then(& &1.in)
@doc """
Returns the number of elapsed ticks this timecode represents in Adobe Premiere Pro.
## Options
- `round`: How to round the resulting 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:
```xml
<clipitem id="clipitem-1">
...
<in>158</in>
<out>1102</out>
<pproTicksIn>1673944272000</pproTicksIn>
<pproTicksOut>11675231568000</pproTicksOut>
...
</clipitem>
```
"""
@spec premiere_ticks(t(), opts :: [round: round()]) :: integer()
def premiere_ticks(timecode, opts \\ []) do
round = Keyword.get(opts, :round, :closest)
with :ok <- ensure_round_enabled(round) do
timecode |> PremiereTicks.from_timecode(opts) |> then(& &1.in)
end
end
@doc """
Returns the number of feet and frames this timecode represents if it were shot on 35mm
4-perf film (16 frames per foot). ex: '5400+13'.
## Options
- `round`: How to round the internal frame count before conversion.
## 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.
- Telecine.
- Sound turnover reference picture.
- Sound turnover change lists.
"""
@spec feet_and_frames(t(), opts :: [format: FilmFormat.t(), round: round()]) ::
FeetAndFrames.t()
def feet_and_frames(timecode, opts \\ []), do: FeetAndFrames.from_timecode(timecode, opts)
# Ensures that rounding is enabled for functions that cannot meaningfully turn
# rounding off, such as those that must return an integer.
@spec ensure_round_enabled(maybe_round(), String.t()) :: :ok
defp ensure_round_enabled(round, arg_name \\ "round")
defp ensure_round_enabled(:off, arg_name), do: raise(ArgumentError.exception("`#{arg_name}` cannot be `:off`"))
defp ensure_round_enabled(_, _), do: :ok
@spec handle_raise_function({:ok, t()} | {:error, Exception.t()}) :: t()
defp handle_raise_function({:ok, result}), do: result
defp handle_raise_function({:error, error}), do: raise(error)
end
defimpl Inspect, for: Vtc.Timecode do
alias Vtc.Timecode
@spec inspect(Timecode.t(), Elixir.Inspect.Opts.t()) :: String.t()
def inspect(timecode, _opts) do
"<#{Timecode.timecode(timecode)} #{inspect(timecode.rate)}>"
end
end
defimpl String.Chars, for: Vtc.Timecode do
alias Vtc.Timecode
@spec to_string(Timecode.t()) :: String.t()
def to_string(timecode), do: inspect(timecode)
end