Packages
vtc
0.1.2
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 `Vtc.Timecode.with_seconds/2` and
`Vtc.Timecode.with_frames/2`
"""
use Ratio, comparison: true
@enforce_keys [:seconds, :rate]
defstruct [:seconds, :rate]
@typedoc """
`Vtc.Timecode` type.
# 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.
"""
@type t :: %Vtc.Timecode{seconds: Ratio.t() | integer, rate: Vtc.Framerate.t()}
defmodule Sections do
@moduledoc """
Holds the individual sections of a timecode for formatting / manipulation.
"""
@enforce_keys [:negative, :hours, :minutes, :seconds, :frames]
defstruct [:negative, :hours, :minutes, :seconds, :frames]
@typedoc """
The type of Vtc.Timecode.Sections.
# Fields
- **negative**: Whether the timecode is less than 0.
- **hours**: Hours place value.
- **minutes**: Minutes place value.
- **seconds**: Seconds place value.
- **frames**: Frames place value.
"""
@type t :: %Sections{
negative: boolean,
hours: integer,
minutes: integer,
seconds: integer,
frames: integer
}
end
@doc """
Returns the number of frames that would have elapsed between 00:00:00:00 and this
timecode.
# 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(Vtc.Timecode.t()) :: integer
def frames(tc = %Vtc.Timecode{}) do
Private.round_ratio?(tc.seconds * tc.rate.playback)
end
@doc """
The individual sections of a timecode string as i64 values.
"""
@spec sections(Vtc.Timecode.t()) :: Sections.t()
def sections(tc = %Vtc.Timecode{}) do
timebase = Vtc.Framerate.timebase(tc.rate)
framesPerMinute = timebase * Private.secondsPerMinute()
framesPerHour = timebase * Private.secondsPerHour()
is_negative = tc.seconds < 0
frames = abs(frames(tc))
{hours, frames} = Private.divmod(frames, framesPerHour)
{minutes, frames} = Private.divmod(frames, framesPerMinute)
{seconds, frames} = Private.divmod(frames, timebase)
frames = Private.round_ratio?(frames)
%Sections{
negative: is_negative,
hours: hours,
minutes: minutes,
seconds: seconds,
frames: frames
}
end
@doc """
Returns the the formatted SMPTE timecode: (ex: 01:00:00:00).
# 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(Vtc.Timecode.t()) :: String.t()
def timecode(tc = %Vtc.Timecode{}) do
sections = sections(tc)
# We'll add a negative sign if the timecode is negative.
sign =
if tc.seconds < 0 do
"-"
else
""
end
# If this is a drop-frame timecode, we need to use a ';' to separate the frames
# from the seconds.
frame_sep =
if tc.rate.ntsc == :Drop do
";"
else
":"
end
hours = sections.hours |> Integer.to_string() |> String.pad_leading(2, "0")
minutes = sections.minutes |> Integer.to_string() |> String.pad_leading(2, "0")
seconds = sections.seconds |> Integer.to_string() |> String.pad_leading(2, "0")
frames = sections.frames |> Integer.to_string() |> String.pad_leading(2, "0")
"#{sign}#{hours}:#{minutes}:#{seconds}#{frame_sep}#{frames}"
end
defmodule ParseError do
@moduledoc """
Exception returned when there is an error parsing a Timecode value.
"""
defexception [:reason]
@typedoc """
Type of `Vtc.Timecode.ParseError`
# Fields
- `:reason`: The reason the error occurred must be one of the following:
- `:unrecognized_format`: Returned when a string value is not a recognized
timecode, runtime, etc. format.
"""
@type t :: %ParseError{reason: :unrecognized_format}
@doc """
Returns a message for the error reason.
"""
@spec message(Vtc.Framerate.ParseError.t()) :: String.t()
def message(error) do
case error.reason do
:unrecognized_format -> "string format not recognized"
end
end
end
@typedoc """
Type returned by `Vtc.Timecode.with_seconds/2` and `Vtc.Timecode.with_frames/2`.
"""
@type parse_result :: {:ok, Vtc.Timecode.t()} | {:error, ParseError.t()}
@doc """
Returns a new `Vtc.Timecode` with a Vtc.Timecode.seconds field value equal to the
seconds arg.
Timecode::with_frames takes many different formats (more than just numeric types) that
represent the frame count of the timecode.
# Arguments
- `seconds` - A value which can be represented as a number of seconds.
- `rate` - The Framerate at which the frames are being played back.
"""
@spec with_seconds(Vtc.Sources.Seconds.t(), Vtc.Framerate.t()) :: parse_result
def with_seconds(seconds, %Vtc.Framerate{} = rate) do
case Vtc.Sources.Seconds.seconds(seconds, rate) do
{:ok, seconds} -> {:ok, %Vtc.Timecode{seconds: seconds, rate: rate}}
{:error, err} -> {:error, err}
end
end
@doc """
As `Vtc.Timecode.with_seconds/2`, but raises on error.
"""
@spec with_seconds!(Vtc.Sources.Seconds.t(), Vtc.Framerate.t()) :: Vtc.Timecode.t()
def with_seconds!(seconds, %Vtc.Framerate{} = rate) do
{:ok, tc} = with_seconds(seconds, rate)
tc
end
@doc """
Returns a new `Vtc.Timecode` with a `Vtc.Timecode.frames/1` return value equal to the
frames arg.
Timecode::with_frames takes many different formats (more than just numeric types) that
represent the frame count of the timecode.
# Arguments
- `frames` - A value which can be represented as a frame number / frame count.
- `rate` - The Framerate at which the frames are being played back.
"""
@spec with_frames(Vtc.Sources.Frames.t(), Vtc.Framerate.t()) :: parse_result
def with_frames(frames, %Vtc.Framerate{} = rate) do
case Vtc.Sources.Frames.frames(frames, rate) do
{:ok, frames} ->
seconds = frames / rate.playback
with_seconds(seconds, rate)
{:error, err} ->
{:error, err}
end
end
@doc """
As `Vtc.Timecode.with_frames/2`, but raises on error.
"""
@spec with_frames!(Vtc.Sources.Frames.t(), Vtc.Framerate.t()) :: Vtc.Timecode.t()
def with_frames!(frames, %Vtc.Framerate{} = rate) do
{:ok, tc} = with_frames(frames, rate)
tc
end
end
defimpl Inspect, for: Vtc.Timecode do
def inspect(tc, opts) do
tc_str = Vtc.Timecode.timecode(tc)
rate_str = Inspect.inspect(tc.rate, opts)
"<#{tc_str} @ #{rate_str}>"
end
end