class Timecode
Constants
- DEFAULT_FPS
- VERSION
Public Class Methods
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
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 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
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
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
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
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
Use initialize for integers and parsing for strings
# 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 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 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 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
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
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 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
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
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
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
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
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
# 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 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
get DF
# File lib/timecode.rb, line 317 def drop? @drop_frame end
get FPS
# File lib/timecode.rb, line 322 def fps @fps end
get frame interval in fractions of a second
# File lib/timecode.rb, line 347 def frame_interval 1.0/@fps end
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
get the number of frames
# File lib/timecode.rb, line 327 def frames value_parts[3] end
get the number of hours
# File lib/timecode.rb, line 342 def hours value_parts[0] end
get the number of minutes
# File lib/timecode.rb, line 337 def minutes value_parts[1] end
get the number of seconds
# File lib/timecode.rb, line 332 def seconds value_parts[2] end
Get the next frame
# File lib/timecode.rb, line 440 def succ self.class.new(@total + 1, @fps) end
get total frames as float
# File lib/timecode.rb, line 388 def to_f @total end
get total frames as integer
# File lib/timecode.rb, line 393 def to_i @total end
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
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
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
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
get total frame count
# File lib/timecode.rb, line 312 def total to_f end
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
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
is the timecode at 00:00:00:00
# File lib/timecode.rb, line 307 def zero? @total.zero? end
Private Instance Methods
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
# File lib/timecode.rb, line 531 def value_parts @value ||= validate! end