julik-timecode 0.1.4

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/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