timecode 1.1.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +9 -0
- data/History.txt +5 -0
- data/Rakefile +25 -12
- data/lib/timecode.rb +140 -83
- data/test/test_timecode.rb +352 -275
- data/timecode.gemspec +53 -0
- metadata +108 -69
- data/.DS_Store +0 -0
- data/.gemtest +0 -0
- data/Manifest.txt +0 -7
data/Gemfile
ADDED
data/History.txt
CHANGED
data/Rakefile
CHANGED
@@ -1,14 +1,27 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require '
|
3
|
-
require './lib/timecode
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
require 'jeweler'
|
3
|
+
require './lib/timecode'
|
4
|
+
require 'thread'
|
5
|
+
Jeweler::Tasks.new do |gem|
|
6
|
+
gem.version = Timecode::VERSION
|
7
|
+
gem.name = "timecode"
|
8
|
+
gem.summary = "Timecode value class"
|
9
|
+
gem.email = "me@julik.nl"
|
10
|
+
gem.homepage = "http://guerilla-di.org/timecode"
|
11
|
+
gem.authors = ["Julik Tarkhanov"]
|
12
|
+
gem.license = 'MIT'
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
14
|
+
# Do not package invisibles
|
15
|
+
gem.files.exclude ".*"
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'rake/testtask'
|
19
|
+
Rake::TestTask.new(:test) do |test|
|
20
|
+
test.libs << 'lib' << 'test'
|
21
|
+
test.pattern = 'test/**/test_*.rb'
|
22
|
+
test.verbose = true
|
23
|
+
end
|
24
|
+
|
25
|
+
Jeweler::RubygemsDotOrgTasks.new
|
26
|
+
|
27
|
+
task :default => [ :test ]
|
data/lib/timecode.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Timecode is a convenience object for calculating SMPTE timecode natively.
|
1
|
+
# Timecode is a convenience object for calculating SMPTE timecode natively.
|
2
2
|
# The promise is that you only have to store two values to know the timecode - the amount
|
3
3
|
# of frames and the framerate. An additional perk might be to save the dropframeness,
|
4
4
|
# but we avoid that at this point.
|
@@ -11,33 +11,49 @@
|
|
11
11
|
# composed_of :source_tc, :class_name => 'Timecode',
|
12
12
|
# :mapping => [%w(source_tc_frames total), %w(tape_fps fps)]
|
13
13
|
|
14
|
+
require "approximately"
|
15
|
+
|
14
16
|
class Timecode
|
15
|
-
VERSION = '1.1.2'
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
VERSION = '2.1.0'
|
19
|
+
|
20
|
+
include Comparable, Approximately
|
21
|
+
|
19
22
|
DEFAULT_FPS = 25.0
|
20
|
-
|
23
|
+
|
21
24
|
#:stopdoc:
|
25
|
+
|
26
|
+
# Quoting the Flame project configs here (as of ver. 2013 at least)
|
27
|
+
# TIMECODE KEYWORD
|
28
|
+
# ----------------
|
29
|
+
# Specifies the default timecode format used by the project. Currently
|
30
|
+
# supported formats are 23.976, 24, 25, 29.97, 30, 50, 59.94 or 60 fps
|
31
|
+
# timecodes.
|
32
|
+
STANDARD_RATES = [23.976, 24, 25, 29.97, 30, 50, 59.94, 60].map do | float |
|
33
|
+
Approximately.approx(float, 0.002) # Tolerance of 2 millisecs should do.
|
34
|
+
end.freeze
|
35
|
+
|
22
36
|
NTSC_FPS = (30.0 * 1000 / 1001).freeze
|
23
37
|
FILMSYNC_FPS = (24.0 * 1000 / 1001).freeze
|
24
38
|
ALLOWED_FPS_DELTA = (0.001).freeze
|
25
|
-
|
39
|
+
|
26
40
|
COMPLETE_TC_RE = /^(\d{2}):(\d{2}):(\d{2}):(\d{2})$/
|
27
41
|
COMPLETE_TC_RE_24 = /^(\d{2}):(\d{2}):(\d{2})\+(\d{2})$/
|
28
42
|
DF_TC_RE = /^(\d{1,2}):(\d{1,2}):(\d{1,2});(\d{2})$/
|
29
|
-
FRACTIONAL_TC_RE = /^(\d{2}):(\d{2}):(\d{2})
|
43
|
+
FRACTIONAL_TC_RE = /^(\d{2}):(\d{2}):(\d{2})[\.,](\d{1,8})$/
|
30
44
|
TICKS_TC_RE = /^(\d{2}):(\d{2}):(\d{2}):(\d{3})$/
|
31
|
-
|
45
|
+
|
32
46
|
WITH_FRACTIONS_OF_SECOND = "%02d:%02d:%02d.%02d"
|
47
|
+
WITH_SRT_FRACTION = "%02d:%02d:%02d,%02d"
|
48
|
+
WITH_FRACTIONS_OF_SECOND_COMMA = "%02d:%02d:%02d,%03d"
|
33
49
|
WITH_FRAMES = "%02d:%02d:%02d:%02d"
|
34
50
|
WITH_FRAMES_24 = "%02d:%02d:%02d+%02d"
|
35
|
-
|
51
|
+
|
36
52
|
#:startdoc:
|
37
|
-
|
53
|
+
|
38
54
|
# All Timecode lib errors inherit from this
|
39
55
|
class Error < RuntimeError; end
|
40
|
-
|
56
|
+
|
41
57
|
# Gets raised if timecode is out of range (like 100 hours long)
|
42
58
|
class RangeError < Error; end
|
43
59
|
|
@@ -46,12 +62,12 @@ class Timecode
|
|
46
62
|
|
47
63
|
# Gets raised when you try to compute two timecodes with different framerates together
|
48
64
|
class WrongFramerate < ArgumentError; end
|
49
|
-
|
65
|
+
|
50
66
|
# Initialize a new Timecode object with a certain amount of frames and a framerate
|
51
67
|
# will be interpreted as the total number of frames
|
52
68
|
def initialize(total = 0, fps = DEFAULT_FPS)
|
53
69
|
raise WrongFramerate, "FPS cannot be zero" if fps.zero?
|
54
|
-
|
70
|
+
self.class.check_framerate!(fps)
|
55
71
|
# If total is a string, use parse
|
56
72
|
raise RangeError, "Timecode cannot be negative" if total.to_i < 0
|
57
73
|
# Always cast framerate to float, and num of rames to integer
|
@@ -59,23 +75,54 @@ class Timecode
|
|
59
75
|
@value = validate!
|
60
76
|
freeze
|
61
77
|
end
|
62
|
-
|
78
|
+
|
63
79
|
def inspect # :nodoc:
|
64
|
-
|
80
|
+
string_repr = if (framerate_in_delta(fps, 24))
|
81
|
+
WITH_FRAMES_24 % value_parts
|
82
|
+
else
|
83
|
+
WITH_FRAMES % value_parts
|
84
|
+
end
|
85
|
+
"#<Timecode:%s (%dF@%.2f)>" % [string_repr, total, fps]
|
65
86
|
end
|
66
|
-
|
87
|
+
|
67
88
|
class << self
|
68
|
-
|
89
|
+
|
90
|
+
# Returns the list of supported framerates for this subclass of Timecode
|
91
|
+
def supported_framerates
|
92
|
+
STANDARD_RATES + (@custom_framerates || [])
|
93
|
+
end
|
94
|
+
|
95
|
+
# Use this to add a custom framerate
|
96
|
+
def add_custom_framerate!(rate)
|
97
|
+
@custom_framerates ||= []
|
98
|
+
@custom_framerates.push(rate)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check the passed framerate and raise if it is not in the list
|
102
|
+
def check_framerate!(fps)
|
103
|
+
unless supported_framerates.include?(fps)
|
104
|
+
supported = "%s and %s are supported" % [supported_framerates[0..-2].join(", "), supported_framerates[-1]]
|
105
|
+
raise WrongFramerate, "Framerate #{fps} is not in the list of supported framerates (#{supported})"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
69
109
|
# Use initialize for integers and parsing for strings
|
70
|
-
def new(from, fps = DEFAULT_FPS)
|
110
|
+
def new(from = nil, fps = DEFAULT_FPS)
|
71
111
|
from.is_a?(String) ? parse(from, fps) : super(from, fps)
|
72
112
|
end
|
73
|
-
|
113
|
+
|
74
114
|
# Parse timecode and return zero if none matched
|
75
115
|
def soft_parse(input, with_fps = DEFAULT_FPS)
|
76
116
|
parse(input) rescue new(0, with_fps)
|
77
117
|
end
|
78
|
-
|
118
|
+
|
119
|
+
# Parses the timecode contained in a passed filename as frame number in a sequence
|
120
|
+
def from_filename_in_sequence(filename_with_or_without_path, fps = DEFAULT_FPS)
|
121
|
+
b = File.basename(filename_with_or_without_path)
|
122
|
+
number = b.scan(/\d+/).flatten[-1].to_i
|
123
|
+
new(number, fps)
|
124
|
+
end
|
125
|
+
|
79
126
|
# Parse timecode entered by the user. Will raise if the string cannot be parsed.
|
80
127
|
# The following formats are supported:
|
81
128
|
# * 10h 20m 10s 1f (or any combination thereof) - will be disassembled to hours, frames, seconds and so on automatically
|
@@ -83,7 +130,7 @@ class Timecode
|
|
83
130
|
# * 00:00:00:00 - will be parsed as zero TC
|
84
131
|
def parse(spaced_input, with_fps = DEFAULT_FPS)
|
85
132
|
input = spaced_input.strip
|
86
|
-
|
133
|
+
|
87
134
|
# Drop frame goodbye
|
88
135
|
if (input =~ DF_TC_RE)
|
89
136
|
raise Error, "We do not support drop-frame TC"
|
@@ -129,32 +176,32 @@ class Timecode
|
|
129
176
|
raise CannotParse, "Cannot parse #{input} into timecode, unknown format"
|
130
177
|
end
|
131
178
|
end
|
132
|
-
|
179
|
+
|
133
180
|
# Initialize a Timecode object at this specfic timecode
|
134
181
|
def at(hrs, mins, secs, frames, with_fps = DEFAULT_FPS)
|
135
182
|
validate_atoms!(hrs, mins, secs, frames, with_fps)
|
136
183
|
total = (hrs*(60*60*with_fps) + mins*(60*with_fps) + secs*with_fps + frames).round
|
137
184
|
new(total, with_fps)
|
138
185
|
end
|
139
|
-
|
186
|
+
|
140
187
|
# Validate the passed atoms for the concrete framerate
|
141
188
|
def validate_atoms!(hrs, mins, secs, frames, with_fps)
|
142
189
|
case true
|
143
|
-
|
144
|
-
raise RangeError, "There can be no more than
|
190
|
+
when hrs > 999
|
191
|
+
raise RangeError, "There can be no more than 999 hours, got #{hrs}"
|
145
192
|
when mins > 59
|
146
193
|
raise RangeError, "There can be no more than 59 minutes, got #{mins}"
|
147
194
|
when secs > 59
|
148
195
|
raise RangeError, "There can be no more than 59 seconds, got #{secs}"
|
149
|
-
when frames
|
150
|
-
raise RangeError, "There can be no more than #{with_fps
|
196
|
+
when frames >= with_fps
|
197
|
+
raise RangeError, "There can be no more than #{with_fps} frames @#{with_fps}, got #{frames}"
|
151
198
|
end
|
152
199
|
end
|
153
|
-
|
200
|
+
|
154
201
|
# Parse a timecode with fractional seconds instead of frames. This is how ffmpeg reports
|
155
202
|
# a timecode
|
156
203
|
def parse_with_fractional_seconds(tc_with_fractions_of_second, fps = DEFAULT_FPS)
|
157
|
-
fraction_expr =
|
204
|
+
fraction_expr = /[\.,](\d+)$/
|
158
205
|
fraction_part = ('.' + tc_with_fractions_of_second.scan(fraction_expr)[0][0]).to_f
|
159
206
|
|
160
207
|
seconds_per_frame = 1.0 / fps.to_f
|
@@ -165,33 +212,33 @@ class Timecode
|
|
165
212
|
parse(tc_with_frameno, fps)
|
166
213
|
end
|
167
214
|
|
168
|
-
# Parse a timecode with ticks of a second instead of frames. A 'tick' is defined as
|
215
|
+
# Parse a timecode with ticks of a second instead of frames. A 'tick' is defined as
|
169
216
|
# 4 msec and has a range of 0 to 249. This format can show up in subtitle files for digital cinema
|
170
217
|
# used by CineCanvas systems
|
171
218
|
def parse_with_ticks(tc_with_ticks, fps = DEFAULT_FPS)
|
172
219
|
ticks_expr = /(\d{3})$/
|
173
220
|
num_ticks = tc_with_ticks.scan(ticks_expr).join.to_i
|
174
|
-
|
221
|
+
|
175
222
|
raise RangeError, "Invalid tick count #{num_ticks}" if num_ticks > 249
|
176
|
-
|
223
|
+
|
177
224
|
seconds_per_frame = 1.0 / fps
|
178
225
|
frame_idx = ( (num_ticks * 0.004) / seconds_per_frame ).floor
|
179
226
|
tc_with_frameno = tc_with_ticks.gsub(ticks_expr, "%02d" % frame_idx)
|
180
|
-
|
227
|
+
|
181
228
|
parse(tc_with_frameno, fps)
|
182
229
|
end
|
183
|
-
|
230
|
+
|
184
231
|
# create a timecode from the number of seconds. This is how current time is supplied by
|
185
232
|
# QuickTime and other systems which have non-frame-based timescales
|
186
233
|
def from_seconds(seconds_float, the_fps = DEFAULT_FPS)
|
187
234
|
total_frames = (seconds_float.to_f * the_fps.to_f).to_i
|
188
235
|
new(total_frames, the_fps)
|
189
236
|
end
|
190
|
-
|
237
|
+
|
191
238
|
# Some systems (like SGIs) and DPX format store timecode as unsigned integer, bit-packed. This method
|
192
239
|
# unpacks such an integer into a timecode.
|
193
240
|
def from_uint(uint, fps = DEFAULT_FPS)
|
194
|
-
tc_elements = (0..7).to_a.reverse.map do | multiplier |
|
241
|
+
tc_elements = (0..7).to_a.reverse.map do | multiplier |
|
195
242
|
((uint >> (multiplier * 4)) & 0x0F)
|
196
243
|
end.join.scan(/(\d{2})/).flatten.map{|e| e.to_i}
|
197
244
|
|
@@ -199,7 +246,7 @@ class Timecode
|
|
199
246
|
at(*tc_elements)
|
200
247
|
end
|
201
248
|
end
|
202
|
-
|
249
|
+
|
203
250
|
def coerce(to)
|
204
251
|
me = case to
|
205
252
|
when String
|
@@ -213,76 +260,81 @@ class Timecode
|
|
213
260
|
end
|
214
261
|
[me, to]
|
215
262
|
end
|
216
|
-
|
263
|
+
|
217
264
|
# is the timecode at 00:00:00:00
|
218
265
|
def zero?
|
219
266
|
@total.zero?
|
220
267
|
end
|
221
|
-
|
268
|
+
|
222
269
|
# get total frame count
|
223
270
|
def total
|
224
271
|
to_f
|
225
272
|
end
|
226
|
-
|
273
|
+
|
227
274
|
# get FPS
|
228
275
|
def fps
|
229
276
|
@fps
|
230
277
|
end
|
231
|
-
|
278
|
+
|
232
279
|
# get the number of frames
|
233
280
|
def frames
|
234
281
|
value_parts[3]
|
235
282
|
end
|
236
|
-
|
283
|
+
|
237
284
|
# get the number of seconds
|
238
285
|
def seconds
|
239
286
|
value_parts[2]
|
240
287
|
end
|
241
|
-
|
288
|
+
|
242
289
|
# get the number of minutes
|
243
290
|
def minutes
|
244
291
|
value_parts[1]
|
245
292
|
end
|
246
|
-
|
293
|
+
|
247
294
|
# get the number of hours
|
248
295
|
def hours
|
249
296
|
value_parts[0]
|
250
297
|
end
|
251
|
-
|
298
|
+
|
252
299
|
# get frame interval in fractions of a second
|
253
300
|
def frame_interval
|
254
301
|
1.0/@fps
|
255
302
|
end
|
256
|
-
|
303
|
+
|
257
304
|
# get the timecode as bit-packed unsigned 32 bit int (suitable for DPX and SGI)
|
258
305
|
def to_uint
|
259
306
|
elements = (("%02d" * 4) % [hours,minutes,seconds,frames]).split(//).map{|e| e.to_i }
|
260
307
|
uint = 0
|
261
308
|
elements.reverse.each_with_index do | p, i |
|
262
|
-
uint |= p << 4 * i
|
309
|
+
uint |= p << 4 * i
|
263
310
|
end
|
264
311
|
uint
|
265
312
|
end
|
266
|
-
|
313
|
+
|
267
314
|
# get the timecode as a floating-point number of seconds (used in Quicktime)
|
268
315
|
def to_seconds
|
269
316
|
(@total / @fps)
|
270
317
|
end
|
271
|
-
|
318
|
+
|
272
319
|
# Convert to different framerate based on the total frames. Therefore,
|
273
|
-
# 1 second of PAL video will convert to 25 frames of NTSC (this
|
320
|
+
# 1 second of PAL video will convert to 25 frames of NTSC (this
|
274
321
|
# is suitable for PAL to film TC conversions and back).
|
275
322
|
def convert(new_fps)
|
276
323
|
self.class.new(@total, new_fps)
|
277
324
|
end
|
278
|
-
|
279
|
-
#
|
325
|
+
|
326
|
+
# Get formatted SMPTE timecode. Hour count larger than 99 will roll over to the next
|
327
|
+
# remainder (129 hours will produce "29:00:00:00:00"). If you need the whole hour count
|
328
|
+
# use `to_s_without_rollover`
|
280
329
|
def to_s
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
330
|
+
vs = value_parts
|
331
|
+
vs[0] = vs[0] % 100 # Rollover any values > 99
|
332
|
+
WITH_FRAMES % vs
|
333
|
+
end
|
334
|
+
|
335
|
+
# Get formatted SMPTE timecode. Hours might be larger than 99 and will not roll over
|
336
|
+
def to_s_without_rollover
|
337
|
+
WITH_FRAMES % value_parts
|
286
338
|
end
|
287
339
|
|
288
340
|
# get total frames as float
|
@@ -294,7 +346,7 @@ class Timecode
|
|
294
346
|
def to_i
|
295
347
|
@total
|
296
348
|
end
|
297
|
-
|
349
|
+
|
298
350
|
# add number of frames (or another timecode) to this one
|
299
351
|
def +(arg)
|
300
352
|
if (arg.is_a?(Timecode) && framerate_in_delta(arg.fps, @fps))
|
@@ -305,13 +357,13 @@ class Timecode
|
|
305
357
|
self.class.new(@total + arg, @fps)
|
306
358
|
end
|
307
359
|
end
|
308
|
-
|
360
|
+
|
309
361
|
# Tells whether the passes timecode is immediately to the left or to the right of that one
|
310
362
|
# with a 1 frame difference
|
311
363
|
def adjacent_to?(another)
|
312
364
|
(self.succ == another) || (another.succ == self)
|
313
365
|
end
|
314
|
-
|
366
|
+
|
315
367
|
# Subtract a number of frames
|
316
368
|
def -(arg)
|
317
369
|
if (arg.is_a?(Timecode) && framerate_in_delta(arg.fps, @fps))
|
@@ -322,68 +374,73 @@ class Timecode
|
|
322
374
|
self.class.new(@total-arg, @fps)
|
323
375
|
end
|
324
376
|
end
|
325
|
-
|
377
|
+
|
326
378
|
# Multiply the timecode by a number
|
327
379
|
def *(arg)
|
328
380
|
raise RangeError, "Timecode multiplier cannot be negative" if (arg < 0)
|
329
381
|
self.class.new(@total*arg.to_i, @fps)
|
330
382
|
end
|
331
|
-
|
383
|
+
|
332
384
|
# Get the next frame
|
333
385
|
def succ
|
334
386
|
self.class.new(@total + 1, @fps)
|
335
387
|
end
|
336
|
-
|
337
|
-
# Get the number of times a passed timecode fits into this time span (if performed with Timecode) or
|
388
|
+
|
389
|
+
# Get the number of times a passed timecode fits into this time span (if performed with Timecode) or
|
338
390
|
# a Timecode that multiplied by arg will give this one
|
339
391
|
def /(arg)
|
340
392
|
arg.is_a?(Timecode) ? (@total / arg.total) : self.class.new(@total / arg, @fps)
|
341
393
|
end
|
342
|
-
|
394
|
+
|
343
395
|
# Timecodes can be compared to each other
|
344
396
|
def <=>(other_tc)
|
345
397
|
if framerate_in_delta(fps, other_tc.fps)
|
346
398
|
self.total <=> other_tc.total
|
347
|
-
else
|
399
|
+
else
|
348
400
|
raise WrongFramerate, "Cannot compare timecodes with different framerates"
|
349
401
|
end
|
350
402
|
end
|
351
|
-
|
403
|
+
|
352
404
|
# FFmpeg expects a fraction of a second as the last element instead of number of frames. Use this
|
353
405
|
# method to get the timecode that adheres to that expectation. The return of this method can be fed
|
354
406
|
# to ffmpeg directly.
|
355
407
|
# Timecode.parse("00:00:10:24", 25).with_frames_as_fraction #=> "00:00:10.96"
|
356
|
-
def with_frames_as_fraction
|
408
|
+
def with_frames_as_fraction(pattern = WITH_FRACTIONS_OF_SECOND)
|
357
409
|
vp = value_parts.dup
|
358
410
|
vp[-1] = (100.0 / @fps) * vp[-1]
|
359
|
-
|
411
|
+
pattern % vp
|
360
412
|
end
|
361
413
|
alias_method :with_fractional_seconds, :with_frames_as_fraction
|
362
|
-
|
414
|
+
|
415
|
+
# SRT uses a fraction of a second as the last element instead of number of frames, with a comma as
|
416
|
+
# the separator
|
417
|
+
# Timecode.parse("00:00:10:24", 25).with_srt_fraction #=> "00:00:10,96"
|
418
|
+
def with_srt_fraction
|
419
|
+
with_frames_as_fraction(WITH_SRT_FRACTION)
|
420
|
+
end
|
421
|
+
|
363
422
|
# Validate that framerates are within a small delta deviation considerable for floats
|
364
423
|
def framerate_in_delta(one, two)
|
365
424
|
(one.to_f - two.to_f).abs <= ALLOWED_FPS_DELTA
|
366
425
|
end
|
367
|
-
|
426
|
+
|
368
427
|
private
|
369
|
-
|
428
|
+
|
370
429
|
# Prepare and format the values for TC output
|
371
430
|
def validate!
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
mins = (secs/60)
|
376
|
-
secs
|
377
|
-
hrs = (mins/60).floor
|
378
|
-
mins-= (hrs*60)
|
431
|
+
secs = (@total / @fps).floor
|
432
|
+
rest_frames = (@total % @fps).floor
|
433
|
+
hrs = secs.to_i / 3600
|
434
|
+
mins = (secs.to_i / 60) % 60
|
435
|
+
secs = secs % 60
|
379
436
|
|
380
|
-
self.class.validate_atoms!(hrs, mins, secs,
|
437
|
+
self.class.validate_atoms!(hrs, mins, secs, rest_frames, @fps)
|
381
438
|
|
382
|
-
[hrs, mins, secs,
|
439
|
+
[hrs, mins, secs, rest_frames]
|
383
440
|
end
|
384
|
-
|
441
|
+
|
385
442
|
def value_parts
|
386
443
|
@value ||= validate!
|
387
444
|
end
|
388
|
-
|
445
|
+
|
389
446
|
end
|