edl 0.0.1

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.
@@ -0,0 +1,5 @@
1
+ === 0.0.1 / 2008-12-22
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
@@ -0,0 +1,46 @@
1
+ = EDL
2
+
3
+ == DESCRIPTION:
4
+
5
+ Work with EDL files from Ruby
6
+
7
+
8
+ == SYNOPSIS:
9
+
10
+ list = EDL::Parser.new(fps=25).parse(File.open('OFFLINE.EDL'))
11
+ list.events.each do | evt |
12
+ puts evt.inspect
13
+ end
14
+
15
+ == REQUIREMENTS:
16
+
17
+ * Timecode gem (sudo gem install timecode)
18
+
19
+ == INSTALL:
20
+
21
+ * sudo gem install edl
22
+
23
+ == LICENSE:
24
+
25
+ (The MIT License)
26
+
27
+ Copyright (c) 2008 Julik Tarkhanov <me@julik.nl>
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining
30
+ a copy of this software and associated documentation files (the
31
+ 'Software'), to deal in the Software without restriction, including
32
+ without limitation the rights to use, copy, modify, merge, publish,
33
+ distribute, sublicense, and/or sell copies of the Software, and to
34
+ permit persons to whom the Software is furnished to do so, subject to
35
+ the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be
38
+ included in all copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
41
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
42
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
43
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
44
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
45
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
46
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/edl.rb'
4
+
5
+ Hoe.new('edl', EDL::VERSION) do |p|
6
+ p.rubyforge_name = 'wiretap'
7
+ p.developer('Julik', 'me@julik.nl')
8
+ p.extra_deps << "flexmock" << "timecode"
9
+ end
@@ -0,0 +1,426 @@
1
+ require "rubygems"
2
+ require "timecode"
3
+
4
+ # A simplistic EDL parser. Current limitations: no support for DF timecode, no support for audio,
5
+ # no support for split edits, no support for key effects, no support for audio
6
+ module EDL
7
+ VERSION = "0.0.1"
8
+
9
+ DEFAULT_FPS = 25
10
+
11
+ # Represents an EDL, is returned from the parser. Traditional operation is functional style, i.e.
12
+ # edl.renumbered.without_transitions.without_generators
13
+ class List
14
+ attr_accessor :events, :fps
15
+
16
+ def initialize(events = [])
17
+ @events = events.dup
18
+ end
19
+
20
+ # Return the same EDL with all dissolves stripped and replaced by the clips under them
21
+ def without_transitions
22
+ # Find dissolves
23
+ cpy = []
24
+
25
+ @events.each_with_index do | e, i |
26
+ # A dissolve always FOLLOWS the incoming clip
27
+ if @events[i+1] && @events[i+1].has_transition?
28
+ dissolve = @events[i+1]
29
+ len = dissolve.transition.duration.to_i
30
+
31
+ # The dissolve contains the OUTGOING clip, we are the INCOMING. Extend the
32
+ # incoming clip by the length of the dissolve, that's the whole mission actually
33
+ incoming = e.copy_properties_to(e.class.new)
34
+ incoming.src_end_tc += len
35
+ incoming.rec_end_tc += len
36
+
37
+ outgoing = dissolve.copy_properties_to(Clip.new)
38
+
39
+ # Add the A suffix to the ex-dissolve
40
+ outgoing.num += 'A'
41
+
42
+ # Take care to join the two if they overlap - TODO
43
+ cpy << incoming
44
+ cpy << outgoing
45
+ elsif e.has_transition?
46
+ # Skip, already handled!
47
+ else
48
+ cpy << e.dup
49
+ end
50
+ end
51
+ # Do not renumber events
52
+ # (0...cpy.length).map{|e| cpy[e].num = "%03d" % e }
53
+ self.class.new(cpy)
54
+ end
55
+
56
+ def renumbered
57
+ renumed = @events.dup
58
+ pad = renumed.length.to_s.length
59
+ pad = 3 if pad < 3
60
+
61
+ (0...renumed.length).map{|e| renumed[e].num = "%0#{pad}d" % (e+1) }
62
+ x = self.class.new(renumed)
63
+ puts x.inspect
64
+ x
65
+ end
66
+
67
+ # Return the same EDL with all timewarps expanded to native length. Clip length
68
+ # changes have rippling effect on footage that comes after the timewarped clip
69
+ # (so this is best used in concert with the original EDL where record TC is pristine)
70
+ def without_timewarps
71
+ self.class.new(
72
+ @events.map do | e |
73
+
74
+ if e.has_timewarp?
75
+ repl = e.copy_properties_to(e.class.new)
76
+ from, to = e.timewarp.actual_src_start_tc, e.timewarp.actual_src_end_tc
77
+ repl.src_start_tc, repl.src_end_tc, repl.timewarp = from, to, nil
78
+ repl
79
+ else
80
+ e
81
+ end
82
+ end
83
+ )
84
+ end
85
+
86
+ # Return the same EDL without AX, BL and other GEN events (like slug, text and solids).
87
+ # Usually used in concert with "without_transitions"
88
+ def without_generators
89
+ self.class.new(@events.reject{|e| e.generator? })
90
+ end
91
+
92
+ # Return the list of clips used by this EDL at full capture length
93
+ def capture_list
94
+ without_generators.without_timewarps.spliced.from_zero
95
+ end
96
+
97
+ # Return the same EDL with the first event starting at 00:00:00:00 and all subsequent events
98
+ # shifted accordingly
99
+ def from_zero
100
+ shift_by = @events[0].rec_start_tc
101
+ self.class.new(
102
+ @events.map do | original |
103
+ e = original.dup
104
+ e.rec_start_tc = (e.rec_start_tc - shift_by)
105
+ e.rec_end_tc = (e.rec_end_tc - shift_by)
106
+ e
107
+ end
108
+ )
109
+ end
110
+
111
+ # Return the same EDL with neighbouring clips joined at cuts where applicable (if a clip
112
+ # is divided in two pieces it will be spliced). Most useful in combination with without_timewarps
113
+ def spliced
114
+ spliced_edl = @events.inject([]) do | spliced, cur |
115
+ latest = spliced[-1]
116
+ # Append to latest if splicable
117
+ if latest && (latest.reel == cur.reel) && (cur.src_start_tc == (latest.src_end_tc + 1))
118
+ latest.src_end_tc = cur.src_end_tc
119
+ latest.rec_end_tc = cur.rec_end_tc
120
+ else
121
+ spliced << cur.dup
122
+ end
123
+ spliced
124
+ end
125
+ self.class.new(spliced_edl)
126
+ end
127
+ end
128
+
129
+ # Represents an edit event
130
+ class Event
131
+ attr_accessor :num,
132
+ :reel,
133
+ :track,
134
+ :src_start_tc,
135
+ :src_end_tc,
136
+ :rec_start_tc,
137
+ :rec_end_tc,
138
+ :comments,
139
+ :original_line
140
+
141
+ def to_s
142
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).map{|a| self.send(a).to_s}.join(" ")
143
+ end
144
+
145
+ def inspect
146
+ to_s
147
+ end
148
+
149
+ def copy_properties_to(evt)
150
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).each do | k |
151
+ evt.send("#{k}=", send(k)) if evt.respond_to?(k)
152
+ end
153
+ evt
154
+ end
155
+ end
156
+
157
+ class Clip < Event
158
+ attr_accessor :clip_name, :timewarp
159
+ attr_accessor :transition
160
+
161
+ # Returns true if the clip starts with a transiton (not a jump cut)
162
+ def has_transition?
163
+ !transition.nil?
164
+ end
165
+
166
+ def has_timewarp?
167
+ !timewarp.nil?
168
+ end
169
+
170
+ def black?
171
+ reel == 'BL'
172
+ end
173
+ alias_method :slug?, :black?
174
+
175
+ def length
176
+ (rec_end_tc - rec_start_tc).to_i
177
+ end
178
+
179
+ def generator?
180
+ black? || (%(AX GEN).include?(reel))
181
+ end
182
+ end
183
+
184
+ # Represents a transition. We currently only support dissolves and SMPTE wipes
185
+ # Will be avilable as EDL::Clip#transition
186
+ class Transition
187
+ attr_accessor :duration, :effect
188
+ end
189
+
190
+ # Represents a dissolve
191
+ class Dissolve < Transition; end
192
+
193
+ # Represents an SMPTE wipe
194
+ class Wipe < Transition
195
+ attr_accessor :smpte_wipe_index
196
+ end
197
+
198
+ # Represents a timewarp
199
+ class Timewarp
200
+ attr_accessor :actual_framerate
201
+ attr_accessor :clip
202
+
203
+ def speed_in_percent
204
+ (actual_framerate.to_f / DEFAULT_FPS.to_f ) * 100
205
+ end
206
+
207
+ # Get the actual end of source that is needed for the timewarp to be computed properly,
208
+ # round up to not generate stills at ends of clips
209
+ def actual_src_end_tc
210
+ unless reverse?
211
+ clip.src_start_tc + actual_length_of_source
212
+ else
213
+ clip.src_start_tc
214
+ end
215
+ end
216
+
217
+ def actual_src_start_tc
218
+ unless reverse?
219
+ clip.src_start_tc
220
+ else
221
+ clip.src_start_tc - actual_length_of_source
222
+ end
223
+ end
224
+
225
+ # Returns the true number of frames that is needed to complete the timewarp edit
226
+ def actual_length_of_source
227
+ length_in_edit = (clip.src_end_tc - clip.src_start_tc).to_i
228
+ ((length_in_edit / 25.0) * actual_framerate).ceil.abs
229
+ end
230
+
231
+ def reverse?
232
+ actual_framerate < 0
233
+ end
234
+ end
235
+
236
+ # A generic matcher
237
+ class Matcher
238
+ class ApplyError < RuntimeError
239
+ def initialize(msg, line)
240
+ super("%s - offending line was '%s'" % [msg, line])
241
+ end
242
+ end
243
+
244
+ def initialize(with_regexp)
245
+ @regexp = with_regexp
246
+ end
247
+
248
+ def matches?(line)
249
+ line =~ @regexp
250
+ end
251
+
252
+ def apply(stack, line)
253
+ STDERR.puts "Skipping #{line}"
254
+ end
255
+ end
256
+
257
+ class NameMatcher < Matcher
258
+ def initialize
259
+ super(/\* FROM CLIP NAME:(\s+)(.+)/)
260
+ end
261
+
262
+ def apply(stack, line)
263
+ stack[-1].clip_name = line.scan(@regexp).flatten.pop.strip
264
+ end
265
+ end
266
+
267
+ class EffectMatcher < Matcher
268
+ def initialize
269
+ super(/\* EFFECT NAME:(\s+)(.+)/)
270
+ end
271
+
272
+ def apply(stack, line)
273
+ stack[-1].transition.effect = line.scan(@regexp).flatten.pop.strip
274
+ end
275
+ end
276
+
277
+ class TimewarpMatcher < Matcher
278
+
279
+ attr_reader :fps
280
+
281
+ def initialize(fps)
282
+ @fps = fps
283
+ @regexp = /M2(\s+)(\w+)(\s+)(\-?\d+\.\d+)(\s+)(\d{1,2}):(\d{1,2}):(\d{1,2}):(\d{1,2})/
284
+ end
285
+
286
+ def apply(stack, line)
287
+ matches = line.scan(@regexp).flatten.map{|e| e.strip}.reject{|e| e.nil? || e.empty?}
288
+
289
+ from_reel = matches.shift
290
+ fps = matches.shift
291
+
292
+ begin
293
+ # FIXME
294
+ tw_start_source_tc = Parser.timecode_from_line_elements(matches, @fps)
295
+ rescue Timecode::Error => e
296
+ raise ApplyError, "Invalid TC in timewarp (#{e})", line
297
+ end
298
+
299
+ evt_with_tw = stack.reverse.find{|e| e.src_start_tc == tw_start_source_tc && e.reel == from_reel }
300
+
301
+ unless evt_with_tw
302
+ raise ApplyError, "Cannot find event marked by timewarp", line
303
+ else
304
+ tw = Timewarp.new
305
+ tw.actual_framerate, tw.clip = fps.to_f, evt_with_tw
306
+ evt_with_tw.timewarp = tw
307
+ end
308
+ end
309
+ end
310
+
311
+ # Drop frame goodbye
312
+ TC = /(\d{1,2}):(\d{1,2}):(\d{1,2}):(\d{1,2})/
313
+
314
+ class EventMatcher < Matcher
315
+
316
+ # 021 009 V C 00:39:04:21 00:39:05:09 01:00:26:17 01:00:27:05
317
+ EVENT_PAT = /(\d+)(\s+)(\w+)(\s+)(\w+)(\s+)(\w+)(\s+)((\w+\s+)?)#{TC} #{TC} #{TC} #{TC}/
318
+
319
+ attr_reader :fps
320
+
321
+ def initialize(some_fps)
322
+ super(EVENT_PAT)
323
+ @fps = some_fps
324
+ end
325
+
326
+ def apply(stack, line)
327
+
328
+ matches = line.scan(@regexp).shift
329
+ props = {:original_line => line}
330
+
331
+ # FIrst one is the event number
332
+ props[:num] = matches.shift
333
+ matches.shift
334
+
335
+ # Then the reel
336
+ props[:reel] = matches.shift
337
+ matches.shift
338
+
339
+ # Then the track
340
+ props[:track] = matches.shift
341
+ matches.shift
342
+
343
+ # Then the type
344
+ props[:transition] = matches.shift
345
+ matches.shift
346
+
347
+ # Then the optional generator group - skip for now
348
+ if props[:transition] != 'C'
349
+ props[:duration] = matches.shift.strip
350
+ else
351
+ matches.shift
352
+ end
353
+ matches.shift
354
+
355
+ # Then the timecodes
356
+ [:src_start_tc, :src_end_tc, :rec_start_tc, :rec_end_tc].each do | k |
357
+ begin
358
+ PROPS[K] = edl::pARSER.TIMECODE_FROM_LINE_ELEMENTS(MATCHES, @FPS)
359
+ rescue Timecode::Error => e
360
+ raise ApplyError, "Cannot parse timecode - #{e}", line
361
+ end
362
+ end
363
+
364
+ evt = Clip.new
365
+ transition_idx = props.delete(:transition)
366
+ evt.transition = case transition_idx
367
+ when 'D'
368
+ d = Dissolve.new
369
+ d.duration = props.delete(:duration)
370
+ d
371
+ when /W/
372
+ w = Wipe.new
373
+ w.duration = props.delete(:duration)
374
+ w.smpte_wipe_index = transition_idx.gsub(/W/, '')
375
+ w
376
+ else
377
+ nil
378
+ end
379
+
380
+ props.each_pair { | k, v | evt.send("#{k}=", v) }
381
+
382
+ stack << evt
383
+ evt # FIXME - we dont need to return this is only used by tests
384
+ end
385
+ end
386
+
387
+ class Parser
388
+
389
+ attr_reader :fps
390
+
391
+ # Initialize an EDL parser. Pass the FPS to it, as the usual EDL does not contain any kind of reference
392
+ # to it's framerate
393
+ def initialize(with_fps = DEFAULT_FPS)
394
+ @fps = with_fps
395
+ end
396
+
397
+ def get_matchers
398
+ [ EventMatcher.new(@fps), EffectMatcher.new, NameMatcher.new, TimewarpMatcher.new(@fps) ]
399
+ end
400
+
401
+ def parse(io)
402
+ stack, matchers = [], get_matchers
403
+ until io.eof?
404
+ current_line = io.gets.strip
405
+ matchers.each do | matcher |
406
+ next unless matcher.matches?(current_line)
407
+
408
+ begin
409
+ matcher.apply(stack, current_line)
410
+ rescue Matcher::ApplyError => e
411
+ STDERR.puts "Cannot parse #{current_line} - #{e}"
412
+ end
413
+ end
414
+ end
415
+
416
+ return List.new(stack)
417
+ end
418
+
419
+ def self.timecode_from_line_elements(elements, fps)
420
+ args = (0..3).map{|_| elements.shift.to_i} + [fps]
421
+ Timecode.at(*args)
422
+ end
423
+ end
424
+
425
+
426
+ end
@@ -0,0 +1,34 @@
1
+ module EDL
2
+ # Can chop an offline edit into events according to the EDL
3
+ class Cutter
4
+ def initialize(source_path)
5
+ @source_path = source_path
6
+ end
7
+
8
+ def cut(edl)
9
+ source_for_cutting = edl.from_zero #.without_transitions.without_generators
10
+ # We need to use the original length in record
11
+ source_for_cutting.events.each do | evt |
12
+ cut_segment(evt, evt.rec_start_tc, evt.rec_start_tc + evt.length)
13
+ end
14
+ end
15
+
16
+ def cut_segment(evt, start_at, end_at)
17
+ STDERR.puts "Cutting #{@source_path} from #{start_at} to #{end_at} - #{evt.num}"
18
+ end
19
+ end
20
+
21
+ class FFMpegCutter < Cutter
22
+ def cut_segment(evt, start_at, end_at)
23
+ source_dir, source_file = File.dirname(@source_path), File.basename(@source_path)
24
+ dest_segment = File.join(source_dir, ('%s_%s' % [evt.num, source_file]))
25
+ # dest_segment.gsub!(/\.mov$/i, '.mp4')
26
+
27
+ offset = end_at - start_at
28
+
29
+ cmd = "/opt/local/bin/ffmpeg -i #{@source_path} -ss #{start_at} -vframes #{offset.total} -vcodec photojpeg -acodec copy #{dest_segment}"
30
+ #puts cmd
31
+ `#{cmd}`
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module EDL
2
+ # When initialized with a file and passed an EDL, will generate thumbnail images
3
+ # of the first frame of every event. It is assumed that the movie file starts at the same
4
+ # frame as the first EDL event.
5
+ class Grabber
6
+ attr_accessor :ffmpeg_bin, :offset
7
+ def initialize(with_file)
8
+ @source_path = with_file
9
+ end
10
+
11
+ def ffmpeg_bin
12
+ @ffmpeg_bin || 'ffmpeg'
13
+ end
14
+
15
+ def grab(edl)
16
+ edl.from_zero.events.each do | evt |
17
+ grab_frame_tc = evt.rec_start_tc + (offset || 0 )
18
+
19
+ to_file = File.dirname(@source_path) + '/' + evt.num + '_' + File.basename(@source_path).gsub(/\.(\w+)$/, '')
20
+ generate_grab(evt.num, grab_frame_tc, to_file)
21
+ end
22
+ end
23
+
24
+ def generate_grab(evt, at, to_file)
25
+ # cmd = "#{ffmpeg_bin} -i #{@source_path} -an -ss #{at} -vframes 1 -r #{at.fps} -y #{to_file}%d.jpg"
26
+ cmd = "#{ffmpeg_bin} -i #{@source_path} -an -ss #{at} -vframes 1 -y #{to_file}%d.jpg"
27
+ `#{cmd}`
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ TITLE: EDIT 15 [14-11-08] 45 SEC EDL
2
+ 001 106771 V C 04:00:57:05 04:00:58:10 10:00:00:00 10:00:01:05
3
+ 002 106771 V C 04:03:44:06 04:03:45:08 10:00:01:05 10:00:02:07
4
+ 003 106770 V C 00:06:55:13 00:06:57:01 10:00:02:07 10:00:03:20
5
+ 004 106771 V C 03:16:35:05 03:16:36:05 10:00:03:20 10:00:04:20
6
+ 005 106770 V C 00:13:03:17 00:13:04:20 10:00:04:20 10:00:05:23
7
+ 006 106771 V C 03:11:43:02 03:11:44:04 10:00:05:23 10:00:07:00
8
+ 007 106857 V C 07:18:40:24 07:18:41:17 10:00:07:00 10:00:07:18
9
+ 008 106857 V C 07:22:25:19 07:22:27:02 10:00:07:18 10:00:09:01
10
+ 009 106863 V C 13:12:46:23 13:12:48:11 10:00:09:01 10:00:10:14
11
+ 010 106857 V C 08:00:32:00 08:00:33:10 10:00:10:14 10:00:11:24
12
+ 011 106857 V C 08:05:36:18 08:05:37:19 10:00:11:24 10:00:13:00
13
+ 012 106857 V C 07:04:47:06 07:04:50:13 10:00:13:00 10:00:16:07
14
+ M2 106857 012.5 07:04:47:06
15
+ 013 106863 V C 10:11:06:07 10:11:08:00 10:00:16:07 10:00:18:00
16
+ 014 106863 V C 10:09:16:21 10:09:18:11 10:00:18:00 10:00:19:15
17
+ 015 106857 V C 07:06:02:17 07:06:04:07 10:00:19:15 10:00:21:05
18
+ 016 106857 V C 09:07:18:07 09:07:20:16 10:00:21:05 10:00:23:14
19
+ M2 106857 002.5 09:07:18:07
20
+ 017 106863 V C 11:02:46:06 11:02:47:06 10:00:23:14 10:00:24:14
21
+ 018 106863 V C 11:10:17:04 11:10:17:20 10:00:24:14 10:00:25:05
22
+ 019 106863 V C 12:06:09:18 12:06:10:16 10:00:25:05 10:00:26:03
23
+ 020 106863 V C 12:10:49:17 12:10:51:02 10:00:26:03 10:00:27:13
24
+ 021 106815 V C 05:01:22:21 05:01:24:05 10:00:27:13 10:00:28:22
25
+ 022 106815 V C 06:01:35:03 06:01:36:18 10:00:28:22 10:00:30:12
26
+ 023 106815 V C 05:13:38:19 05:13:40:10 10:00:30:12 10:00:32:03
27
+ M2 106815 -025.0 05:13:38:19
28
+ 024 106857 V C 09:13:03:09 09:13:05:11 10:00:32:03 10:00:34:05
29
+ M2 106857 -025.0 09:13:03:09
30
+ 025 106857 V C 09:13:01:07 09:13:01:07 10:00:34:05 10:00:34:05
31
+ 025 106771 V D 022 03:11:55:00 03:11:56:17 10:00:34:05 10:00:35:22
32
+ M2 106857 -025.0 09:13:01:07
33
+ * BLEND, DISSOLVE
34
+ 026 106771 V C 03:11:56:17 03:11:56:17 10:00:35:22 10:00:35:22
35
+ 026 106771 V D 012 03:06:30:09 03:06:31:19 10:00:35:22 10:00:37:07
36
+ * BLEND, DISSOLVE
37
+ 027 106863 V C 13:15:29:20 13:15:31:02 10:00:37:07 10:00:38:14
38
+ 028 106771 V C 03:15:40:22 03:15:42:11 10:00:38:14 10:00:40:03
39
+ 029 106863 V C 13:15:51:02 13:15:52:08 10:00:40:03 10:00:41:09
40
+ 030 106863 V C 13:15:52:08 13:15:52:08 10:00:41:09 10:00:41:09
41
+ 030 BL V D 032 00:00:00:00 00:00:01:07 10:00:41:09 10:00:42:16
42
+ * BLEND, DISSOLVE
43
+ >>> SOURCE 106770 106770 4919975f.793b8d33
44
+ >>> SOURCE 106771 106771 4919a179.79630563
45
+ >>> SOURCE 106815 106815 4919b521.79afcb70
46
+ >>> SOURCE 106857 106857 4919cbb9.7a080d9d
47
+ >>> SOURCE 106863 106863 4919e0c3.7a5a3cfc
48
+ 
@@ -0,0 +1,3 @@
1
+ TITLE: REVERSED_EDL
2
+ 001 106857 V C 09:13:03:09 09:13:05:11 10:00:32:03 10:00:34:05
3
+ M2 106857 -025.0 09:13:03:09
@@ -0,0 +1,9 @@
1
+ TITLE: SIMPLE_DISSOLVE
2
+ FCM: NON-DROP FRAME
3
+
4
+ 001 BL V C 00:00:00:00 00:00:00:00 01:00:00:00 01:00:00:00
5
+ 001 006I V D 043 06:42:50:18 06:42:52:13 01:00:00:00 01:00:01:20
6
+ * EFFECT NAME: CROSS DISSOLVE
7
+ * TO CLIP NAME: TAPE_6-10.MOV
8
+ * COMMENT:
9
+
@@ -0,0 +1,8 @@
1
+ TITLE: SPLICEME
2
+ FCM: NON-DROP FRAME
3
+
4
+ 001 006I V C 06:42:50:18 06:42:52:13 01:00:00:00 01:00:01:20
5
+
6
+ 002 006I V C 06:42:52:14 06:42:52:16 01:00:01:21 01:00:01:23
7
+
8
+ 003 006I V C 06:42:52:17 06:42:52:24 01:00:01:24 01:00:02:06
@@ -0,0 +1,5 @@
1
+ TITLE: TIMEWARP_EDL
2
+ FCM: NON-DROP FRAME
3
+
4
+ 001 003 V C 03:03:19:19 03:03:20:04 01:00:04:03 01:00:04:13
5
+ M2 003 309.6 03:03:19:19
@@ -0,0 +1,5 @@
1
+ TITLE: TIMEWARP_HALF_EDL
2
+ FCM: NON-DROP FRAME
3
+
4
+ 001 003 V C 03:03:19:19 03:03:20:04 01:00:04:03 01:00:04:13
5
+ M2 003 12.5 03:03:19:19
@@ -0,0 +1,303 @@
1
+ require File.dirname(__FILE__) + '/../lib/edl'
2
+ require File.dirname(__FILE__) + '/../lib/edl/cutter'
3
+ require File.dirname(__FILE__) + '/../lib/edl/grabber'
4
+
5
+ require 'rubygems'
6
+ require 'test/unit'
7
+ require 'flexmock'
8
+ require 'flexmock/test_unit'
9
+
10
+ TRAILER_EDL = File.dirname(__FILE__) + '/samples/TRAILER_EDL.edl'
11
+ SIMPLE_DISSOLVE = File.dirname(__FILE__) + '/samples/SIMPLE_DISSOLVE.EDL'
12
+ SPLICEME = File.dirname(__FILE__) + '/samples/SPLICEME.EDL'
13
+ SIMPLE_TIMEWARP = File.dirname(__FILE__) + '/samples/TIMEWARP.EDL'
14
+ SLOMO_TIMEWARP = File.dirname(__FILE__) + '/samples/TIMEWARP_HALF.EDL'
15
+ FORTY_FIVER = File.dirname(__FILE__) + '/samples/45S_SAMPLE.EDL'
16
+ REVERSE = File.dirname(__FILE__) + '/samples/REVERSE.EDL'
17
+
18
+ class TestEvent < Test::Unit::TestCase
19
+ def test_attributes_defined
20
+ evt = EDL::Event.new
21
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc ).each do | em |
22
+ assert_respond_to evt, em
23
+ end
24
+ end
25
+ end
26
+
27
+ class Test::Unit::TestCase
28
+ def parse_evt(matcher_klass, line)
29
+ stack = []
30
+ matcher_klass.new.apply(stack, line)
31
+ stack.pop
32
+ end
33
+ end
34
+
35
+ class TestParser < Test::Unit::TestCase
36
+ def test_inst
37
+ assert_nothing_raised { EDL::Parser.new }
38
+ end
39
+
40
+
41
+ def test_inits_matchers_with_framerate
42
+ p = EDL::Parser.new(30)
43
+ matchers = p.get_matchers
44
+ event_matcher = matchers.find{|e| e.is_a?(EDL::EventMatcher) }
45
+ assert_equal 30, event_matcher.fps
46
+ end
47
+
48
+ def test_timecode_from_elements
49
+ elems = ["08", "04", "24", "24"]
50
+ assert_nothing_raised { @tc = EDL::Parser.timecode_from_line_elements(elems, 30) }
51
+ assert_kind_of Timecode, @tc
52
+ assert_equal "08:04:24:24", @tc.to_s
53
+ assert_equal 30, @tc.fps
54
+ assert elems.empty?, "The elements used for timecode should have been removed from the array"
55
+ end
56
+
57
+ def test_dissolve
58
+ p = EDL::Parser.new
59
+ assert_nothing_raised{ @edl = p.parse File.open(SIMPLE_DISSOLVE) }
60
+ assert_kind_of EDL::List, @edl
61
+ assert_equal 2, @edl.events.length
62
+
63
+ first = @edl.events[0]
64
+ assert_kind_of EDL::Clip, first
65
+
66
+ second = @edl.events[1]
67
+ assert_kind_of EDL::Clip, second
68
+ assert second.has_transition?
69
+
70
+ no_trans = @edl.without_transitions
71
+
72
+ assert_equal 2, no_trans.events.length
73
+ target_tc = (Timecode.parse('01:00:00:00') + 43)
74
+ assert_equal target_tc, no_trans.events[0].rec_end_tc,
75
+ "The incoming clip should have been extended by the length of the dissolve"
76
+
77
+ target_tc = Timecode.parse('01:00:00:00')
78
+ assert_equal target_tc, no_trans.events[1].rec_start_tc
79
+ "The outgoing clip should have been left in place"
80
+ end
81
+
82
+ def test_spliced
83
+ p = EDL::Parser.new
84
+ assert_nothing_raised{ @edl = p.parse(File.open(SPLICEME)) }
85
+ assert_equal 3, @edl.events.length
86
+
87
+ spliced = @edl.spliced
88
+ assert_equal 1, spliced.events.length, "Should have been spliced to one event"
89
+ end
90
+ end
91
+
92
+ class TimewarpMatcherTest < Test::Unit::TestCase
93
+
94
+ def test_parses_as_one_event
95
+ @edl = EDL::Parser.new.parse(File.open(SIMPLE_TIMEWARP))
96
+ assert_kind_of EDL::List, @edl
97
+ assert_equal 1, @edl.events.length
98
+ end
99
+
100
+ def test_timewarp_attributes
101
+ @edl = EDL::Parser.new.parse(File.open(SIMPLE_TIMEWARP))
102
+ assert_kind_of EDL::List, @edl
103
+ assert_equal 1, @edl.events.length
104
+
105
+ clip = @edl.events[0]
106
+ assert clip.has_timewarp?, "Should respond true to has_timewarp?"
107
+ assert_not_nil clip.timewarp
108
+ assert_kind_of EDL::Timewarp, clip.timewarp
109
+
110
+ assert clip.timewarp.actual_src_end_tc > clip.src_end_tc
111
+ assert_equal "03:03:24:18", clip.timewarp.actual_src_end_tc.to_s
112
+ assert_equal 124, clip.timewarp.actual_length_of_source
113
+ assert !clip.timewarp.reverse?
114
+
115
+ end
116
+
117
+ def test_timwarp_slomo
118
+ @edl = EDL::Parser.new.parse(File.open(SLOMO_TIMEWARP))
119
+ clip = @edl.events[0]
120
+ assert clip.has_timewarp?, "Should respond true to has_timewarp?"
121
+ assert_not_nil clip.timewarp
122
+ assert_kind_of EDL::Timewarp, clip.timewarp
123
+
124
+ assert clip.timewarp.actual_src_end_tc < clip.src_end_tc
125
+ assert_equal "03:03:19:24", clip.timewarp.actual_src_end_tc.to_s
126
+ assert_equal 10, clip.length
127
+ assert_equal 5, clip.timewarp.actual_length_of_source
128
+ assert_equal 50, clip.timewarp.speed_in_percent.to_i
129
+ assert !clip.timewarp.reverse?
130
+
131
+ end
132
+ end
133
+
134
+ class ReverseTimewarpTest < Test::Unit::TestCase
135
+ def test_parse
136
+ @edl = EDL::Parser.new.parse(File.open(REVERSE))
137
+ assert_equal 1, @edl.events.length
138
+
139
+ clip = @edl.events[0]
140
+ assert_equal 52, clip.length
141
+
142
+ assert clip.has_timewarp?, "Should respond true to has_timewarp?"
143
+ tw = clip.timewarp
144
+
145
+ assert_equal( -25, tw.actual_framerate.to_i)
146
+ assert tw.reverse?
147
+ assert_equal clip.length, tw.actual_length_of_source
148
+ assert_equal clip.src_start_tc, tw.actual_src_end_tc
149
+ assert_equal clip.src_start_tc - 52, tw.actual_src_start_tc
150
+ assert_equal( -100, clip.timewarp.speed_in_percent.to_i)
151
+
152
+ end
153
+ end
154
+
155
+ class EventMatcherTest < Test::Unit::TestCase
156
+
157
+ EVT_PATTERNS = [
158
+ '020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17',
159
+ '021 009 V C 00:39:04:21 00:39:05:09 01:00:26:17 01:00:27:05',
160
+ '022 008C V C 08:08:01:23 08:08:02:18 01:00:27:05 01:00:28:00',
161
+ '023 008C V C 08:07:30:02 08:07:30:21 01:00:28:00 01:00:28:19',
162
+ '024 AX V C 00:00:00:00 00:00:01:00 01:00:28:19 01:00:29:19',
163
+ '025 BL V C 00:00:00:00 00:00:00:00 01:00:29:19 01:00:29:19',
164
+ '025 GEN V D 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20',
165
+ ]
166
+
167
+ def test_clip_generation_from_line
168
+ m = EDL::EventMatcher.new(25)
169
+
170
+ clip = m.apply([],
171
+ '020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17'
172
+ )
173
+
174
+ assert_not_nil clip
175
+ assert_kind_of EDL::Clip, clip
176
+ assert_equal '020', clip.num
177
+ assert_equal '008C', clip.reel
178
+ assert_equal 'V', clip.track
179
+ assert_equal '08:04:24:24', clip.src_start_tc.to_s
180
+ assert_equal '08:04:25:19', clip.src_end_tc.to_s
181
+ assert_equal '01:00:25:22', clip.rec_start_tc.to_s
182
+ assert_equal '01:00:26:17', clip.rec_end_tc.to_s
183
+ assert_equal '020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17', clip.original_line
184
+ end
185
+
186
+ def test_dissolve_generation_from_line
187
+ m = EDL::EventMatcher.new(25)
188
+ dissolve = m.apply([],
189
+ '025 GEN V D 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20'
190
+ )
191
+ assert_not_nil dissolve
192
+ assert_kind_of EDL::Clip, dissolve
193
+ assert_equal '025', dissolve.num
194
+ assert_equal 'GEN', dissolve.reel
195
+ assert_equal 'V', dissolve.track
196
+
197
+ assert dissolve.has_transition?
198
+ assert_not_nil dissolve.transition
199
+ assert_kind_of EDL::Dissolve, dissolve.transition
200
+ assert_equal '025', dissolve.transition.duration
201
+ end
202
+
203
+ def test_wipe_generation_from_line
204
+ m = EDL::EventMatcher.new(25)
205
+ wipe = m.apply([],
206
+ '025 GEN V W001 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20'
207
+ )
208
+ assert_not_nil wipe
209
+ assert_kind_of EDL::Clip, wipe
210
+ assert wipe.generator?
211
+ assert_equal '025', wipe.num
212
+ assert_equal 'GEN', wipe.reel
213
+ assert_equal 'V', wipe.track
214
+
215
+ assert wipe.has_transition?
216
+
217
+ assert_not_nil wipe.transition
218
+ assert_kind_of EDL::Wipe, wipe.transition
219
+ assert_equal '025', wipe.transition.duration
220
+ assert_equal '001', wipe.transition.smpte_wipe_index
221
+ end
222
+
223
+ def test_black_generation_from_line
224
+ m = EDL::EventMatcher.new(25)
225
+ black = m.apply([],
226
+ '025 BL V C 00:00:00:00 00:00:00:00 01:00:29:19 01:00:29:19'
227
+ )
228
+
229
+ assert_not_nil black
230
+
231
+ assert black.black?, "Black should be black?"
232
+ assert black.slug?, "Black should be slug?"
233
+
234
+ assert black.generator?, "Should be generator?"
235
+ assert_equal '025', black.num
236
+ assert_equal 'BL', black.reel
237
+ assert_equal 'V', black.track
238
+ assert_equal '025 BL V C 00:00:00:00 00:00:00:00 01:00:29:19 01:00:29:19', black.original_line
239
+ end
240
+
241
+ def test_matches_all_patterns
242
+ EVT_PATTERNS.each do | pat |
243
+ assert EDL::EventMatcher.new(25).matches?(pat), "EventMatcher should match #{pat}"
244
+ end
245
+ end
246
+ end
247
+
248
+ class ClipNameMatcherTest < Test::Unit::TestCase
249
+ def test_matches
250
+ line = "* FROM CLIP NAME: TAPE_6-10.MOV"
251
+ assert EDL::NameMatcher.new.matches?(line)
252
+ end
253
+
254
+ def test_apply
255
+ line = "* FROM CLIP NAME: TAPE_6-10.MOV"
256
+ mok_evt = flexmock
257
+ mok_evt.should_receive(:clip_name=).with('TAPE_6-10.MOV').once
258
+ EDL::NameMatcher.new.apply([mok_evt], line)
259
+ end
260
+ end
261
+
262
+ class EffectMatcherTest < Test::Unit::TestCase
263
+ def test_matches
264
+ line = "* EFFECT NAME: CROSS DISSOLVE"
265
+ assert EDL::EffectMatcher.new.matches?(line)
266
+ end
267
+
268
+ def test_apply
269
+ line = "* EFFECT NAME: CROSS DISSOLVE"
270
+ mok_evt, mok_transition = flexmock, flexmock
271
+
272
+ mok_evt.should_receive(:transition).once.and_return(mok_transition)
273
+ mok_transition.should_receive(:effect=).with("CROSS DISSOLVE").once
274
+
275
+ EDL::EffectMatcher.new.apply([mok_evt], line)
276
+ end
277
+ end
278
+
279
+ class ComplexTest < Test::Unit::TestCase
280
+ def test_parses_cleanly
281
+ assert_nothing_raised { EDL::Parser.new.parse(File.open(FORTY_FIVER)) }
282
+ end
283
+
284
+ def test_from_zero
285
+ complex = EDL::Parser.new.parse(File.open(FORTY_FIVER))
286
+
287
+ from_zero = complex.from_zero
288
+ assert_equal '00:00:00:00', from_zero.events[0].rec_start_tc.to_s,
289
+ "The starting timecode of the first event should have been shifted to zero"
290
+ assert_equal '00:00:42:16', from_zero.events[-1].rec_end_tc.to_s,
291
+ "The ending timecode of the last event should have been shifted 10 hours back"
292
+ end
293
+ end
294
+
295
+ # class GrabberTest < Test::Unit::TestCase
296
+ # FILM = '/Users/julik/Downloads/HC_CORRECT-TCS_VIDEO1.edl.txt'
297
+ # def test_cutter
298
+ # complex = EDL::Parser.new.parse(File.open(FILM))
299
+ # cutter = EDL::Grabber.new("/Users/julik/Desktop/Cutto/HC_CORRECT-TCS.mov")
300
+ # cutter.ffmpeg_bin = '/opt/local/bin/ffmpeg'
301
+ # cutter.grab(complex)
302
+ # end
303
+ # end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: edl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-27 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: flexmock
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: timecode
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: hoe
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.2
44
+ version:
45
+ description: Work with EDL files from Ruby
46
+ email:
47
+ - me@julik.nl
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - History.txt
54
+ - README.txt
55
+ files:
56
+ - lib/edl.rb
57
+ - lib/edl/cutter.rb
58
+ - lib/edl/grabber.rb
59
+ - test/samples/45S_SAMPLE.EDL
60
+ - test/samples/REVERSE.EDL
61
+ - test/samples/SIMPLE_DISSOLVE.EDL
62
+ - test/samples/SPLICEME.EDL
63
+ - test/samples/TIMEWARP.EDL
64
+ - test/samples/TIMEWARP_HALF.EDL
65
+ - test/samples/TRAILER_EDL.edl
66
+ - test/test_edl.rb
67
+ - Rakefile
68
+ - History.txt
69
+ - README.txt
70
+ has_rdoc: true
71
+ homepage:
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --main
75
+ - README.txt
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ requirements: []
91
+
92
+ rubyforge_project: wiretap
93
+ rubygems_version: 1.3.1
94
+ signing_key:
95
+ specification_version: 2
96
+ summary: Work with EDL files from Ruby
97
+ test_files:
98
+ - test/test_edl.rb