timecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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