Packages
vtc
0.2.6
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/framerate.ex
defmodule Vtc.Framerate do
@moduledoc """
The rate at which a video file frames are played back.
Framerate is measured in frames-per-second (24/1 = 24 frames-per-second).
"""
alias Vtc.Utils.Rational
@enforce_keys [:playback, :ntsc]
defstruct [:playback, :ntsc]
@typedoc """
Enum of `Ntsc` types.
## Values
- `nil`: Not an NTSC value
- `:non_drop` A non-drop NTSC value.
- `:drop` A drop-frame ntsc value.
For more information on NTSC standards and framerate conventions, see
[Frame.io's](frame.io)
[blogpost](https://blog.frame.io/2017/07/17/timecode-and-frame-rates) on the subject.
"""
@type ntsc() :: :non_drop | :drop | nil
@typedoc """
Type of `Framerate`
## Fields
- **playback**: The rational representation of the real-world playback speed as a
fraction in frames-per-second.
- **ntsc**: Atom representing which, if any, NTSC convention this framerate adheres to.
"""
@type t :: %__MODULE__{playback: Rational.t(), ntsc: ntsc()}
@doc """
The rational representation of the timecode timebase speed as a fraction in
frames-per-second.
"""
@spec timebase(t()) :: Rational.t()
def timebase(%__MODULE__{ntsc: nil} = framerate), do: framerate.playback
def timebase(framerate), do: Rational.round(framerate.playback)
defmodule ParseError do
@moduledoc """
Exception returned when a framerate cannot be parsed.
"""
defexception [:reason]
@typedoc """
Type of `ParseError`
## Fields
- **reason**: The reason the error occurred must be one of the following:
- `:bad_drop_rate`: Returned when the playback speed of a framerate with an ntsc
value of :drop is not divisible by 3000/1001 (29.97), for more on why drop-frame
framerates must be a multiple of 29.97, see:
https://www.davidheidelberger.com/2010/06/10/drop-frame-timecode/
- `:invalid_ntsc`: Returned when the ntsc value is not one of the allowed atom
values.
- `:unrecognized_format`: Returned when a string value is not a recognized format.
- `:imprecise` - Returned when a float was passed with an NTSC value of nil.
Without the ability to round to the nearest valid NTSC value, floats are not
precise enough to build an arbitrary framerate.
"""
@type t() :: %__MODULE__{
reason: :bad_drop_rate | :invalid_ntsc | :unrecognized_format | :imprecise
}
@doc """
Returns a message for the error reason.
"""
@spec message(t()) :: String.t()
def message(%__MODULE__{reason: :bad_drop_rate}),
do: "drop-frame rates must be divisible by 30000/1001"
def message(%__MODULE__{reason: :invalid_ntsc}),
do: "ntsc is not a valid atom. must be :non_drop, :drop, or nil"
def message(%__MODULE__{reason: :unrecognized_format}),
do: "framerate string format not recognized"
def message(%__MODULE__{reason: :imprecise}),
do: "non-whole floats are not precise enough to create a non-NTSC Framerate"
end
@typedoc """
Type returned by `new/2`
"""
@type parse_result() :: {:ok, t()} | {:error, ParseError.t()}
@doc """
Creates a new Framerate with a playback speed or timebase.
## Arguments
- **rate**: Either the playback rate or timebase. For NTSC framerates, the value will
be rounded to the nearest correct value.
- **ntsc**: Atom representing the which (or whether an) NTSC standard is being used.
- **coerce_seconds_per_frame?**: If `true`, then values such as `1/24` are assumed to be
in seconds-per-frame format and automatically converted to `24/1`. Useful when you want
to convert strings from multiple sources when some are seconds-per-frame and others are
frames-per-second. NOTE: if you expect to be dealing with record-rate values for timelapse
use at your own risk!
NOTE: Floats cannot be passed if the rate is not NTSC and the value is not a while
number, as there is no way to know the precise time do to floating-point errors.
"""
@spec new(Rational.t() | float() | String.t(), ntsc(), boolean()) :: parse_result()
def new(rate, ntsc, coerce_seconds_per_frame? \\ true)
def new(rate, nil, _)
when is_float(rate) and rate != Kernel.floor(rate),
do: {:error, %ParseError{reason: :imprecise}}
def new(rate, ntsc, coerce?)
when is_float(rate) or is_integer(rate) or is_struct(rate, Ratio),
do: rate |> Ratio.new(1) |> new_core(ntsc, coerce?)
def new(rate, ntsc, coerce?) when is_binary(rate) do
# for binaries we need to try to match integer, float, and rational string
# representations
parsers = [
&Integer.parse/1,
&Float.parse/1,
&parse_rational_string/1
]
parsers
|> Stream.map(fn parser -> parser.(rate) end)
|> Enum.find_value(:error, fn
{parsed, ""} -> parsed
_ -> false
end)
|> then(fn
:error -> {:error, %ParseError{reason: :unrecognized_format}}
value -> new(value, ntsc, coerce?)
end)
end
@doc """
As `new/2` but raises an error instead.
"""
@spec new!(Rational.t() | float() | String.t(), ntsc(), boolean()) :: t()
def new!(rate, ntsc, coerce_seconds_per_frame? \\ true) do
case new(rate, ntsc, coerce_seconds_per_frame?) do
{:ok, framerate} -> framerate
{:error, error} -> raise error
end
end
# validates that a rate is a proper drop-frame framerate.
@spec validate_drop(Ratio.t(), ntsc()) :: :ok | {:error, ParseError.t()}
defp validate_drop(rate, :drop) do
case Ratio.div(rate, Ratio.new(30_000, 1_001)) do
whole_number when is_integer(whole_number) -> :ok
_ -> {:error, %ParseError{reason: :bad_drop_rate}}
end
end
defp validate_drop(_, _), do: :ok
# The core parser used to parse a rational or integer rate value.
@spec new_core(Rational.t(), ntsc(), boolean()) :: parse_result()
defp new_core(rate, ntsc, coerce_seconds_per_frame?) do
# validate that our ntsc atom is one of the acceptable values.
with :ok <- validate_ntsc(ntsc),
rate <- coerce_seconds_per_frame(rate, coerce_seconds_per_frame?),
rate <- coerce_ntsc_rate(rate, ntsc),
:ok <- validate_drop(rate, ntsc) do
{:ok, %__MODULE__{playback: rate, ntsc: ntsc}}
end
end
# validates that the ntsc atom is one of our allowed values.
@spec validate_ntsc(ntsc()) :: :ok | {:error, ParseError.t()}
defp validate_ntsc(ntsc) when ntsc in [:drop, :non_drop, nil], do: :ok
defp validate_ntsc(_), do: {:error, %ParseError{reason: :invalid_ntsc}}
# coerces a rate to the closest proper NTSC playback rate.
@spec coerce_ntsc_rate(Ratio.t(), ntsc()) :: Ratio.t()
defp coerce_ntsc_rate(rate, nil), do: rate
defp coerce_ntsc_rate(%Ratio{denominator: 1001} = rate, _), do: rate
defp coerce_ntsc_rate(rate, _),
do: rate |> Rational.round() |> Ratio.mult(Ratio.new(1000, 1001))
# Coerces timebase to framerate by flipping the numberator and denominator.
@spec coerce_seconds_per_frame(Rational.t(), boolean()) :: Rational.t()
defp coerce_seconds_per_frame(%Ratio{numerator: x, denominator: y}, true)
when x < y,
do: Ratio.new(y, x)
defp coerce_seconds_per_frame(rate, _), do: rate
# Parses a rational string value like '24/1'. Conforms to the same API as
# `Integer.parse/1` and `Float.parse/1`.
@spec parse_rational_string(String.t()) :: {Rational.t(), String.t()} | :error
defp parse_rational_string(binary) do
case String.split(binary, "/") do
[_, _] = split ->
split
|> Enum.map(&String.to_integer/1)
|> then(fn [x, y] -> {Ratio.new(x, y), ""} end)
_ ->
:error
end
end
@doc """
Returns true if the value represents and NTSC framerate, therefore will return true
on a Framerate with an `:ntsc` value of `:non_drop` and `:drop`.
"""
@spec ntsc?(t()) :: boolean()
def ntsc?(%__MODULE__{ntsc: nil}), do: false
def ntsc?(_), do: true
@doc """
Example returns:
- 23.98 NTSC DF
- 23.98 NTSC NDF
- 23.98 fps
"""
@spec to_string(t()) :: String.t()
def to_string(rate) do
float_str =
Ratio.to_float(rate.playback)
|> Float.round(2)
|> Float.to_string()
ntsc_string = if ntsc?(rate), do: " NTSC", else: " fps"
drop_string =
case rate.ntsc do
:non_drop -> " NDF"
:drop -> " DF"
nil -> ""
end
"<#{float_str}#{ntsc_string}#{drop_string}>"
end
end
defimpl Inspect, for: Vtc.Framerate do
alias Vtc.Framerate
@spec inspect(Framerate.t(), Elixir.Inspect.Opts.t()) :: String.t()
def inspect(rate, _opts), do: Framerate.to_string(rate)
end
defimpl String.Chars, for: Vtc.Framerate do
alias Vtc.Framerate
@spec to_string(Framerate.t()) :: String.t()
def to_string(term), do: Framerate.to_string(term)
end