edl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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