defmodule Vtc.Source.Frames.SMPTETimecodeStr do
@moduledoc """
Implementation of [Frames](`Vtc.Source.Frames`) for timecode string. See
`Vtc.Framestamp.smpte_timecode/2` for more information on this format.
This struct is used as an input wrapper only, not as the general-purpose Premiere
ticks unit.
By default, this wrapper does not need to be used by callers, as the string
implementation of the frames protocol calls this type's impl automatically. Only use
this type if you do not wish for the parser to fall back to feet+frames parsing as
well.
"""
alias Vtc.Framestamp
@enforce_keys [:in]
defstruct [:in]
@typedoc """
Contains only a single field for wrapping the underlying string.
"""
@type t() :: %__MODULE__{in: String.t()}
@doc false
@spec from_framestamp(Framestamp.t(), opts :: [round: Framestamp.round()]) :: t()
def from_framestamp(framestamp, opts) do
sections = Framestamp.smpte_timecode_sections(framestamp, opts)
sign = if Ratio.lt?(framestamp.seconds, 0), do: "-", else: ""
frame_sep = if framestamp.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()
|> then(&%__MODULE__{in: &1})
end
@spec render_tc_field(integer()) :: String.t()
defp render_tc_field(value), do: value |> Integer.to_string() |> String.pad_leading(2, "0")
end
defimpl Vtc.Source.Frames, for: Vtc.Source.Frames.SMPTETimecodeStr do
@moduledoc """
Implements [Seconds](`Vtc.Source.Seconds`) protocol for Premiere ticks.
"""
alias Vtc.Framerate
alias Vtc.SMPTETimecode
alias Vtc.Source.Frames
alias Vtc.Source.Frames.SMPTETimecodeStr
alias Vtc.Utils.Consts
alias Vtc.Utils.DropFrame
alias Vtc.Utils.Parse
alias Vtc.Utils.Rational
@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 frames(SMPTETimecodeStr.t(), Framerate.t()) :: Frames.result()
def frames(tc_str, rate) do
with {:ok, matched} <- Parse.apply_regex(@tc_regex, tc_str.in) do
matched
|> tc_matched_to_sections()
|> tc_sections_to_frames(rate)
end
end
# Extract TC sections from regex match.
@spec tc_matched_to_sections(map()) :: SMPTETimecode.Sections.t()
defp tc_matched_to_sections(matched) do
negative? = Map.fetch!(matched, "negative") == "-"
sections = Parse.extract_time_sections(matched, 3)
{seconds, sections} = Parse.pop_time_section(sections)
{minutes, sections} = Parse.pop_time_section(sections)
{hours, _} = Parse.pop_time_section(sections)
frames = matched |> Map.fetch!("frames") |> String.to_integer()
%SMPTETimecode.Sections{
negative?: negative?,
hours: hours,
minutes: minutes,
seconds: seconds,
frames: frames
}
end
# Converts all TC fields to a total frame count
@spec tc_sections_to_frames(SMPTETimecode.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.smpte_timebase(rate)
seconds_for_minutes = Ratio.new(sections.minutes * Consts.seconds_per_minute())
seconds_for_hours = Ratio.new(sections.hours * Consts.seconds_per_hour())
frames_ratio = Ratio.new(sections.frames)
sections.seconds
|> Ratio.new()
|> Ratio.add(seconds_for_minutes)
|> Ratio.add(seconds_for_hours)
|> Ratio.mult(frames_per_second)
|> Ratio.add(frames_ratio)
|> Ratio.add(Ratio.new(adjustment))
|> Rational.round()
|> then(fn frames -> if sections.negative?, do: -frames, else: frames end)
|> then(&{:ok, &1})
end
end
end