class Timecode

Constants

DEFAULT_FPS
VERSION

Public Class Methods

add_custom_framerate!(rate) click to toggle source

Use this to add a custom framerate

# File lib/timecode.rb, line 123
def add_custom_framerate!(rate)
  @custom_framerates ||= []
  @custom_framerates.push(rate)
end
at(hrs, mins, secs, frames, with_fps = DEFAULT_FPS, drop_frame = false) click to toggle source

Initialize a Timecode object at this specfic timecode

# File lib/timecode.rb, line 209
def at(hrs, mins, secs, frames, with_fps = DEFAULT_FPS, drop_frame = false)
  validate_atoms!(hrs, mins, secs, frames, with_fps)
  comp = ComputationValues.new(with_fps, drop_frame)
  if drop_frame && secs == 0 && (mins % 10) && (frames < comp.drop_count)
    frames = comp.drop_count
  end
  
  total = hrs * comp.frames_per_hour
  if drop_frame
    total += (mins / 10) * comp.frames_per_10_min
    total += (mins % 10) * comp.frames_per_min
  else
    total += mins * comp.frames_per_min
  end
  rounded_base = with_fps.round
  total += secs * rounded_base
  total += frames
  new(total, with_fps, drop_frame)
end
check_framerate!(fps) click to toggle source

Check the passed framerate and raise if it is not in the list

# File lib/timecode.rb, line 129
def check_framerate!(fps)
  unless supported_framerates.include?(fps)
    supported = "%s and %s are supported" % [supported_framerates[0..-2].join(", "), supported_framerates[-1]]
    raise WrongFramerate, "Framerate #{fps} is not in the list of supported framerates (#{supported})"
  end
end
from_filename_in_sequence(filename_with_or_without_path, fps = DEFAULT_FPS) click to toggle source

Parses the timecode contained in a passed filename as frame number in a sequence

# File lib/timecode.rb, line 147
def from_filename_in_sequence(filename_with_or_without_path, fps = DEFAULT_FPS)
  b = File.basename(filename_with_or_without_path)
  number = b.scan(/\d+/).flatten[-1].to_i
  new(number, fps)
end
from_seconds(seconds_float, the_fps = DEFAULT_FPS, drop_frame = false) click to toggle source

create a timecode from the number of seconds. This is how current time is supplied by QuickTime and other systems which have non-frame-based timescales

# File lib/timecode.rb, line 275
def from_seconds(seconds_float, the_fps = DEFAULT_FPS, drop_frame = false)
  total_frames = (seconds_float.to_f * the_fps.to_f).round.to_i
  new(total_frames, the_fps, drop_frame)
end
from_uint(uint, fps = DEFAULT_FPS) click to toggle source

Some systems (like SGIs) and DPX format store timecode as unsigned integer, bit-packed. This method unpacks such an integer into a timecode.

# File lib/timecode.rb, line 282
def from_uint(uint, fps = DEFAULT_FPS)
  tc_elements = (0..7).to_a.reverse.map do | multiplier |
    ((uint >> (multiplier * 4)) & 0x0F)
  end.join.scan(/(\d{2})/).flatten.map{|e| e.to_i}

  tc_elements << fps
  at(*tc_elements)
end
new(total = 0, fps = DEFAULT_FPS, drop_frame = false) click to toggle source

Initialize a new Timecode object with a certain amount of frames, a framerate and an optional drop frame flag will be interpreted as the total number of frames

# File lib/timecode.rb, line 94
def initialize(total = 0, fps = DEFAULT_FPS, drop_frame = false)
  raise WrongFramerate, "FPS cannot be zero" if fps.zero?
  self.class.check_framerate!(fps)
  # If total is a string, use parse
  raise RangeError, "Timecode cannot be negative" if total.to_i < 0
  # Always cast framerate to float, and num of frames to integer
  @total, @fps = total.to_i, fps.to_f
  @drop_frame = drop_frame
  @value = validate!
  freeze
end
new(from = nil, fps = DEFAULT_FPS, drop_frame = false) click to toggle source

Use initialize for integers and parsing for strings

Calls superclass method
# File lib/timecode.rb, line 137
def new(from = nil, fps = DEFAULT_FPS, drop_frame = false)
  from.is_a?(String) ? parse(from, fps) : super(from, fps, drop_frame)
end
parse(spaced_input, with_fps = DEFAULT_FPS) click to toggle source

Parse timecode entered by the user. Will raise if the string cannot be parsed. The following formats are supported:

  • 10h 20m 10s 1f (or any combination thereof) - will be disassembled to hours, frames, seconds and so on automatically

  • 123 - will be parsed as 00:00:01:23

  • 00:00:00:00 - will be parsed as zero TC

# File lib/timecode.rb, line 158
def parse(spaced_input, with_fps = DEFAULT_FPS)
  input = spaced_input.strip

  # 00:00:00;00
  if (input =~ DF_TC_RE)
    atoms_and_fps = input.scan(DF_TC_RE).to_a.flatten.map{|e| e.to_i} + [with_fps, true]
    return at(*atoms_and_fps)
  # 00:00:00:00
  elsif (input =~ COMPLETE_TC_RE)
    atoms_and_fps = input.scan(COMPLETE_TC_RE).to_a.flatten.map{|e| e.to_i} + [with_fps]
    return at(*atoms_and_fps)
  # 00:00:00+00
  elsif (input =~ COMPLETE_TC_RE_24)
    atoms_and_fps = input.scan(COMPLETE_TC_RE_24).to_a.flatten.map{|e| e.to_i} + [24]
    return at(*atoms_and_fps)
  # 00:00:00.0
  elsif input =~ FRACTIONAL_TC_RE
    parse_with_fractional_seconds(input, with_fps)
  # 00:00:00:000
  elsif input =~ TICKS_TC_RE
    parse_with_ticks(input, with_fps)
  # 10h 20m 10s 1f 00:00:00:01 - space separated is a sum of parts
  elsif input =~ /\s/
    parts = input.gsub(/\s/, ' ').split.reject{|e| e.strip.empty? }
    raise CannotParse, "No atoms" if parts.empty?
    parts.map{|part|  parse(part, with_fps) }.inject{|sum, p| sum + p.total }
  # 10s
  elsif input =~ /^(\d+)s$/
    return new(input.to_i * with_fps, with_fps)
  # 10h
  elsif input =~ /^(\d+)h$/i
    return new(input.to_i * 60 * 60 * with_fps, with_fps)
  # 20m
  elsif input =~ /^(\d+)m$/i
    return new(input.to_i * 60 * with_fps, with_fps)
  # 60f - 60 frames, or 2 seconds and 10 frames
  elsif input =~ /^(\d+)f$/i
    return new(input.to_i, with_fps)
  # Only a bunch of digits, treat 12345 as 00:01:23:45
  elsif (input =~ /^(\d+)$/)
    atoms_len = 2 * 4
    # left-pad input AND truncate if needed
    padded = input[0..atoms_len].rjust(8, "0")
    atoms = padded.scan(/(\d{2})/).flatten.map{|e| e.to_i } + [with_fps]
    return at(*atoms)
  else
    raise CannotParse, "Cannot parse #{input} into timecode, unknown format"
  end
end
parse_with_fractional_seconds(tc_with_fractions_of_second, fps = DEFAULT_FPS) click to toggle source

Parse a timecode with fractional seconds instead of frames. This is how ffmpeg reports a timecode

# File lib/timecode.rb, line 245
def parse_with_fractional_seconds(tc_with_fractions_of_second, fps = DEFAULT_FPS)
  fraction_expr = /[\.,](\d+)$/
  fraction_part = ('.' + tc_with_fractions_of_second.scan(fraction_expr)[0][0]).to_f

  seconds_per_frame = 1.0 / fps.to_f
  frame_idx = (fraction_part / seconds_per_frame).floor

  tc_with_frameno = tc_with_fractions_of_second.gsub(fraction_expr, ":%02d" % frame_idx)

  parse(tc_with_frameno, fps)
end
parse_with_ticks(tc_with_ticks, fps = DEFAULT_FPS) click to toggle source

Parse a timecode with ticks of a second instead of frames. A 'tick' is defined as 4 msec and has a range of 0 to 249. This format can show up in subtitle files for digital cinema used by CineCanvas systems

# File lib/timecode.rb, line 260
def parse_with_ticks(tc_with_ticks, fps = DEFAULT_FPS)
  ticks_expr = /(\d{3})$/
  num_ticks = tc_with_ticks.scan(ticks_expr).join.to_i

  raise RangeError, "Invalid tick count #{num_ticks}" if num_ticks > 249

  seconds_per_frame = 1.0 / fps
  frame_idx = ( (num_ticks * 0.004) / seconds_per_frame ).floor
  tc_with_frameno = tc_with_ticks.gsub(ticks_expr, "%02d" % frame_idx)

  parse(tc_with_frameno, fps)
end
soft_parse(input, with_fps = DEFAULT_FPS) click to toggle source

Parse timecode and return zero if none matched

# File lib/timecode.rb, line 142
def soft_parse(input, with_fps = DEFAULT_FPS)
  parse(input) rescue new(0, with_fps)
end
supported_framerates() click to toggle source

Returns the list of supported framerates for this subclass of Timecode

# File lib/timecode.rb, line 118
def supported_framerates
  STANDARD_RATES + (@custom_framerates || [])
end
validate_atoms!(hrs, mins, secs, frames, with_fps) click to toggle source

Validate the passed atoms for the concrete framerate

# File lib/timecode.rb, line 230
def validate_atoms!(hrs, mins, secs, frames, with_fps)
  case true
  when hrs > 999
      raise RangeError, "There can be no more than 999 hours, got #{hrs}"
    when mins > 59
      raise RangeError, "There can be no more than 59 minutes, got #{mins}"
    when secs > 59
      raise RangeError, "There can be no more than 59 seconds, got #{secs}"
    when frames >= with_fps
      raise RangeError, "There can be no more than #{with_fps} frames @#{with_fps}, got #{frames}"
  end
end

Public Instance Methods

*(arg) click to toggle source

Multiply the timecode by a number

# File lib/timecode.rb, line 434
def *(arg)
  raise RangeError, "Timecode multiplier cannot be negative" if (arg < 0)
  self.class.new(@total*arg.to_i, @fps, @drop_frame)
end
+(arg) click to toggle source

add number of frames (or another timecode) to this one

# File lib/timecode.rb, line 398
def +(arg)
  if (arg.is_a?(Timecode) && framerate_in_delta(arg.fps, @fps) && (arg.drop? == @drop_frame))
    self.class.new(@total + arg.total, @fps, @drop_frame)
  elsif (arg.is_a?(Timecode))
    if (arg.drop? != @drop_frame)
      raise WrongDropFlag, "You are calculating timecodes with different drop flag values"
    else
      raise WrongFramerate, "You are calculating timecodes with different framerates"
    end      
  else
    self.class.new(@total + arg, @fps, @drop_frame)
  end
end
-(arg) click to toggle source

Subtract a number of frames

# File lib/timecode.rb, line 419
def -(arg)
  if (arg.is_a?(Timecode) &&  framerate_in_delta(arg.fps, @fps) && (arg.drop? == @drop_frame))
    self.class.new(@total-arg.total, @fps, @drop_frame)
  elsif (arg.is_a?(Timecode))
    if (arg.drop? != @drop_frame)
      raise WrongDropFlag, "You are calculating timecodes with different drop flag values"
    else
      raise WrongFramerate, "You are calculating timecodes with different framerates"
    end      
  else
    self.class.new(@total-arg, @fps, @drop_frame)
  end
end
/(arg) click to toggle source

Get the number of times a passed timecode fits into this time span (if performed with Timecode) or a Timecode that multiplied by arg will give this one

# File lib/timecode.rb, line 446
def /(arg)
  arg.is_a?(Timecode) ?  (@total / arg.total) : self.class.new(@total / arg, @fps, @drop_frame)
end
<=>(other_tc) click to toggle source

Timecodes can be compared to each other

# File lib/timecode.rb, line 451
def <=>(other_tc)
  if framerate_in_delta(fps, other_tc.fps)
    self.total <=> other_tc.total
  else
    raise WrongFramerate, "Cannot compare timecodes with different framerates"
  end
end
adjacent_to?(another) click to toggle source

Tells whether the passes timecode is immediately to the left or to the right of that one with a 1 frame difference

# File lib/timecode.rb, line 414
def adjacent_to?(another)
  (self.succ == another) || (another.succ == self)
end
coerce(to) click to toggle source
# File lib/timecode.rb, line 292
def coerce(to)
  me = case to
    when String
      to_s
    when Integer
      to_i
    when Float
      to_f
    else
      self
  end
  [me, to]
end
convert(new_fps, drop_frame = @drop_frame) click to toggle source

Convert to different framerate and drop frame based on the total frames. Therefore, 1 second of PAL video will convert to 25 frames of NTSC (this is suitable for PAL to film TC conversions and back).

# File lib/timecode.rb, line 369
def convert(new_fps, drop_frame = @drop_frame)
  self.class.new(@total, new_fps, drop_frame)
end
drop?() click to toggle source

get DF

# File lib/timecode.rb, line 317
def drop?
  @drop_frame
end
fps() click to toggle source

get FPS

# File lib/timecode.rb, line 322
def fps
  @fps
end
frame_interval() click to toggle source

get frame interval in fractions of a second

# File lib/timecode.rb, line 347
def frame_interval
  1.0/@fps
end
framerate_in_delta(one, two) click to toggle source

Validate that framerates are within a small delta deviation considerable for floats

# File lib/timecode.rb, line 478
def framerate_in_delta(one, two)
  (one.to_f - two.to_f).abs <= ALLOWED_FPS_DELTA
end
frames() click to toggle source

get the number of frames

# File lib/timecode.rb, line 327
def frames
  value_parts[3]
end
hours() click to toggle source

get the number of hours

# File lib/timecode.rb, line 342
def hours
  value_parts[0]
end
minutes() click to toggle source

get the number of minutes

# File lib/timecode.rb, line 337
def minutes
  value_parts[1]
end
seconds() click to toggle source

get the number of seconds

# File lib/timecode.rb, line 332
def seconds
  value_parts[2]
end
succ() click to toggle source

Get the next frame

# File lib/timecode.rb, line 440
def succ
  self.class.new(@total + 1, @fps)
end
to_f() click to toggle source

get total frames as float

# File lib/timecode.rb, line 388
def to_f
  @total
end
to_i() click to toggle source

get total frames as integer

# File lib/timecode.rb, line 393
def to_i
  @total
end
to_s() click to toggle source

Get formatted SMPTE timecode. Hour count larger than 99 will roll over to the next remainder (129 hours will produce “29:00:00:00:00”). If you need the whole hour count use `to_s_without_rollover`

# File lib/timecode.rb, line 376
def to_s
  vs = value_parts
  vs[0] = vs[0] % 100 # Rollover any values > 99
  (@drop_frame ? WITH_FRAMES_DF : WITH_FRAMES) % vs 
end
to_s_without_rollover() click to toggle source

Get formatted SMPTE timecode. Hours might be larger than 99 and will not roll over

# File lib/timecode.rb, line 383
def to_s_without_rollover
  WITH_FRAMES % value_parts
end
to_seconds() click to toggle source

get the timecode as a floating-point number of seconds (used in Quicktime)

# File lib/timecode.rb, line 362
def to_seconds
  (@total / @fps)
end
to_uint() click to toggle source

get the timecode as bit-packed unsigned 32 bit int (suitable for DPX and SGI)

# File lib/timecode.rb, line 352
def to_uint
  elements = (("%02d" * 4) % [hours,minutes,seconds,frames]).split(//).map{|e| e.to_i }
  uint = 0
  elements.reverse.each_with_index do | p, i |
    uint |= p << 4 * i
  end
  uint
end
total() click to toggle source

get total frame count

# File lib/timecode.rb, line 312
def total
  to_f
end
with_fractional_seconds(pattern = WITH_FRACTIONS_OF_SECOND)
with_frames_as_fraction(pattern = WITH_FRACTIONS_OF_SECOND) click to toggle source

FFmpeg expects a fraction of a second as the last element instead of number of frames. Use this method to get the timecode that adheres to that expectation. The return of this method can be fed to ffmpeg directly.

Timecode.parse("00:00:10:24", 25).with_frames_as_fraction #=> "00:00:10.96"
# File lib/timecode.rb, line 463
def with_frames_as_fraction(pattern = WITH_FRACTIONS_OF_SECOND)
  vp = value_parts.dup
  vp[-1] = (100.0 / @fps) * vp[-1]
  pattern % vp
end
Also aliased as: with_fractional_seconds
with_srt_fraction() click to toggle source

SRT uses a fraction of a second as the last element instead of number of frames, with a comma as the separator

Timecode.parse("00:00:10:24", 25).with_srt_fraction #=> "00:00:10,96"
# File lib/timecode.rb, line 473
def with_srt_fraction
  with_frames_as_fraction(WITH_SRT_FRACTION)
end
zero?() click to toggle source

is the timecode at 00:00:00:00

# File lib/timecode.rb, line 307
def zero?
  @total.zero?
end

Private Instance Methods

validate!() click to toggle source

Prepare and format the values for TC output

# File lib/timecode.rb, line 485
def validate!
  comp = ComputationValues.new(@fps, @drop_frame)
  
  frames_dropped = false
  temp_total = @total
  hrs = (temp_total / comp.frames_per_hour).floor
  
  temp_total %= comp.frames_per_hour
  mins = (temp_total / comp.frames_per_10_min * 10).floor
  
  temp_total %= comp.frames_per_10_min
  if (temp_total >= comp.nd_frames_per_min)
    temp_total -= comp.nd_frames_per_min
    mins += ((temp_total / comp.frames_per_min) + 1).floor
    temp_total %= comp.frames_per_min
    frames_dropped = @drop_frame
  end
  
  rounded_base = @fps.round
  secs = (temp_total / rounded_base).floor
  rest_frames = (temp_total % rounded_base).floor
  
  if frames_dropped
    rest_frames += comp.drop_count
    if rest_frames >= rounded_base
      rest_frames -= rounded_base
      secs += 1
      if secs >= 60
        secs = 0
        mins += 1
        if mins >= 60
          mins = 0
          hrs += 1
          if hrs >= 999
            hrs = 0
          end
        end
      end
    end
  end   

  self.class.validate_atoms!(hrs, mins, secs, rest_frames, @fps)

  [hrs, mins, secs, rest_frames]
end
value_parts() click to toggle source
# File lib/timecode.rb, line 531
def value_parts
  @value ||= validate!
end