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 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
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ SPECS.txt
5
+ Rakefile
6
+ lib/timecode.rb
7
+ test/test_timecode.rb
8
+ timecode.gemspec
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