defmodule Vtc.Source.Frames.TimecodeStr do @moduledoc """ Implementation of [Frames](`Vtc.Source.Frames`) for timecode string. See `Vtc.Timecode.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.Timecode @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_timecode(Timecode.t(), opts :: [round: Timecode.round()]) :: t() def from_timecode(timecode, opts) do sections = Timecode.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() |> 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.TimecodeStr do @moduledoc """ Implements [Seconds](`Vtc.Source.Seconds`) protocol for Premiere ticks. """ alias Vtc.Framerate alias Vtc.Source.Frames alias Vtc.Source.Frames.TimecodeStr alias Vtc.Timecode alias Vtc.Utils.Consts alias Vtc.Utils.DropFrame alias Vtc.Utils.Parse alias Vtc.Utils.Rational @tc_regex ~r/^(?P-)?((?P[0-9]+)[:|;])?((?P[0-9]+)[:|;])?((?P[0-9]+)[:|;])?(?P[0-9]+)$/ @spec frames(TimecodeStr.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()) :: Timecode.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() %Timecode.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(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) 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