Packages
vtc
0.3.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/sources.ex
defmodule Vtc.Source do
@moduledoc """
Protocols for source values that can be used to construct a timecode.
"""
alias Vtc.Timecode
alias Vtc.Utils.Rational
defprotocol Seconds do
@moduledoc """
Protocol which types can implement to be passed as the main value of
`Timecode.with_seconds/2`.
## Implementations
Out of the box, this protocol is implemented for the following types:
- `Ratio`
- `Integer`
- `Float`
- `String`
- runtime ("01:00:00.0")
- decimal ("3600.0")
"""
alias Vtc.Framerate
alias Vtc.Source
alias Vtc.Timecode
@typedoc """
Result type of `Source.Seconds.seconds/2`.
"""
@type result() :: {:ok, Rational.t()} | {:error, Timecode.ParseError.t()}
@doc """
Returns the value as a rational seconds value.
## Arguments
- **value**: The source value.
- **rate**: The framerate of the timecode being parsed.
## Returns
A result tuple with a rational representation of the seconds value using `Ratio` on
success.
"""
@spec seconds(t(), Framerate.t()) :: result()
def seconds(value, rate)
end
defimpl Seconds, for: [Ratio, Integer] do
alias Vtc.Framerate
alias Vtc.Private.Parse
alias Vtc.Source
alias Vtc.Utils.Rational
@spec seconds(Rational.t(), Framerate.t()) :: Seconds.result()
def seconds(value, rate), do: Parse.from_seconds_core(value, rate)
end
defimpl Seconds, for: Float do
alias Vtc.Framerate
alias Vtc.Source
@spec seconds(float(), Framerate.t()) :: Seconds.result()
def seconds(value, rate), do: value |> Ratio.new(1) |> Seconds.seconds(rate)
end
defimpl Seconds, for: [String, BitString] do
alias Vtc.Framerate
alias Vtc.Private.Parse
alias Vtc.Source
@spec seconds(String.t(), Framerate.t()) :: Seconds.result()
def seconds(value, rate), do: Parse.parse_runtime_string(value, rate)
end
defprotocol Frames do
@moduledoc """
Protocol which types can implement to be passed as the main value of
`Timecode.with_frames/2`.
## Implementations
Out of the box, this protocol is implemented for the following types:
- `Integer`
- `String` & 'BitString'
- timecode ("01:00:00:00")
- integer ("86400")
- Feet+Frames ("5400+00")
"""
alias Vtc.Framerate
alias Vtc.Source
alias Vtc.Timecode
@typedoc """
Result type of `Vtc.Source.Frames.frames/2 aaa`.
"""
@type result() :: {:ok, integer()} | {:error, Timecode.ParseError.t()}
@doc """
Returns the value as a frame count.
# Arguments
- **value**: The source value.
- **rate**: The framerate of the timecode being parsed.
# Returns
A result tuple with an integer value representing the frame count on success.
"""
@spec frames(t(), Framerate.t()) :: result()
def frames(value, rate)
end
defimpl Frames, for: Integer do
alias Vtc.Framerate
alias Vtc.Source.Frames
@spec frames(integer(), Framerate.t()) :: Frames.result()
def frames(value, _rate), do: {:ok, value}
end
defimpl Frames, for: [String, BitString] do
alias Vtc.Framerate
alias Vtc.Private.Parse
alias Vtc.Source.Frames
@spec frames(String.t(), Framerate.t()) :: Frames.result()
def frames(value, rate), do: Parse.parse_frames_string(value, rate)
end
defprotocol PremiereTicks do
@moduledoc """
Protocol which types can implement to be passed as the main value of
`Vtc.Timecode.with_premiere_ticks/2`.
# Implementations
Out of the box, this protocol is implemented for the following types:
- `Integer`
"""
@doc """
Returns the number of Adobe Premiere Pro ticks as an integer.
# Arguments
- **value**: The source value.
- **rate**: The framerate of the timecode being parsed.
# Returns
A result tuple with a rational representation of the seconds value using `Ratio` on
success.
"""
alias Vtc.Framerate
alias Vtc.Source
alias Vtc.Timecode
@typedoc """
Result type of `ticks/2`.
"""
@type result() :: {:ok, integer()} | {:error, Timecode.ParseError.t()}
@spec ticks(t(), Framerate.t()) :: result()
def ticks(value, rate)
end
defimpl PremiereTicks, for: Integer do
alias Vtc.Framerate
alias Vtc.Source
@spec ticks(integer(), Framerate.t()) :: PremiereTicks.result()
def ticks(value, _rate), do: {:ok, value}
end
end
defmodule Vtc.Private.Parse do
@moduledoc false
use Ratio
alias Vtc.Framerate
alias Vtc.Private.Consts
alias Vtc.Private.DropFrame
alias Vtc.Source
alias Vtc.Source.Frames
alias Vtc.Source.Seconds
alias Vtc.Timecode
alias Vtc.Utils.Rational
@spec from_seconds_core(Rational.t(), Framerate.t()) :: Seconds.result()
def from_seconds_core(input, rate) do
# If the vaue doesn't cleany divide into the framerate then we need to round to the
# nearest frame.
case Ratio.div(input, rate.playback) do
%Ratio{} ->
frames = rate.playback |> Ratio.mult(input) |> Rational.round()
{:ok, Ratio.div(frames, rate.playback)}
_ ->
{:ok, input}
end
end
@spec parse_frames_string(String.t(), Framerate.t()) :: Frames.result()
def parse_frames_string(value, rate) do
case parse_tc_string(value, rate) do
{:ok, _} = result -> result
{:error, %Timecode.ParseError{reason: :bad_drop_frames}} = error -> error
{:error, _} -> parse_feet_and_frames(value, rate)
end
end
@tc_regex ~r/^(?P<negative>-)?((?P<section_1>[0-9]+)[:|;])?((?P<section_2>[0-9]+)[:|;])?((?P<section_3>[0-9]+)[:|;])?(?P<frames>[0-9]+)$/
@spec parse_tc_string(String.t(), Framerate.t()) :: Frames.result()
def parse_tc_string(value, rate) do
with {:ok, matched} <- apply_regex(@tc_regex, value) do
matched
|> tc_matched_to_sections()
|> tc_sections_to_frames(rate)
end
end
@spec apply_regex(Regex.t(), String.t()) :: {:ok, map()} | {:error, Timecode.ParseError.t()}
defp apply_regex(regex, value) do
regex
|> Regex.named_captures(value)
|> then(fn
matched when is_map(matched) -> {:ok, matched}
nil -> {:error, %Timecode.ParseError{reason: :unrecognized_format}}
end)
end
# Extract TC sections from regex match.
@spec tc_matched_to_sections(map()) :: Timecode.Sections.t()
defp tc_matched_to_sections(matched) do
negative? = Map.fetch!(matched, "negative") == "-"
sections = extract_time_sections(matched, 3)
{seconds, sections} = pop_time_section(sections)
{minutes, sections} = pop_time_section(sections)
{hours, _} = pop_time_section(sections)
frames = matched |> Map.fetch!("frames") |> String.to_integer()
%Timecode.Sections{
negative?: negative?,
hours: hours,
minutes: minutes,
seconds: seconds,
frames: frames
}
end
# Extracts a set of sections in a time string of format xx:yy:.. that may or may not
# be truncated at the head.
#
# The regex matches are expected to have a series of fields like "section_1",
# "section_2", etc that denote present sections whose meaning depends on the number
# of sections present.
@spec extract_time_sections(map(), non_neg_integer()) :: [String.t()]
defp extract_time_sections(regex_matches, section_count) do
1..section_count
|> Enum.map(&Integer.to_string/1)
|> Enum.reduce([], fn section_index, sections ->
case Map.fetch!(regex_matches, "section_#{section_index}") do
"" -> sections
this_section -> [this_section | sections]
end
end)
end
# Pops the next section at the end of the list and returns it as an integer.
#
# Returns `0` if the value is not present
@spec pop_time_section([String.t()]) :: {integer(), [String.t()]}
defp pop_time_section(["" | remaining]), do: {0, remaining}
defp pop_time_section([value | remaining]), do: {String.to_integer(value), remaining}
defp pop_time_section([]), do: {0, []}
# Converts all TC fields to a total frame count
@spec tc_sections_to_frames(Timecode.Sections.t(), Framerate.t()) :: Frames.result()
defp tc_sections_to_frames(sections, rate) do
with {:ok, adjustment} <- DropFrame.parse_adjustment(sections, rate) do
frames_per_second = Framerate.timebase(rate)
sections.seconds
|> Ratio.add(sections.minutes * Consts.seconds_per_minute())
|> Ratio.add(sections.hours * Consts.seconds_per_hour())
|> Ratio.mult(frames_per_second)
|> Ratio.add(sections.frames)
|> Ratio.add(adjustment)
|> Rational.round()
|> then(fn frames -> if sections.negative?, do: -frames, else: frames end)
|> then(&{:ok, &1})
end
end
@ff_regex ~r/(?P<negative>-)?(?P<feet>[0-9]+)\+(?P<frames>[0-9]+)/
@spec parse_feet_and_frames(String.t(), Framerate.t()) :: Frames.result()
defp parse_feet_and_frames(value, rate) do
with {:ok, groups} <- apply_regex(@ff_regex, value) do
negative? = Map.fetch!(groups, "negative") == "-"
feet = groups |> Map.fetch!("feet") |> String.to_integer()
groups
|> Map.fetch!("frames")
|> String.to_integer()
|> Ratio.add(feet * Consts.frames_per_foot())
|> then(fn frames -> if negative?, do: -frames, else: frames end)
|> Frames.frames(rate)
end
end
@runtime_regex ~r/^(?P<negative>-)?((?P<section_1>[0-9]+)[:|;])?((?P<section_2>[0-9]+)[:|;])?(?P<seconds>[0-9]+(\.[0-9]+)?)$/
@spec parse_runtime_string(String.t(), Framerate.t()) :: Seconds.result()
def parse_runtime_string(value, rate) do
with {:ok, matched} <- apply_regex(@runtime_regex, value) do
matched
|> runtime_matched_to_second()
|> Source.Seconds.seconds(rate)
end
end
@spec runtime_matched_to_second(map()) :: Rational.t()
defp runtime_matched_to_second(matched) do
negative? = Map.fetch!(matched, "negative") == "-"
sections = extract_time_sections(matched, 2)
{minutes, sections} = pop_time_section(sections)
{hours, _} = pop_time_section(sections)
matched
|> Map.fetch!("seconds")
|> Decimal.new()
|> Ratio.new(1)
|> Ratio.add(hours * Consts.seconds_per_hour())
|> Ratio.add(minutes * Consts.seconds_per_minute())
|> then(fn seconds -> if negative?, do: -seconds, else: seconds end)
end
end