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