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 NDF>>, <02:00:00:00 <23.98 NTSC NDF>>]"
iex>
iex>
iex> Enum.sort([tc_01, tc_02], {:desc, Timecode}) |> inspect()
"[<02:00:00:00 <23.98 NTSC NDF>>, <01:00:00:00 <23.98 NTSC NDF>>]"
iex>
iex>
iex> Enum.max([tc_02, tc_01], Timecode) |> inspect()
"<02:00:00:00 <23.98 NTSC NDF>>"
iex>
iex>
iex> Enum.min([tc_02, tc_01], Timecode) |> inspect()
"<01:00:00:00 <23.98 NTSC NDF>>"
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 NDF>>}, %{id: 1, tc: <02:00:00:00 <23.98 NTSC NDF>>}]"
```
"""
import Kernel, except: [div: 2, rem: 2, abs: 1]
alias Vtc.Framerate
alias Vtc.Source.Frames
alias Vtc.Source.PremiereTicks
alias Vtc.Source.Seconds
alias Vtc.Timecode.ParseError
alias Vtc.Timecode.Sections
alias Vtc.Utils.Consts
alias Vtc.Utils.DropFrame
alias Vtc.Utils.Rational
@enforce_keys [:seconds, :rate]
defstruct [:seconds, :rate]
@typedoc """
`Timecode` type.
"""
@type t() :: %__MODULE__{
seconds: Rational.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 NDF>>}"
```
... floats...
```elixir
iex> Timecode.with_seconds(3600.5, Rates.f23_98) |> inspect()
"{:ok, <00:59:56:22 <23.98 NTSC NDF>>}"
```
... integers...
```elixir
iex> Timecode.with_seconds(3600, Rates.f23_98) |> inspect()
"{:ok, <00:59:56:10 <23.98 NTSC NDF>>}"
```
... integer Strings...
```elixir
iex> Timecode.with_seconds("3600", Rates.f23_98) |> inspect()
"{:ok, <00:59:56:10 <23.98 NTSC NDF>>}"
```
... and float strings.
```elixir
iex> Timecode.with_seconds("3600.5", Rates.f23_98) |> inspect()
"{:ok, <00:59:56:22 <23.98 NTSC NDF>>}"
```
"""
@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(Rational.t(), Framerate.t(), maybe_round()) :: Rational.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{} ->
rate.playback
|> Ratio.mult(seconds)
|> Rational.round(round)
|> Ratio.div(rate.playback)
_ ->
seconds
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 NDF>>}"
```
... feet+frames strings...
```elixir
iex> Timecode.with_frames("5400+00", Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC NDF>>}"
```
... integers...
```elixir
iex> Timecode.with_frames(86400, Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC NDF>>}"
```
... and integer strings.
```elixir
iex> Timecode.with_frames("86400", Rates.f23_98) |> inspect()
"{:ok, <01:00:00:00 <23.98 NTSC NDF>>}"
```
"""
@spec with_frames(Frames.t(), Framerate.t()) :: parse_result()
def with_frames(frames, rate) do
with {:ok, frames} <- Frames.frames(frames, rate) do
frames
|> 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 """
Returns a new `Timecode` with a `premiere_ticks/1` return value equal
to the ticks arg.
## Arguments
- `ticks`: Any value that can represent the number of ticks for a given timecode.
Must implement the `PremiereTicks` protocol.
- `rate`: Frame-per-second playback value of the timecode.
## Options
- `round`: How to round the result with regards to whole-frames.
## Examples
Accetps integers.
```elixir
iex> Timecode.with_premiere_ticks(254_016_000_000, Rates.f23_98) |> inspect()
"{:ok, <00:00:01:00 <23.98 NTSC NDF>>}"
```
"""
@spec with_premiere_ticks(
PremiereTicks.t(),
Framerate.t(),
opts :: [round: maybe_round()]
) :: parse_result()
def with_premiere_ticks(ticks, rate, opts \\ []) do
with {:ok, ticks} <- PremiereTicks.ticks(ticks, rate) do
seconds = Ratio.div(ticks, Consts.ppro_tick_per_second())
with_seconds(seconds, rate, opts)
end
end
@doc """
As `with_premiere_ticks/3`, but raises on error.
"""
@spec with_premiere_ticks!(
PremiereTicks.t(),
Framerate.t(),
opts :: [round: maybe_round()]
) :: t()
def with_premiere_ticks!(ticks, rate, opts \\ []) do
ticks
|> with_premiere_ticks(rate, opts)
|> 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 NDF>>"
```
"""
@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`.
`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 `a`. This is mostly to support quick
scripting. This function will raise if there is an error parsing `b`.
This function can be used anyware the standard library expexts a `sorter`.
## 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(), b :: t() | Frames.t()) :: :lt | :eq | :gt
def compare(a, %__MODULE__{} = b), do: Ratio.compare(a.seconds, b.seconds)
def compare(a, b), do: compare(a, with_frames!(b, a.rate))
@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.
`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 `a`. This is mostly to support quick
scripting. This function will raise if there is an error parsing `b`.
## 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 NDF>>"
```
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 NDF>>"
```
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 NDF>>"
```
"""
@spec add(a :: t(), b :: t() | Frames.t(), opts :: [round: maybe_round()]) :: t()
def add(a, b, opts \\ [])
def add(a, %__MODULE__{} = b, opts),
do: a.seconds |> Ratio.add(b.seconds) |> with_seconds!(a.rate, opts)
def add(a, b, opts), do: add(a, with_frames!(b, a.rate), opts)
@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.
`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 `a`. This is mostly to support quick
scripting. This function will raise if there is an error parsing `b`.
## 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 NDF>>"
```
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 NDF>>"
```
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 NDF>>"
```
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 NDF>>"
```
"""
@spec sub(a :: t(), b :: t() | Frames.t(), opts :: [round: maybe_round()]) :: t()
def sub(a, b, opts \\ [])
def sub(a, %__MODULE__{} = b, opts),
do: a.seconds |> Ratio.sub(b.seconds) |> with_seconds!(a.rate, opts)
def sub(a, b, opts), do: sub(a, with_frames!(b, a.rate), opts)
@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 NDF>>"
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 NDF>>"
```
"""
@spec mult(a :: t(), b :: Ratio.t() | number(), opts :: [round: maybe_round()]) :: t()
def mult(a, b, opts \\ []), do: a.seconds |> Ratio.mult(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 NDF>>"
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 NDF>>"
```
"""
@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(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 NDF>>, <00:00:00:01 <23.98 NTSC NDF>>}"
```
"""
@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) |> 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 NDF>>"
```
"""
@spec rem(
dividend :: t(),
divisor :: Ratio.t() | number(),
opts :: [round_frames: round(), round_remainder: round()]
) :: t()
def rem(dividend, divisor, opts \\ []), do: dividend |> divrem(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.negate(tc) |> inspect()
"<-01:00:00:00 <23.98 NTSC NDF>>"
```
```elixir
iex> tc = Timecode.with_frames!("-01:00:00:00", Rates.f23_98())
iex> Timecode.negate(tc) |> inspect()
"<01:00:00:00 <23.98 NTSC NDF>>"
```
"""
@spec negate(t()) :: t()
def negate(tc), do: %{tc | seconds: Ratio.negate(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 NDF>>"
```
```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 NDF>>"
```
"""
@spec abs(t()) :: t()
def abs(tc), do: %{tc | seconds: Ratio.abs(tc.seconds)}
@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
rate = timecode.rate
timebase = Framerate.timebase(rate)
frames_per_minute = Ratio.mult(timebase, Consts.seconds_per_minute())
frames_per_hour = Ratio.mult(timebase, Consts.seconds_per_hour())
total_frames =
timecode
|> frames(opts)
|> Kernel.abs()
|> then(&(&1 + DropFrame.frame_num_adjustment(&1, rate)))
{hours, remainder} = Rational.divrem(total_frames, frames_per_hour)
{minutes, remainder} = Rational.divrem(remainder, frames_per_minute)
{seconds, frames} = Rational.divrem(remainder, timebase)
%Sections{
negative?: timecode.seconds < 0,
hours: hours,
minutes: minutes,
seconds: seconds,
frames: Rational.round(frames, round)
}
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
sections = sections(timecode, opts)
sign = if Ratio.lt?(timecode.seconds, 0), do: "-", else: ""
frame_sep = if timecode.rate.ntsc == :drop, do: ";", else: ":"
[
sections.hours,
sections.minutes,
sections.seconds,
sections.frames
]
|> Enum.map(&render_tc_field/1)
|> Enum.intersperse(":")
|> then(&[sign | &1])
|> List.replace_at(-2, frame_sep)
|> List.to_string()
end
@spec render_tc_field(integer()) :: String.t()
defp render_tc_field(value),
do: value |> Integer.to_string() |> String.pad_leading(2, "0")
@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 NDF>> has a true runtime of
'01:00:03.6'
"""
@spec runtime(t(), integer()) :: String.t()
def runtime(timecode, precision \\ 9) do
{seconds, negative?} =
if Ratio.lt?(timecode.seconds, 0),
do: {Ratio.negate(timecode.seconds), true},
else: {timecode.seconds, false}
seconds = Decimal.div(Ratio.numerator(seconds), Ratio.denominator(seconds))
{hours, seconds} = Decimal.div_rem(seconds, Consts.seconds_per_hour())
{minutes, seconds} = Decimal.div_rem(seconds, Consts.seconds_per_minute())
Decimal.Context
seconds = Decimal.round(seconds, precision)
seconds_floor = Decimal.round(seconds, 0, :down)
fractal_seconds = Decimal.sub(seconds, seconds_floor)
hours = hours |> Decimal.to_integer() |> Integer.to_string() |> String.pad_leading(2, "0")
minutes = minutes |> Decimal.to_integer() |> Integer.to_string() |> String.pad_leading(2, "0")
seconds_floor =
seconds_floor |> Decimal.to_integer() |> Integer.to_string() |> String.pad_leading(2, "0")
fractal_seconds = runtime_render_fractal_seconds(fractal_seconds)
# We'll add a negative sign if the timecode is negative.
sign = if negative?, do: "-", else: ""
"#{sign}#{hours}:#{minutes}:#{seconds_floor}#{fractal_seconds}"
end
# Renders fractal seconds to a string.
@spec runtime_render_fractal_seconds(Decimal.t()) :: String.t()
defp runtime_render_fractal_seconds(seconds_fractal) do
rendered =
if Decimal.eq?(seconds_fractal, 0) do
""
else
Decimal.to_string(seconds_fractal)
|> String.trim_leading("0")
|> String.trim_trailing("0")
|> String.trim_trailing(".")
end
if rendered == "", do: ".0", else: rendered
end
@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.seconds |> Ratio.mult(Consts.ppro_tick_per_second()) |> Rational.round(round)
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 :: [round: round()]) :: String.t()
def feet_and_frames(timecode, opts \\ []) do
total_frames = timecode |> frames(opts) |> Kernel.abs()
feet = total_frames |> Kernel.div(Consts.frames_per_foot()) |> Integer.to_string()
frames =
total_frames
|> Kernel.rem(Consts.frames_per_foot())
|> Integer.to_string()
|> String.pad_leading(2, "0")
sign = if Ratio.lt?(timecode.seconds, 0), do: "-", else: ""
"#{sign}#{feet}+#{frames}"
end
# 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