Class: Timecode

Inherits:
Object
  • Object
show all
Includes:
Approximately, Comparable
Defined in:
lib/timecode.rb,
lib/timecode/version.rb

Defined Under Namespace

Classes: CannotParse, ComputationValues, Error, RangeError, WrongDropFlag, WrongFramerate

Constant Summary collapse

DEFAULT_FPS =
25.0
STANDARD_RATES =

Quoting the Flame project configs here (as of ver. 2013 at least) TIMECODE KEYWORD


Specifies the default timecode format used by the project. Currently supported formats are 23.976, 24, 25, 29.97, 30, 50, 59.94 or 60 fps timecodes.

[23.976, 24, 25, 29.97, 30, 50, 59.94, 60].map do | float |
  Approximately.approx(float, 0.002) # Tolerance of 2 millisecs should do.
end.freeze
NTSC_FPS =
(30.0 * 1000 / 1001).freeze
FILMSYNC_FPS =
(24.0 * 1000 / 1001).freeze
ALLOWED_FPS_DELTA =
(0.001).freeze
COMPLETE_TC_RE =
/^(\d{2}):(\d{2}):(\d{2}):(\d{2})$/
COMPLETE_TC_RE_24 =
/^(\d{2}):(\d{2}):(\d{2})\+(\d{2})$/
DF_TC_RE =
/^(\d{1,2}):(\d{1,2}):(\d{1,2});(\d{2})$/
FRACTIONAL_TC_RE =
/^(\d{2}):(\d{2}):(\d{2})[\.,](\d{1,8})$/
TICKS_TC_RE =
/^(\d{2}):(\d{2}):(\d{2}):(\d{3})$/
WITH_FRACTIONS_OF_SECOND =
"%02d:%02d:%02d.%02d"
WITH_SRT_FRACTION =
"%02d:%02d:%02d,%02d"
WITH_FRACTIONS_OF_SECOND_COMMA =
"%02d:%02d:%02d,%03d"
WITH_FRAMES =
"%02d:%02d:%02d:%02d"
WITH_FRAMES_DF =
"%02d:%02d:%02d;%02d"
WITH_FRAMES_24 =
"%02d:%02d:%02d+%02d"
VERSION =
'2.2.2'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(total = 0, fps = DEFAULT_FPS, drop_frame = false) ⇒ Timecode

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

Raises:



94
95
96
97
98
99
100
101
102
103
104
# 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

Class Method Details

.add_custom_framerate!(rate) ⇒ Object

Use this to add a custom framerate



123
124
125
126
# 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) ⇒ Object

Initialize a Timecode object at this specfic timecode



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# 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) ⇒ Object

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



129
130
131
132
133
134
# 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) ⇒ Object

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



147
148
149
150
151
# 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) ⇒ Object

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



275
276
277
278
# 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) ⇒ Object

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



282
283
284
285
286
287
288
289
# 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(from = nil, fps = DEFAULT_FPS, drop_frame = false) ⇒ Object

Use initialize for integers and parsing for strings



137
138
139
# 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) ⇒ Object

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



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# 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) ⇒ Object

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



245
246
247
248
249
250
251
252
253
254
255
# 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) ⇒ Object

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

Raises:



260
261
262
263
264
265
266
267
268
269
270
271
# 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) ⇒ Object

Parse timecode and return zero if none matched



142
143
144
# File 'lib/timecode.rb', line 142

def soft_parse(input, with_fps = DEFAULT_FPS)
  parse(input) rescue new(0, with_fps)
end

.supported_frameratesObject

Returns the list of supported framerates for this subclass of Timecode



118
119
120
# File 'lib/timecode.rb', line 118

def supported_framerates
  STANDARD_RATES + (@custom_framerates || [])
end

.validate_atoms!(hrs, mins, secs, frames, with_fps) ⇒ Object

Validate the passed atoms for the concrete framerate



230
231
232
233
234
235
236
237
238
239
240
241
# 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

Instance Method Details

#*(arg) ⇒ Object

Multiply the timecode by a number

Raises:



434
435
436
437
# 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) ⇒ Object

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



398
399
400
401
402
403
404
405
406
407
408
409
410
# 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) ⇒ Object

Subtract a number of frames



419
420
421
422
423
424
425
426
427
428
429
430
431
# 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) ⇒ Object

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



446
447
448
# 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) ⇒ Object

Timecodes can be compared to each other



451
452
453
454
455
456
457
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


414
415
416
# File 'lib/timecode.rb', line 414

def adjacent_to?(another)
  (self.succ == another) || (another.succ == self)
end

#coerce(to) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
# 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) ⇒ Object

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).



369
370
371
# File 'lib/timecode.rb', line 369

def convert(new_fps, drop_frame = @drop_frame)
  self.class.new(@total, new_fps, drop_frame)
end

#drop?Boolean

get DF

Returns:

  • (Boolean)


317
318
319
# File 'lib/timecode.rb', line 317

def drop?
  @drop_frame
end

#fpsObject

get FPS



322
323
324
# File 'lib/timecode.rb', line 322

def fps
  @fps
end

#frame_intervalObject

get frame interval in fractions of a second



347
348
349
# File 'lib/timecode.rb', line 347

def frame_interval
  1.0/@fps
end

#framerate_in_delta(one, two) ⇒ Object

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



478
479
480
# File 'lib/timecode.rb', line 478

def framerate_in_delta(one, two)
  (one.to_f - two.to_f).abs <= ALLOWED_FPS_DELTA
end

#framesObject

get the number of frames



327
328
329
# File 'lib/timecode.rb', line 327

def frames
  value_parts[3]
end

#hoursObject

get the number of hours



342
343
344
# File 'lib/timecode.rb', line 342

def hours
  value_parts[0]
end

#inspectObject

:nodoc:



106
107
108
109
110
111
112
113
# File 'lib/timecode.rb', line 106

def inspect # :nodoc:
  string_repr = if (framerate_in_delta(fps, 24))
    WITH_FRAMES_24 % value_parts
  else
    WITH_FRAMES % value_parts
  end
  "#<Timecode:%s (%dF@%.2f)>" % [string_repr, total, fps]
end

#minutesObject

get the number of minutes



337
338
339
# File 'lib/timecode.rb', line 337

def minutes
  value_parts[1]
end

#secondsObject

get the number of seconds



332
333
334
# File 'lib/timecode.rb', line 332

def seconds
  value_parts[2]
end

#succObject

Get the next frame



440
441
442
# File 'lib/timecode.rb', line 440

def succ
  self.class.new(@total + 1, @fps)
end

#to_fObject

get total frames as float



388
389
390
# File 'lib/timecode.rb', line 388

def to_f
  @total
end

#to_iObject

get total frames as integer



393
394
395
# File 'lib/timecode.rb', line 393

def to_i
  @total
end

#to_sObject

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`



376
377
378
379
380
# 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_rolloverObject

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



383
384
385
# File 'lib/timecode.rb', line 383

def to_s_without_rollover
  WITH_FRAMES % value_parts
end

#to_secondsObject

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



362
363
364
# File 'lib/timecode.rb', line 362

def to_seconds
  (@total / @fps)
end

#to_uintObject

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



352
353
354
355
356
357
358
359
# 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

#totalObject

get total frame count



312
313
314
# File 'lib/timecode.rb', line 312

def total
  to_f
end

#with_frames_as_fraction(pattern = WITH_FRACTIONS_OF_SECOND) ⇒ Object Also known as: with_fractional_seconds

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"


463
464
465
466
467
# 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

#with_srt_fractionObject

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"


473
474
475
# File 'lib/timecode.rb', line 473

def with_srt_fraction
  with_frames_as_fraction(WITH_SRT_FRACTION)
end

#zero?Boolean

is the timecode at 00:00:00:00

Returns:

  • (Boolean)


307
308
309
# File 'lib/timecode.rb', line 307

def zero?
  @total.zero?
end