timecode 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-12-15
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,6 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/timecode.rb
6
+ test/test_timecode.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = timecode
2
+
3
+ * http://projects.juli.nl/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,9 @@
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.rubyforge_name = 'wiretap'
9
+ end
data/lib/timecode.rb ADDED
@@ -0,0 +1,327 @@
1
+ # Timecode is a convenience object for calculating SMPTE timecode natively. It is used in
2
+ # various StoryTool models and templates, offers string output and is immutable.
3
+ #
4
+ # The promise is that you only have to store two values to know the timecode - the amount
5
+ # of frames and the framerate. An additional perk might be to save the dropframeness,
6
+ # but we avoid that at this point.
7
+ #
8
+ # You can calculate in timecode objects ass well as with conventional integers and floats.
9
+ # Timecode is immutable and can be used as a value object. Timecode objects are sortable.
10
+ #
11
+ # Here's how to use it with ActiveRecord (your column names will be source_tc_frames_total and tape_fps)
12
+ #
13
+ # composed_of :source_tc, :class_name => 'Timecode',
14
+ # :mapping => [%w(source_tc_frames total), %w(tape_fps fps)]
15
+
16
+ class Timecode
17
+ VERSION = '0.1.0'
18
+
19
+ include Comparable
20
+ DEFAULT_FPS = 25
21
+ COMPLETE_TC_RE = /^(\d{1,2}):(\d{1,2}):(\d{1,2}):(\d{1,2})$/
22
+
23
+ # All Timecode lib errors inherit from this
24
+ class Error < RuntimeError; end
25
+
26
+ # Will be raised for functions that are not supported
27
+ class TimecodeLibError < Error; end
28
+
29
+ # Gets raised if timecode is out of range (like 100 hours long)
30
+ class RangeError < Error; end
31
+
32
+ # Self-explanatory
33
+ class NonPositiveFps < RangeError; end
34
+
35
+ # Gets raised when float frame count is passed
36
+ class FrameIsWhole < RangeError; end
37
+
38
+ # Gets raised when you divide by zero
39
+ class TcIsZero < ZeroDivisionError; end
40
+
41
+ # Gets raised when a timecode cannot be parsed
42
+ class CannotParse < Error; end
43
+
44
+ # Gets raised when you try to compute two timecodes with different framerates together
45
+ class WrongFramerate < ArgumentError; end
46
+
47
+ # Well well...
48
+ class MethodRequiresTimecode < ArgumentError; end
49
+
50
+ # Initialize a new Timecode. If a string is passed, it will be parsed, an integer
51
+ # will be interpreted as the total number of frames
52
+ def self.new(total_or_string = 0, fps = DEFAULT_FPS)
53
+ if total_or_string.nil?
54
+ new(0, fps)
55
+ elsif total_or_string.is_a?(String)
56
+ parse(total_or_string, fps)
57
+ else
58
+ super(total_or_string, fps)
59
+ end
60
+ end
61
+
62
+ def initialize(total = 0, fps = DEFAULT_FPS) # :nodoc:
63
+ if total.is_a?(Float)
64
+ raise FrameIsWhole, "the number of frames cannot be partial (Integer value needed)"
65
+ end
66
+
67
+ raise RangeError, "Timecode cannot be negative" if total.to_f < 0
68
+ raise WrongFramerate, "FPS cannot be zero" if fps.zero?
69
+ @total, @fps = total, fps
70
+ @value = validate!
71
+ freeze
72
+ end
73
+
74
+ def inspect # :nodoc:
75
+ super.gsub(/@fps/, self.to_s + ' @fps').gsub(/ @value=\[(.+)\],/, '')
76
+ end
77
+
78
+ TIME_FIELDS = 7 # :nodoc:
79
+
80
+ class << self
81
+
82
+ # Parse timecode and return zero if none matched
83
+ def soft_parse(input, with_fps = DEFAULT_FPS)
84
+ parse(input) rescue new(0, with_fps)
85
+ end
86
+
87
+ # Parse timecode entered by the user. Will raise if the string cannot be parsed.
88
+ def parse(input, with_fps = DEFAULT_FPS)
89
+ # Drop frame goodbye
90
+ raise TimecodeLibError, "We do not support drop frame" if (input =~ /\;/)
91
+
92
+ hrs, mins, secs, frames = 0,0,0,0
93
+ atoms = []
94
+
95
+ # 10h 20m 10s 1f
96
+ if input =~ /\s/
97
+ return input.split.map{|part| parse(part, with_fps) }.inject { |sum, p| sum + p.total }
98
+ # 10s
99
+ elsif input =~ /^(\d+)s$/
100
+ return new(input.to_i * with_fps, with_fps)
101
+ # 10h
102
+ elsif input =~ /^(\d+)h$/i
103
+ return new(input.to_i * 60 * 60 * with_fps, with_fps)
104
+ # 20m
105
+ elsif input =~ /^(\d+)m$/i
106
+ return new(input.to_i * 60 * with_fps, with_fps)
107
+ # 60f - 60 frames, or 2 seconds and 10 frames
108
+ elsif input =~ /^(\d+)f$/i
109
+ return new(input.to_i, with_fps)
110
+ # A bunch of integers
111
+ elsif (input =~ /^(\d+)$/)
112
+ ints = input.split(//)
113
+ atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
114
+ atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
115
+ atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
116
+ atoms.unshift [ints.pop, ints.pop].reverse.join.to_i
117
+ elsif (input =~ COMPLETE_TC_RE)
118
+ atoms = input.scan(COMPLETE_TC_RE).to_a.flatten
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
+ def parse_with_fractional_seconds(tc_with_fractions_of_second, fps = DEFAULT_FPS)
150
+ fraction_expr = /\.(\d+)$/
151
+ fraction_part = ('.' + tc_with_fractions_of_second.scan(fraction_expr)[0][0]).to_f
152
+
153
+ seconds_per_frame = 1.0 / fps.to_f
154
+ frame_idx = (fraction_part / seconds_per_frame).floor
155
+
156
+ tc_with_frameno = tc_with_fractions_of_second.gsub(fraction_expr, ":#{frame_idx}")
157
+
158
+ parse(tc_with_frameno, fps)
159
+ end
160
+
161
+ # create a timecode from seconds. Seconds can be float (this is how current time is supplied by
162
+ # QuickTime and other systems which have non-frame-based timescales)
163
+ def from_seconds(seconds_float, the_fps = DEFAULT_FPS)
164
+ total_frames = (seconds_float.to_f * the_fps.to_f).ceil
165
+ new(total_frames, the_fps)
166
+ end
167
+
168
+
169
+ # Some systems (like SGIs) and DPX format store timecode as unsigned integer, bit-packed
170
+ def from_uint(uint)
171
+ shift = 4 * TIME_FIELDS
172
+ tc_elements = (0..TIME_FIELDS).map do
173
+ part = ((uint >> shift) & 0x0F)
174
+ shift -= 4
175
+ part
176
+ end.join.scan(/(\d{2})/).flatten.map{|e| e.to_i}
177
+ at(*tc_elements)
178
+ end
179
+ end
180
+
181
+ # is the timecode at 00:00:00:00
182
+ def zero?
183
+ @total.zero?
184
+ end
185
+
186
+ # get total frame count
187
+ def total
188
+ to_f
189
+ end
190
+
191
+ #get FPS
192
+ def fps
193
+ @fps
194
+ end
195
+
196
+ # get the number of frames
197
+ def frames
198
+ value_parts[3]
199
+ end
200
+
201
+ # get the number of seconds
202
+ def seconds
203
+ value_parts[2]
204
+ end
205
+
206
+ # get the number of minutes
207
+ def minutes
208
+ value_parts[1]
209
+ end
210
+
211
+ # get the number of hours
212
+ def hours
213
+ value_parts[0]
214
+ end
215
+
216
+ # get frame interval in fractions of a second
217
+ def frame_interval
218
+ 1.0/@fps
219
+ end
220
+
221
+ # get the timecode as bit-packed unsigned int (suitable for DPX and SGI)
222
+ def to_uint
223
+ elements = (("%02d" * 4) % [hours,minutes,seconds,frames]).split(//).map{|e| e.to_i }
224
+ uint = 0
225
+ elements.each do | el |
226
+ uint = ((uint >> el))
227
+ end
228
+ uint
229
+ end
230
+
231
+ # Convert to different framerate based on the total frames. Therefore,
232
+ # 1 second of PAL video will convert to 25 frames of NTSC (this
233
+ # is suitable for PAL to film TC conversions and back).
234
+ # It does not account for pulldown or anything in that sense, because
235
+ # then you need to think about cadences and such
236
+ def convert(new_fps)
237
+ raise NonPositiveFps, "FPS cannot be less than 0" if new_fps < 1
238
+ self.class.new((total/fps)*new_fps, new_fps)
239
+ end
240
+
241
+ # get formatted SMPTE timecode
242
+ def to_s
243
+ "%02d:%02d:%02d:%02d" % value_parts
244
+ end
245
+
246
+ # get total frames as float
247
+ def to_f
248
+ @total
249
+ end
250
+
251
+ # get total frames as integer
252
+ def to_i
253
+ @total
254
+ end
255
+
256
+ # add number of frames (or another timecode) to this one
257
+ def +(arg)
258
+ if (arg.is_a?(Timecode) && arg.fps == @fps)
259
+ Timecode.new(@total+arg.total, @fps)
260
+ elsif (arg.is_a?(Timecode))
261
+ raise WrongFramerate, "You are calculating timecodes with different framerates"
262
+ else
263
+ Timecode.new(@total + arg, @fps)
264
+ end
265
+ end
266
+
267
+ # Subtract a number of frames
268
+ def -(arg)
269
+ if (arg.is_a?(Timecode) && arg.fps == @fps)
270
+ Timecode.new(@total-arg.total, @fps)
271
+ elsif (arg.is_a?(Timecode))
272
+ raise WrongFramerate, "You are calculating timecodes with different framerates"
273
+ else
274
+ Timecode.new(@total-arg, @fps)
275
+ end
276
+ end
277
+
278
+ # Multiply the timecode by a number
279
+ def *(arg)
280
+ raise RangeError, "Timecode multiplier cannot be negative" if (arg < 0)
281
+ Timecode.new(@total*arg.to_i, @fps)
282
+ end
283
+
284
+ # Get the next frame
285
+ def succ
286
+ self.class.new(@total + 1)
287
+ end
288
+
289
+ # Slice the timespan in pieces
290
+ def /(arg)
291
+ Timecode.new(@total/arg, @fps)
292
+ end
293
+
294
+ # Timecodes can be compared to each other
295
+ def <=>(other_tc)
296
+ if other_tc.is_a?(Timecode)
297
+ self.total <=> other_tc.class.new(other_tc.total, self.fps).total
298
+ else
299
+ self.total <=> other_tc
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ # Formats the actual timecode output from the number of frames
306
+ def validate!
307
+ frames = @total
308
+ secs = (@total.to_f/@fps).floor
309
+ frames-=(secs*@fps)
310
+ mins = (secs/60).floor
311
+ secs -= (mins*60)
312
+ hrs = (mins/60).floor
313
+ mins-= (hrs*60)
314
+
315
+ raise RangeError, "Timecode cannot be longer that 99 hrs" if hrs > 99
316
+ raise RangeError, "More than 59 minutes" if mins > 59
317
+ raise RangeError, "More than 59 seconds" if secs > 59
318
+ raise TimecodeLibError, "More than #{@fps.to_s} frames (#{frames}) in the last second" if frames >= @fps
319
+
320
+ [hrs, mins, secs, frames]
321
+ end
322
+
323
+ def value_parts
324
+ @value ||= validate!
325
+ end
326
+
327
+ end
@@ -0,0 +1,160 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'timecode'
4
+
5
+ # for Fixnum#hours
6
+ require 'active_support'
7
+
8
+ class TimecodeTest < Test::Unit::TestCase
9
+
10
+ def test_basics
11
+ five_seconds_of_pal = 5.seconds * 25
12
+ tc = Timecode.new(five_seconds_of_pal, 25)
13
+ assert_equal 0, tc.hours
14
+ assert_equal 0, tc.minutes
15
+ assert_equal 5, tc.seconds
16
+ assert_equal 0, tc.frames
17
+ assert_equal five_seconds_of_pal, tc.total
18
+ assert_equal "00:00:05:00", tc.to_s
19
+
20
+ one_and_a_half_hour_of_hollywood = 90.minutes * 24
21
+
22
+ film_tc = Timecode.new(one_and_a_half_hour_of_hollywood, 24)
23
+ assert_equal 1, film_tc.hours
24
+ assert_equal 30, film_tc.minutes
25
+ assert_equal 0, film_tc.seconds
26
+ assert_equal 0, film_tc.frames
27
+ assert_equal one_and_a_half_hour_of_hollywood, film_tc.total
28
+ assert_equal "01:30:00:00", film_tc.to_s
29
+
30
+ assert_equal "01:30:00:04", (film_tc + 4).to_s
31
+ assert_equal "01:30:01:04", (film_tc + 28).to_s
32
+
33
+ assert_raise(Timecode::WrongFramerate) do
34
+ tc + film_tc
35
+ end
36
+
37
+ two_seconds_and_five_frames_of_pal = ((2.seconds * 25) + 5)
38
+ pal_tc = Timecode.new(two_seconds_and_five_frames_of_pal, 25)
39
+ assert_nothing_raised do
40
+ added_tc = pal_tc + tc
41
+ assert_equal "00:00:07:05", added_tc.to_s
42
+ end
43
+
44
+ end
45
+
46
+ def test_parse
47
+ simple_tc = "00:10:34:10"
48
+
49
+ assert_nothing_raised do
50
+ @tc = Timecode.parse(simple_tc)
51
+ assert_equal simple_tc, @tc.to_s
52
+ end
53
+
54
+ bad_tc = "00:76:89:30"
55
+ unknown_gobbledygook = "this is insane"
56
+
57
+ assert_raise(Timecode::CannotParse) do
58
+ tc = Timecode.parse(unknown_gobbledygook, 25)
59
+ end
60
+
61
+ assert_raise(Timecode::RangeError) do
62
+ Timecode.parse(bad_tc, 25)
63
+ end
64
+ end
65
+
66
+ def test_succ
67
+ assert_equal Timecode.new(23), Timecode.new(22).succ
68
+ end
69
+
70
+ def test_zero
71
+ assert Timecode.new(0).zero?
72
+ assert !Timecode.new(1).zero?
73
+ assert !Timecode.new(1000).zero?
74
+ end
75
+
76
+ def test_parse_from_numbers
77
+ assert_equal Timecode.new(10), Timecode.parse("10")
78
+ assert_equal Timecode.new(60), Timecode.parse("210")
79
+ assert_equal "10:10:10:10", Timecode.parse("10101010").to_s
80
+ end
81
+
82
+ def test_parse_with_f
83
+ assert_equal Timecode.new(60), Timecode.parse("60f")
84
+ end
85
+
86
+ def test_parse_s
87
+ assert_equal Timecode.new(50), Timecode.parse("2s")
88
+ assert_equal Timecode.new(60), Timecode.parse("2s", 30)
89
+ end
90
+
91
+ def test_parse_m
92
+ assert_equal Timecode.new(25 * 60 * 3), Timecode.parse("3m")
93
+ end
94
+
95
+ def test_parse_h
96
+ assert_equal Timecode.new(25 * 60 * 60 * 3), Timecode.parse("3h")
97
+ end
98
+
99
+ def test_parse_from_elements
100
+ assert_equal '01:00:00:04', Timecode.parse("1h 4f").to_s
101
+ assert_equal '01:00:00:04', Timecode.parse("4f 1h").to_s
102
+ assert_equal '01:00:01:04', Timecode.parse("29f 1h").to_s
103
+ end
104
+
105
+ def test_float_framerate
106
+ tc = Timecode.new(25, 12.5)
107
+ assert_equal "00:00:02:00", tc.to_s
108
+ end
109
+
110
+ def test_timecode_with_nil_gives_zero
111
+ assert_equal Timecode.new(0), Timecode.new(nil)
112
+ end
113
+
114
+ def test_parse_fractional_tc
115
+ fraction = "00:00:07.1"
116
+ tc = Timecode.parse_with_fractional_seconds(fraction, 10)
117
+ assert_equal "00:00:07:01", tc.to_s
118
+
119
+ fraction = "00:00:07.5"
120
+ tc = Timecode.parse_with_fractional_seconds(fraction, 10)
121
+ assert_equal "00:00:07:05", tc.to_s
122
+
123
+ fraction = "00:00:07.04"
124
+ tc = Timecode.parse_with_fractional_seconds(fraction, 12.5)
125
+ assert_equal "00:00:07:00", tc.to_s
126
+
127
+ fraction = "00:00:07.16"
128
+ tc = Timecode.parse_with_fractional_seconds(fraction, 12.5)
129
+ assert_equal "00:00:07:02", tc.to_s
130
+ end
131
+
132
+ # def test_parse_with_calculation
133
+ # tc = Timecode.parse_with_calculation("00:00:00:15 +2f")
134
+ # assert_equal Timecode.new(17), tc
135
+ # end
136
+
137
+ def test_from_uint
138
+ uint, tc = 87310853, Timecode.at(5,34,42,5)
139
+ assert_equal tc, Timecode.from_uint(uint)
140
+ end
141
+
142
+ def test_to_uint
143
+ uint, tc = 87310853, Timecode.at(5,34,42,5)
144
+ assert_equal uint, tc.to_uint
145
+ end
146
+
147
+ def test_from_seconds
148
+ fraction = 7.1
149
+ tc = Timecode.from_seconds(fraction, 10)
150
+ assert_equal "00:00:07:01", tc.to_s
151
+
152
+ fraction = 7.5
153
+ tc = Timecode.from_seconds(fraction, 10)
154
+ assert_equal "00:00:07:05", tc.to_s
155
+
156
+ fraction = 7.16
157
+ tc = Timecode.from_seconds(fraction, 12.5)
158
+ assert_equal "00:00:07:02", tc.to_s
159
+ end
160
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timecode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Julik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-19 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.2
24
+ version:
25
+ description: Value class for SMPTE timecode information
26
+ email:
27
+ - me@julik.nl
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - lib/timecode.rb
42
+ - test/test_timecode.rb
43
+ has_rdoc: true
44
+ homepage: http://projects.juli.nl/timecode
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project: wiretap
66
+ rubygems_version: 1.3.1
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: Value class for SMPTE timecode information
70
+ test_files:
71
+ - test/test_timecode.rb