julik-timecode 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +26 -0
- data/Manifest.txt +8 -0
- data/README.txt +48 -0
- data/Rakefile +15 -0
- data/SPECS.txt +74 -0
- data/lib/timecode.rb +342 -0
- data/test/test_timecode.rb +318 -0
- data/timecode.gemspec +36 -0
- metadata +82 -0
data/History.txt
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
* Tests remade using test/spec
|
2
|
+
|
3
|
+
=== 0.1.4 / 2000-01-01
|
4
|
+
|
5
|
+
* Expanded test coverage
|
6
|
+
* Some formatting/doc improvements
|
7
|
+
|
8
|
+
=== 0.1.3 / 2008-12-25
|
9
|
+
|
10
|
+
* Implement the format FFmpeg uses (fractional seconds instead of frames)
|
11
|
+
|
12
|
+
=== 0.1.2 / 2008-12-25
|
13
|
+
|
14
|
+
* Fix to_uint
|
15
|
+
* Always use float frame rates and rely on a small delta for comparison
|
16
|
+
|
17
|
+
=== 0.1.1 / 2008-12-15
|
18
|
+
|
19
|
+
* Allow passing framerate to from_uint
|
20
|
+
|
21
|
+
=== 0.1.0 / 2008-12-15
|
22
|
+
|
23
|
+
* 1 major enhancement
|
24
|
+
|
25
|
+
* Birthday!
|
26
|
+
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= timecode
|
2
|
+
|
3
|
+
* http://wiretap.rubyforge.org/timecode
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Value class for SMPTE timecode information
|
8
|
+
|
9
|
+
== SYNOPSIS:
|
10
|
+
|
11
|
+
tc = Timecode.parse("00:00:10:12", fps = 25)
|
12
|
+
tc.total #=> 262
|
13
|
+
|
14
|
+
plus_ten = tc + Timecode.parse("10h", fps = 25)
|
15
|
+
plus_ten #=> "10:00:10:12"
|
16
|
+
|
17
|
+
== PROBLEMS:
|
18
|
+
|
19
|
+
Currently there is no support for drop-frame timecode
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* sudo gem install timecode
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2008 Julik Tarkhanov
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require './lib/timecode.rb'
|
4
|
+
|
5
|
+
Hoe.new('timecode', Timecode::VERSION) do |p|
|
6
|
+
p.developer('Julik', 'me@julik.nl')
|
7
|
+
p.extra_deps.reject! {|e| e[0] == 'hoe' }
|
8
|
+
p.extra_deps << 'test-spec'
|
9
|
+
p.rubyforge_name = 'wiretap'
|
10
|
+
p.remote_rdoc_dir = 'timecode'
|
11
|
+
end
|
12
|
+
|
13
|
+
task "specs" do
|
14
|
+
`specrb test/* --rdox > SPECS.txt`
|
15
|
+
end
|
data/SPECS.txt
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
== Timecode on instantiation should
|
3
|
+
* be instantable from int
|
4
|
+
* always coerce FPS to float
|
5
|
+
* create a zero TC with no arguments
|
6
|
+
|
7
|
+
== An existing Timecode should
|
8
|
+
* report that the framerates are in delta
|
9
|
+
* validate equality based on delta
|
10
|
+
* report total as it's to_i
|
11
|
+
* support hours
|
12
|
+
* support minutes
|
13
|
+
* support seconds
|
14
|
+
* support frames
|
15
|
+
* report frame_interval as a float
|
16
|
+
|
17
|
+
== A Timecode of zero should
|
18
|
+
* properly respond to zero?
|
19
|
+
|
20
|
+
== An existing TImecode on inspection should
|
21
|
+
* properly present himself via inspect
|
22
|
+
* properly print itself
|
23
|
+
|
24
|
+
== An existing Timecode used within ranges should
|
25
|
+
* properly provide successive value that is one frame up
|
26
|
+
* work as a range member
|
27
|
+
|
28
|
+
== A Timecode on conversion should
|
29
|
+
* copy itself with a different framerate
|
30
|
+
|
31
|
+
== A Timecode on calculations should
|
32
|
+
* support addition
|
33
|
+
* should raise on addition if framerates do not match
|
34
|
+
* when added with an integer instead calculate on total
|
35
|
+
* support subtraction
|
36
|
+
* on subtraction of an integer instead calculate on total
|
37
|
+
* raise when subtracting a Timecode with a different framerate
|
38
|
+
* support multiplication
|
39
|
+
* raise when the resultig Timecode is negative
|
40
|
+
* yield a Timecode when divided by an Integer
|
41
|
+
* yield a number when divided by another Timecode
|
42
|
+
|
43
|
+
== A Timecode used with fractional number of seconds
|
44
|
+
* should properly return fractional seconds
|
45
|
+
* properly translate to frames when instantiated from fractional seconds
|
46
|
+
|
47
|
+
== Timecode.at() should
|
48
|
+
* disallow more than 99 hrs
|
49
|
+
* disallow more than 59 minutes
|
50
|
+
* disallow more than 59 seconds
|
51
|
+
* disallow more frames than what the framerate permits
|
52
|
+
* propery accept usable values
|
53
|
+
|
54
|
+
== Timecode.parse() should
|
55
|
+
* handle complete SMPTE timecode
|
56
|
+
* refuse to handle timecode that is out of range for the framerate
|
57
|
+
* parse a row of numbers as parts of a timecode starting from the right
|
58
|
+
* parse a number with f suffix as frames
|
59
|
+
* parse a number with s suffix as seconds
|
60
|
+
* parse a number with m suffix as minutes
|
61
|
+
* parse a number with h suffix as hours
|
62
|
+
* parse different suffixes as a sum of elements
|
63
|
+
* parse timecode with fractional second instead of frames
|
64
|
+
* raise on improper format
|
65
|
+
|
66
|
+
== Timecode.soft_parse should
|
67
|
+
* parse the timecode
|
68
|
+
* not raise on improper format and return zero TC instead
|
69
|
+
|
70
|
+
== Timecode with unsigned integer conversions should
|
71
|
+
* parse from a 4x4bits packed 32bit unsigned int
|
72
|
+
* properly convert itself back to 4x4 bits 32bit unsigned int
|
73
|
+
|
74
|
+
48 specifications (80 requirements), 0 failures
|
data/lib/timecode.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
# Timecode is a convenience object for calculating SMPTE timecode natively.
|
2
|
+
# The promise is that you only have to store two values to know the timecode - the amount
|
3
|
+
# of frames and the framerate. An additional perk might be to save the dropframeness,
|
4
|
+
# but we avoid that at this point.
|
5
|
+
#
|
6
|
+
# You can calculate in timecode objects ass well as with conventional integers and floats.
|
7
|
+
# Timecode is immutable and can be used as a value object. Timecode objects are sortable.
|
8
|
+
#
|
9
|
+
# Here's how to use it with ActiveRecord (your column names will be source_tc_frames_total and tape_fps)
|
10
|
+
#
|
11
|
+
# composed_of :source_tc, :class_name => 'Timecode',
|
12
|
+
# :mapping => [%w(source_tc_frames total), %w(tape_fps fps)]
|
13
|
+
|
14
|
+
class Timecode
|
15
|
+
VERSION = '0.1.4'
|
16
|
+
|
17
|
+
include Comparable
|
18
|
+
|
19
|
+
DEFAULT_FPS = 25.0
|
20
|
+
|
21
|
+
#:stopdoc:
|
22
|
+
NTSC_FPS = (30.0 * 1000 / 1001).freeze
|
23
|
+
ALLOWED_FPS_DELTA = (0.001).freeze
|
24
|
+
|
25
|
+
COMPLETE_TC_RE = /^(\d{2}):(\d{2}):(\d{2}):(\d{2})$/
|
26
|
+
DF_TC_RE = /^(\d{1,2}):(\d{1,2}):(\d{1,2});(\d{2})$/
|
27
|
+
FRACTIONAL_TC_RE = /^(\d{2}):(\d{2}):(\d{2}).(\d{1,8})$/
|
28
|
+
|
29
|
+
WITH_FRACTIONS_OF_SECOND = "%02d:%02d:%02d.%02d"
|
30
|
+
WITH_FRAMES = "%02d:%02d:%02d:%02d"
|
31
|
+
#:startdoc:
|
32
|
+
|
33
|
+
# All Timecode lib errors inherit from this
|
34
|
+
class Error < RuntimeError; end
|
35
|
+
|
36
|
+
# Will be raised for functions that are not supported
|
37
|
+
class TimecodeLibError < Error; end
|
38
|
+
|
39
|
+
# Gets raised if timecode is out of range (like 100 hours long)
|
40
|
+
class RangeError < Error; end
|
41
|
+
|
42
|
+
# Gets raised when a timecode cannot be parsed
|
43
|
+
class CannotParse < Error; end
|
44
|
+
|
45
|
+
# Gets raised when you try to compute two timecodes with different framerates together
|
46
|
+
class WrongFramerate < ArgumentError; end
|
47
|
+
|
48
|
+
# Initialize a new Timecode object with a certain amount of frames and a framerate
|
49
|
+
# will be interpreted as the total number of frames
|
50
|
+
def initialize(total = 0, fps = DEFAULT_FPS)
|
51
|
+
raise WrongFramerate, "FPS cannot be zero" if fps.zero?
|
52
|
+
|
53
|
+
# If total is a string, use parse
|
54
|
+
raise RangeError, "Timecode cannot be negative" if total.to_i < 0
|
55
|
+
# Always cast framerate to float, and num of rames to integer
|
56
|
+
@total, @fps = total.to_i, fps.to_f
|
57
|
+
@value = validate!
|
58
|
+
freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect # :nodoc:
|
62
|
+
"#<Timecode:%s (%dF@%.2f)>" % [to_s, total, fps]
|
63
|
+
end
|
64
|
+
|
65
|
+
TIME_FIELDS = 7 # :nodoc:
|
66
|
+
|
67
|
+
class << self
|
68
|
+
|
69
|
+
# Use initialize for integers and parsing for strings
|
70
|
+
def new(from, fps = DEFAULT_FPS)
|
71
|
+
from.is_a?(String) ? parse(from, fps) : super(from, fps)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse timecode and return zero if none matched
|
75
|
+
def soft_parse(input, with_fps = DEFAULT_FPS)
|
76
|
+
parse(input) rescue new(0, with_fps)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Parse timecode entered by the user. Will raise if the string cannot be parsed.
|
80
|
+
# The following formats are supported:
|
81
|
+
# * 10h 20m 10s 1f (or any combination thereof) - will be disassembled to hours, frames, seconds and so on automatically
|
82
|
+
# * 123 - will be parsed as 00:00:01:23
|
83
|
+
# * 00:00:00:00 - will be parsed as zero TC
|
84
|
+
def parse(input, with_fps = DEFAULT_FPS)
|
85
|
+
# Drop frame goodbye
|
86
|
+
raise Error, "We do not support drop frame" if (input =~ /\;/)
|
87
|
+
|
88
|
+
hrs, mins, secs, frames = 0,0,0,0
|
89
|
+
atoms = []
|
90
|
+
|
91
|
+
# 00:00:00:00
|
92
|
+
if (input =~ COMPLETE_TC_RE)
|
93
|
+
atoms = input.scan(COMPLETE_TC_RE).to_a.flatten
|
94
|
+
# 00:00:00.0
|
95
|
+
elsif input =~ FRACTIONAL_TC_RE
|
96
|
+
parse_with_fractional_seconds(input, with_fps)
|
97
|
+
# 10h 20m 10s 1f
|
98
|
+
elsif input =~ /\s/
|
99
|
+
return input.split.map{|part| parse(part, with_fps) }.inject { |sum, p| sum + p.total }
|
100
|
+
# 10s
|
101
|
+
elsif input =~ /^(\d+)s$/
|
102
|
+
return new(input.to_i * with_fps, with_fps)
|
103
|
+
# 10h
|
104
|
+
elsif input =~ /^(\d+)h$/i
|
105
|
+
return new(input.to_i * 60 * 60 * with_fps, with_fps)
|
106
|
+
# 20m
|
107
|
+
elsif input =~ /^(\d+)m$/i
|
108
|
+
return new(input.to_i * 60 * with_fps, with_fps)
|
109
|
+
# 60f - 60 frames, or 2 seconds and 10 frames
|
110
|
+
elsif input =~ /^(\d+)f$/i
|
111
|
+
return new(input.to_i, with_fps)
|
112
|
+
# A bunch of integers
|
113
|
+
elsif (input =~ /^(\d+)$/)
|
114
|
+
ints = input.split(//)
|
115
|
+
atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
|
116
|
+
atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
|
117
|
+
atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
|
118
|
+
atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
|
119
|
+
else
|
120
|
+
raise CannotParse, "Cannot parse #{input} into timecode, no match"
|
121
|
+
end
|
122
|
+
|
123
|
+
if atoms.any?
|
124
|
+
hrs, mins, secs, frames = atoms.map{|e| e.to_i}
|
125
|
+
else
|
126
|
+
raise CannotParse, "Cannot parse #{input} into timecode, atoms were empty"
|
127
|
+
end
|
128
|
+
|
129
|
+
at(hrs, mins, secs, frames, with_fps)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Initialize a Timecode object at this specfic timecode
|
133
|
+
def at(hrs, mins, secs, frames, with_fps = DEFAULT_FPS)
|
134
|
+
case true
|
135
|
+
when hrs > 99
|
136
|
+
raise RangeError, "There can be no more than 99 hours, got #{hrs}"
|
137
|
+
when mins > 59
|
138
|
+
raise RangeError, "There can be no more than 59 minutes, got #{mins}"
|
139
|
+
when secs > 59
|
140
|
+
raise RangeError, "There can be no more than 59 seconds, got #{secs}"
|
141
|
+
when frames > (with_fps -1)
|
142
|
+
raise RangeError, "There can be no more than #{with_fps -1} frames @#{with_fps}, got #{frames}"
|
143
|
+
end
|
144
|
+
|
145
|
+
total = (hrs*(60*60*with_fps) + mins*(60*with_fps) + secs*with_fps + frames).round
|
146
|
+
new(total, with_fps)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Parse a timecode with fractional seconds instead of frames. This is how ffmpeg reports
|
150
|
+
# a timecode
|
151
|
+
def parse_with_fractional_seconds(tc_with_fractions_of_second, fps = DEFAULT_FPS)
|
152
|
+
fraction_expr = /\.(\d+)$/
|
153
|
+
fraction_part = ('.' + tc_with_fractions_of_second.scan(fraction_expr)[0][0]).to_f
|
154
|
+
|
155
|
+
seconds_per_frame = 1.0 / fps.to_f
|
156
|
+
frame_idx = (fraction_part / seconds_per_frame).floor
|
157
|
+
|
158
|
+
tc_with_frameno = tc_with_fractions_of_second.gsub(fraction_expr, ":%02d" % frame_idx)
|
159
|
+
|
160
|
+
parse(tc_with_frameno, fps)
|
161
|
+
end
|
162
|
+
|
163
|
+
# create a timecode from the number of seconds. This is how current time is supplied by
|
164
|
+
# QuickTime and other systems which have non-frame-based timescales
|
165
|
+
def from_seconds(seconds_float, the_fps = DEFAULT_FPS)
|
166
|
+
total_frames = (seconds_float.to_f * the_fps.to_f).ceil
|
167
|
+
new(total_frames, the_fps)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Some systems (like SGIs) and DPX format store timecode as unsigned integer, bit-packed. This method
|
171
|
+
# unpacks such an integer into a timecode.
|
172
|
+
def from_uint(uint, fps = DEFAULT_FPS)
|
173
|
+
tc_elements = (0..7).to_a.reverse.map do | multiplier |
|
174
|
+
((uint >> (multiplier * 4)) & 0x0F)
|
175
|
+
end.join.scan(/(\d{2})/).flatten.map{|e| e.to_i}
|
176
|
+
|
177
|
+
tc_elements << fps
|
178
|
+
at(*tc_elements)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# is the timecode at 00:00:00:00
|
183
|
+
def zero?
|
184
|
+
@total.zero?
|
185
|
+
end
|
186
|
+
|
187
|
+
# get total frame count
|
188
|
+
def total
|
189
|
+
to_f
|
190
|
+
end
|
191
|
+
|
192
|
+
# get FPS
|
193
|
+
def fps
|
194
|
+
@fps
|
195
|
+
end
|
196
|
+
|
197
|
+
# get the number of frames
|
198
|
+
def frames
|
199
|
+
value_parts[3]
|
200
|
+
end
|
201
|
+
|
202
|
+
# get the number of seconds
|
203
|
+
def seconds
|
204
|
+
value_parts[2]
|
205
|
+
end
|
206
|
+
|
207
|
+
# get the number of minutes
|
208
|
+
def minutes
|
209
|
+
value_parts[1]
|
210
|
+
end
|
211
|
+
|
212
|
+
# get the number of hours
|
213
|
+
def hours
|
214
|
+
value_parts[0]
|
215
|
+
end
|
216
|
+
|
217
|
+
# get frame interval in fractions of a second
|
218
|
+
def frame_interval
|
219
|
+
1.0/@fps
|
220
|
+
end
|
221
|
+
|
222
|
+
# get the timecode as bit-packed unsigned 32 bit int (suitable for DPX and SGI)
|
223
|
+
def to_uint
|
224
|
+
elements = (("%02d" * 4) % [hours,minutes,seconds,frames]).split(//).map{|e| e.to_i }
|
225
|
+
uint = 0
|
226
|
+
elements.reverse.each_with_index do | p, i |
|
227
|
+
uint |= p << 4 * i
|
228
|
+
end
|
229
|
+
uint
|
230
|
+
end
|
231
|
+
|
232
|
+
# Convert to different framerate based on the total frames. Therefore,
|
233
|
+
# 1 second of PAL video will convert to 25 frames of NTSC (this
|
234
|
+
# is suitable for PAL to film TC conversions and back).
|
235
|
+
def convert(new_fps)
|
236
|
+
self.class.new(@total, new_fps)
|
237
|
+
end
|
238
|
+
|
239
|
+
# get formatted SMPTE timecode
|
240
|
+
def to_s
|
241
|
+
WITH_FRAMES % value_parts
|
242
|
+
end
|
243
|
+
|
244
|
+
# get total frames as float
|
245
|
+
def to_f
|
246
|
+
@total
|
247
|
+
end
|
248
|
+
|
249
|
+
# get total frames as integer
|
250
|
+
def to_i
|
251
|
+
@total
|
252
|
+
end
|
253
|
+
|
254
|
+
# add number of frames (or another timecode) to this one
|
255
|
+
def +(arg)
|
256
|
+
if (arg.is_a?(Timecode) && framerate_in_delta(arg.fps, @fps))
|
257
|
+
Timecode.new(@total+arg.total, @fps)
|
258
|
+
elsif (arg.is_a?(Timecode))
|
259
|
+
raise WrongFramerate, "You are calculating timecodes with different framerates"
|
260
|
+
else
|
261
|
+
Timecode.new(@total + arg, @fps)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Subtract a number of frames
|
266
|
+
def -(arg)
|
267
|
+
if (arg.is_a?(Timecode) && framerate_in_delta(arg.fps, @fps))
|
268
|
+
Timecode.new(@total-arg.total, @fps)
|
269
|
+
elsif (arg.is_a?(Timecode))
|
270
|
+
raise WrongFramerate, "You are calculating timecodes with different framerates"
|
271
|
+
else
|
272
|
+
Timecode.new(@total-arg, @fps)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Multiply the timecode by a number
|
277
|
+
def *(arg)
|
278
|
+
raise RangeError, "Timecode multiplier cannot be negative" if (arg < 0)
|
279
|
+
Timecode.new(@total*arg.to_i, @fps)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Get the next frame
|
283
|
+
def succ
|
284
|
+
self.class.new(@total + 1, @fps)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Get the number of times a passed timecode fits into this time span (if performed with Timecode) or
|
288
|
+
# a Timecode that multiplied by arg will give this one
|
289
|
+
def /(arg)
|
290
|
+
arg.is_a?(Timecode) ? (@total / arg.total) : Timecode.new(@total /arg, @fps)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Timecodes can be compared to each other
|
294
|
+
def <=>(other_tc)
|
295
|
+
if other_tc.is_a?(Timecode) && framerate_in_delta(fps, other_tc.fps)
|
296
|
+
self.total <=> other_tc.total
|
297
|
+
else
|
298
|
+
self.total <=> other_tc
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# FFmpeg expects a fraction of a second as the last element instead of number of frames. Use this
|
303
|
+
# method to get the timecode that adheres to that expectation. The return of this method can be fed
|
304
|
+
# to ffmpeg directly.
|
305
|
+
# Timecode.parse("00:00:10:24", 25).with_frames_as_fraction #=> "00:00:10.96"
|
306
|
+
def with_frames_as_fraction
|
307
|
+
vp = value_parts.dup
|
308
|
+
vp[-1] = (100.0 / @fps) * vp[-1]
|
309
|
+
WITH_FRACTIONS_OF_SECOND % vp
|
310
|
+
end
|
311
|
+
alias_method :with_fractional_seconds, :with_frames_as_fraction
|
312
|
+
|
313
|
+
# Validate that framerates are within a small delta deviation considerable for floats
|
314
|
+
def framerate_in_delta(one, two)
|
315
|
+
(one.to_f - two.to_f).abs <= ALLOWED_FPS_DELTA
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
|
320
|
+
# Formats the actual timecode output from the number of frames
|
321
|
+
def validate!
|
322
|
+
frames = @total
|
323
|
+
secs = (@total.to_f/@fps).floor
|
324
|
+
frames-=(secs*@fps)
|
325
|
+
mins = (secs/60).floor
|
326
|
+
secs -= (mins*60)
|
327
|
+
hrs = (mins/60).floor
|
328
|
+
mins-= (hrs*60)
|
329
|
+
|
330
|
+
raise RangeError, "Timecode cannot be longer that 99 hrs" if hrs > 99
|
331
|
+
raise RangeError, "More than 59 minutes" if mins > 59
|
332
|
+
raise RangeError, "More than 59 seconds" if secs > 59
|
333
|
+
raise RangeError, "More than #{@fps.to_s} frames (#{frames}) in the last second" if frames >= @fps
|
334
|
+
|
335
|
+
[hrs, mins, secs, frames]
|
336
|
+
end
|
337
|
+
|
338
|
+
def value_parts
|
339
|
+
@value ||= validate!
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'test/spec'
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + '/../lib/timecode'
|
6
|
+
|
7
|
+
|
8
|
+
context "Timecode on instantiation should" do
|
9
|
+
|
10
|
+
specify "be instantable from int" do
|
11
|
+
tc = Timecode.new(10)
|
12
|
+
tc.should.be.kind_of Timecode
|
13
|
+
tc.total.should.equal 10
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "always coerce FPS to float" do
|
17
|
+
Timecode.new(10, 24).fps.should.be.kind_of(Float)
|
18
|
+
Timecode.new(10, 25.0).fps.should.be.kind_of(Float)
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "create a zero TC with no arguments" do
|
22
|
+
Timecode.new(nil).should.be.zero?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "An existing Timecode should" do
|
27
|
+
|
28
|
+
before do
|
29
|
+
@five_seconds = Timecode.new(5*25, 25)
|
30
|
+
@one_and_a_half_film = (90 * 60) * 24
|
31
|
+
@film_tc = Timecode.new(@one_and_a_half_film, 24)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
specify "report that the framerates are in delta" do
|
36
|
+
tc = Timecode.new(1)
|
37
|
+
tc.framerate_in_delta(25.0000000000000001, 25.0000000000000003).should.equal(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "validate equality based on delta" do
|
41
|
+
t1, t2 = Timecode.new(10, 25.0000000000000000000000000001), Timecode.new(10, 25.0000000000000000000000000002)
|
42
|
+
t1.should.equal(t2)
|
43
|
+
end
|
44
|
+
|
45
|
+
specify "report total as it's to_i" do
|
46
|
+
Timecode.new(10).to_i.should.equal(10)
|
47
|
+
end
|
48
|
+
|
49
|
+
specify "support hours" do
|
50
|
+
@five_seconds.should.respond_to :hours
|
51
|
+
@five_seconds.hours.should.equal 0
|
52
|
+
@film_tc.hours.should.equal 1
|
53
|
+
end
|
54
|
+
|
55
|
+
specify "support minutes" do
|
56
|
+
@five_seconds.should.respond_to :minutes
|
57
|
+
@five_seconds.minutes.should.equal 0
|
58
|
+
@film_tc.minutes.should.equal 30
|
59
|
+
end
|
60
|
+
|
61
|
+
specify "support seconds" do
|
62
|
+
@five_seconds.should.respond_to :seconds
|
63
|
+
@five_seconds.seconds.should.equal 5
|
64
|
+
@film_tc.seconds.should.equal 0
|
65
|
+
end
|
66
|
+
|
67
|
+
specify "support frames" do
|
68
|
+
@film_tc.frames.should.equal 0
|
69
|
+
end
|
70
|
+
|
71
|
+
specify "report frame_interval as a float" do
|
72
|
+
tc = Timecode.new(10)
|
73
|
+
tc.should.respond_to :frame_interval
|
74
|
+
|
75
|
+
tc.frame_interval.should.be.close 0.04, 0.0001
|
76
|
+
tc = Timecode.new(10, 30)
|
77
|
+
tc.frame_interval.should.be.close 0.03333, 0.0001
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
context "A Timecode of zero should" do
|
83
|
+
specify "properly respond to zero?" do
|
84
|
+
Timecode.new(0).should.respond_to :zero?
|
85
|
+
Timecode.new(0).should.be.zero
|
86
|
+
Timecode.new(1).should.not.be.zero
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "An existing TImecode on inspection should" do
|
91
|
+
specify "properly present himself via inspect" do
|
92
|
+
Timecode.new(10, 25).inspect.should.equal "#<Timecode:00:00:00:10 (10F@25.00)>"
|
93
|
+
end
|
94
|
+
|
95
|
+
specify "properly print itself" do
|
96
|
+
Timecode.new(5, 25).to_s.should.equal "00:00:00:05"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "An existing Timecode used within ranges should" do
|
101
|
+
specify "properly provide successive value that is one frame up" do
|
102
|
+
Timecode.new(10).succ.total.should.equal 11
|
103
|
+
Timecode.new(22).succ.should.equal Timecode.new(23)
|
104
|
+
end
|
105
|
+
|
106
|
+
specify "work as a range member" do
|
107
|
+
r = Timecode.new(10)...Timecode.new(20)
|
108
|
+
r.to_a.length.should.equal 10
|
109
|
+
r.to_a[4].should.equal Timecode.new(14)
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
context "A Timecode on conversion should" do
|
115
|
+
specify "copy itself with a different framerate" do
|
116
|
+
tc = Timecode.new(40,25)
|
117
|
+
at24 = tc.convert(24)
|
118
|
+
at24.total.should.equal 40
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "A Timecode on calculations should" do
|
123
|
+
|
124
|
+
specify "support addition" do
|
125
|
+
a, b = Timecode.new(24, 25.000000000000001), Timecode.new(22, 25.000000000000002)
|
126
|
+
(a + b).should.equal Timecode.new(24 + 22, 25.000000000000001)
|
127
|
+
end
|
128
|
+
|
129
|
+
specify "should raise on addition if framerates do not match" do
|
130
|
+
lambda{ Timecode.new(10, 25) + Timecode.new(10, 30) }.should.raise(Timecode::WrongFramerate)
|
131
|
+
end
|
132
|
+
|
133
|
+
specify "when added with an integer instead calculate on total" do
|
134
|
+
(Timecode.new(5) + 5).should.equal(Timecode.new(10))
|
135
|
+
end
|
136
|
+
|
137
|
+
specify "support subtraction" do
|
138
|
+
a, b = Timecode.new(10), Timecode.new(4)
|
139
|
+
(a - b).should.equal Timecode.new(6)
|
140
|
+
end
|
141
|
+
|
142
|
+
specify "on subtraction of an integer instead calculate on total" do
|
143
|
+
(Timecode.new(15) - 5).should.equal Timecode.new(10)
|
144
|
+
end
|
145
|
+
|
146
|
+
specify "raise when subtracting a Timecode with a different framerate" do
|
147
|
+
lambda { Timecode.new(10, 25) - Timecode.new(10, 30) }.should.raise(Timecode::WrongFramerate)
|
148
|
+
end
|
149
|
+
|
150
|
+
specify "support multiplication" do
|
151
|
+
(Timecode.new(10) * 10).should.equal(Timecode.new(100))
|
152
|
+
end
|
153
|
+
|
154
|
+
specify "raise when the resultig Timecode is negative" do
|
155
|
+
lambda { Timecode.new(10) * -200 }.should.raise(Timecode::RangeError)
|
156
|
+
end
|
157
|
+
|
158
|
+
specify "yield a Timecode when divided by an Integer" do
|
159
|
+
v = Timecode.new(200) / 20
|
160
|
+
v.should.be.kind_of(Timecode)
|
161
|
+
v.should.equal Timecode.new(10)
|
162
|
+
end
|
163
|
+
|
164
|
+
specify "yield a number when divided by another Timecode" do
|
165
|
+
v = Timecode.new(200) / Timecode.new(20)
|
166
|
+
v.should.be.kind_of(Numeric)
|
167
|
+
v.should.equal 10
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context "A Timecode used with fractional number of seconds" do
|
172
|
+
|
173
|
+
specify "should properly return fractional seconds" do
|
174
|
+
tc = Timecode.new(100 -1, fps = 25)
|
175
|
+
tc.frames.should.equal 24
|
176
|
+
|
177
|
+
tc.with_frames_as_fraction.should.equal "00:00:03.96"
|
178
|
+
tc.with_fractional_seconds.should.equal "00:00:03.96"
|
179
|
+
end
|
180
|
+
|
181
|
+
specify "properly translate to frames when instantiated from fractional seconds" do
|
182
|
+
fraction = 7.1
|
183
|
+
tc = Timecode.from_seconds(fraction, 10)
|
184
|
+
tc.to_s.should.equal "00:00:07:01"
|
185
|
+
|
186
|
+
fraction = 7.5
|
187
|
+
tc = Timecode.from_seconds(fraction, 10)
|
188
|
+
tc.to_s.should.equal "00:00:07:05"
|
189
|
+
|
190
|
+
fraction = 7.16
|
191
|
+
tc = Timecode.from_seconds(fraction, 12.5)
|
192
|
+
tc.to_s.should.equal "00:00:07:02"
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
context "Timecode.at() should" do
|
198
|
+
|
199
|
+
specify "disallow more than 99 hrs" do
|
200
|
+
lambda{ Timecode.at(99,0,0,0) }.should.not.raise
|
201
|
+
lambda{ Timecode.at(100,0,0,0) }.should.raise(Timecode::RangeError)
|
202
|
+
end
|
203
|
+
|
204
|
+
specify "disallow more than 59 minutes" do
|
205
|
+
lambda{ Timecode.at(1,60,0,0) }.should.raise(Timecode::RangeError)
|
206
|
+
end
|
207
|
+
|
208
|
+
specify "disallow more than 59 seconds" do
|
209
|
+
lambda{ Timecode.at(1,0,60,0) }.should.raise(Timecode::RangeError)
|
210
|
+
end
|
211
|
+
|
212
|
+
specify "disallow more frames than what the framerate permits" do
|
213
|
+
lambda{ Timecode.at(1,0,60,25, 25) }.should.raise(Timecode::RangeError)
|
214
|
+
lambda{ Timecode.at(1,0,60,32, 30) }.should.raise(Timecode::RangeError)
|
215
|
+
end
|
216
|
+
|
217
|
+
specify "propery accept usable values" do
|
218
|
+
Timecode.at(20, 20, 10, 5).to_s.should.equal "20:20:10:05"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
context "Timecode.parse() should" do
|
224
|
+
|
225
|
+
specify "handle complete SMPTE timecode" do
|
226
|
+
simple_tc = "00:10:34:10"
|
227
|
+
Timecode.parse(simple_tc).to_s.should.equal(simple_tc)
|
228
|
+
end
|
229
|
+
|
230
|
+
specify "handle complete SMPTE timecode via new" do
|
231
|
+
simple_tc = "00:10:34:10"
|
232
|
+
Timecode.new(simple_tc).to_s.should.equal(simple_tc)
|
233
|
+
end
|
234
|
+
|
235
|
+
specify "refuse to handle timecode that is out of range for the framerate" do
|
236
|
+
bad_tc = "00:76:89:30"
|
237
|
+
lambda { Timecode.parse(bad_tc, 25) }.should.raise(Timecode::RangeError)
|
238
|
+
end
|
239
|
+
|
240
|
+
specify "parse a row of numbers as parts of a timecode starting from the right" do
|
241
|
+
Timecode.parse("10").should.equal Timecode.new(10)
|
242
|
+
Timecode.parse("210").should.equal Timecode.new(60)
|
243
|
+
Timecode.parse("10101010").to_s.should.equal "10:10:10:10"
|
244
|
+
end
|
245
|
+
|
246
|
+
specify "parse a number with f suffix as frames" do
|
247
|
+
Timecode.parse("60f").should.equal Timecode.new(60)
|
248
|
+
end
|
249
|
+
|
250
|
+
specify "parse a number with s suffix as seconds" do
|
251
|
+
Timecode.parse("2s", 25).should.equal Timecode.new(50, 25)
|
252
|
+
Timecode.parse("2s", 30).should.equal Timecode.new(60, 30)
|
253
|
+
end
|
254
|
+
|
255
|
+
specify "parse a number with m suffix as minutes" do
|
256
|
+
Timecode.parse("3m").should.equal Timecode.new(25 * 60 * 3)
|
257
|
+
end
|
258
|
+
|
259
|
+
specify "parse a number with h suffix as hours" do
|
260
|
+
Timecode.parse("3h").should.equal Timecode.new(25 * 60 * 60 * 3)
|
261
|
+
end
|
262
|
+
|
263
|
+
specify "parse different suffixes as a sum of elements" do
|
264
|
+
Timecode.parse("1h 4f").to_s.should.equal '01:00:00:04'
|
265
|
+
Timecode.parse("4f 1h").to_s.should.equal '01:00:00:04'
|
266
|
+
Timecode.parse("29f 1h").to_s.should.equal '01:00:01:04'
|
267
|
+
end
|
268
|
+
|
269
|
+
specify "parse timecode with fractional second instead of frames" do
|
270
|
+
fraction = "00:00:07.1"
|
271
|
+
tc = Timecode.parse_with_fractional_seconds(fraction, 10)
|
272
|
+
tc.to_s.should.equal "00:00:07:01"
|
273
|
+
|
274
|
+
fraction = "00:00:07.5"
|
275
|
+
tc = Timecode.parse_with_fractional_seconds(fraction, 10)
|
276
|
+
tc.to_s.should.equal "00:00:07:05"
|
277
|
+
|
278
|
+
fraction = "00:00:07.04"
|
279
|
+
tc = Timecode.parse_with_fractional_seconds(fraction, 12.5)
|
280
|
+
tc.to_s.should.equal "00:00:07:00"
|
281
|
+
|
282
|
+
fraction = "00:00:07.16"
|
283
|
+
tc = Timecode.parse_with_fractional_seconds(fraction, 12.5)
|
284
|
+
tc.to_s.should.equal "00:00:07:02"
|
285
|
+
end
|
286
|
+
|
287
|
+
specify "raise on improper format" do
|
288
|
+
lambda { Timecode.parse("Meaningless nonsense", 25) }.should.raise Timecode::CannotParse
|
289
|
+
lambda { Timecode.parse("", 25) }.should.raise Timecode::CannotParse
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
context "Timecode.soft_parse should" do
|
295
|
+
specify "parse the timecode" do
|
296
|
+
Timecode.soft_parse('200').to_s.should.equal "00:00:02:00"
|
297
|
+
end
|
298
|
+
|
299
|
+
specify "not raise on improper format and return zero TC instead" do
|
300
|
+
lambda do
|
301
|
+
tc = Timecode.soft_parse("Meaningless nonsense", 25)
|
302
|
+
tc.should.be.zero?
|
303
|
+
end.should.not.raise
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context "Timecode with unsigned integer conversions should" do
|
308
|
+
|
309
|
+
specify "parse from a 4x4bits packed 32bit unsigned int" do
|
310
|
+
uint, tc = 87310853, Timecode.at(5,34,42,5)
|
311
|
+
Timecode.from_uint(uint).should.equal tc
|
312
|
+
end
|
313
|
+
|
314
|
+
specify "properly convert itself back to 4x4 bits 32bit unsigned int" do
|
315
|
+
uint, tc = 87310853, Timecode.at(5,34,42,5)
|
316
|
+
tc.to_uint.should.equal uint
|
317
|
+
end
|
318
|
+
end
|
data/timecode.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{timecode}
|
3
|
+
s.version = "0.1.4"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Julik"]
|
7
|
+
s.date = %q{2009-01-14}
|
8
|
+
s.description = %q{Value class for SMPTE timecode information}
|
9
|
+
s.email = ["me@julik.nl"]
|
10
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt", "SPECS.txt"]
|
11
|
+
s.files = ["History.txt", "Manifest.txt", "README.txt", "SPECS.txt", "Rakefile", "lib/timecode.rb", "test/test_timecode.rb", "timecode.gemspec"]
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.homepage = %q{http://wiretap.rubyforge.org/timecode}
|
14
|
+
s.rdoc_options = ["--main", "README.txt"]
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubyforge_project = %q{wiretap}
|
17
|
+
s.rubygems_version = %q{1.3.1}
|
18
|
+
s.summary = %q{Value class for SMPTE timecode information}
|
19
|
+
s.test_files = ["test/test_timecode.rb"]
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<test-spec>, [">= 0"])
|
27
|
+
s.add_development_dependency(%q<hoe>, [">= 1.8.2"])
|
28
|
+
else
|
29
|
+
s.add_dependency(%q<test-spec>, [">= 0"])
|
30
|
+
s.add_dependency(%q<hoe>, [">= 1.8.2"])
|
31
|
+
end
|
32
|
+
else
|
33
|
+
s.add_dependency(%q<test-spec>, [">= 0"])
|
34
|
+
s.add_dependency(%q<hoe>, [">= 1.8.2"])
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: julik-timecode
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julik
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-14 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: test-spec
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: hoe
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.8.2
|
32
|
+
version:
|
33
|
+
description: Value class for SMPTE timecode information
|
34
|
+
email:
|
35
|
+
- me@julik.nl
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- History.txt
|
42
|
+
- Manifest.txt
|
43
|
+
- README.txt
|
44
|
+
- SPECS.txt
|
45
|
+
files:
|
46
|
+
- History.txt
|
47
|
+
- Manifest.txt
|
48
|
+
- README.txt
|
49
|
+
- SPECS.txt
|
50
|
+
- Rakefile
|
51
|
+
- lib/timecode.rb
|
52
|
+
- test/test_timecode.rb
|
53
|
+
- timecode.gemspec
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://wiretap.rubyforge.org/timecode
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README.txt
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: wiretap
|
77
|
+
rubygems_version: 1.2.0
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: Value class for SMPTE timecode information
|
81
|
+
test_files:
|
82
|
+
- test/test_timecode.rb
|