edl 0.0.1 → 0.0.2

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/lib/edl.rb CHANGED
@@ -1,31 +1,34 @@
1
1
  require "rubygems"
2
2
  require "timecode"
3
+ require 'stringio'
4
+
5
+ require File.dirname(__FILE__) + '/edl/event'
6
+ require File.dirname(__FILE__) + '/edl/transition'
7
+ require File.dirname(__FILE__) + '/edl/timewarp'
3
8
 
4
9
  # A simplistic EDL parser. Current limitations: no support for DF timecode, no support for audio,
5
10
  # no support for split edits, no support for key effects, no support for audio
6
11
  module EDL
7
- VERSION = "0.0.1"
8
-
12
+ VERSION = "0.0.2"
9
13
  DEFAULT_FPS = 25
10
14
 
11
15
  # Represents an EDL, is returned from the parser. Traditional operation is functional style, i.e.
12
16
  # edl.renumbered.without_transitions.without_generators
13
- class List
14
- attr_accessor :events, :fps
17
+ class List < Array
15
18
 
16
- def initialize(events = [])
17
- @events = events.dup
19
+ def events #:nodoc:
20
+ STDERR.puts "EDL::List#events is deprecated and will be removed, use EDL::List as an array instead"
21
+ self
18
22
  end
19
23
 
20
- # Return the same EDL with all dissolves stripped and replaced by the clips under them
24
+ # Return the same EDL with all dissolves stripped and replaced by the clips underneath
21
25
  def without_transitions
22
26
  # Find dissolves
23
27
  cpy = []
24
-
25
- @events.each_with_index do | e, i |
28
+ each_with_index do | e, i |
26
29
  # A dissolve always FOLLOWS the incoming clip
27
- if @events[i+1] && @events[i+1].has_transition?
28
- dissolve = @events[i+1]
30
+ if e.ends_with_transition?
31
+ dissolve = self[i+1]
29
32
  len = dissolve.transition.duration.to_i
30
33
 
31
34
  # The dissolve contains the OUTGOING clip, we are the INCOMING. Extend the
@@ -34,7 +37,7 @@ module EDL
34
37
  incoming.src_end_tc += len
35
38
  incoming.rec_end_tc += len
36
39
 
37
- outgoing = dissolve.copy_properties_to(Clip.new)
40
+ outgoing = dissolve.copy_properties_to(Event.new)
38
41
 
39
42
  # Add the A suffix to the ex-dissolve
40
43
  outgoing.num += 'A'
@@ -43,7 +46,7 @@ module EDL
43
46
  cpy << incoming
44
47
  cpy << outgoing
45
48
  elsif e.has_transition?
46
- # Skip, already handled!
49
+ # Skip, already handled on the previous clip
47
50
  else
48
51
  cpy << e.dup
49
52
  end
@@ -53,15 +56,14 @@ module EDL
53
56
  self.class.new(cpy)
54
57
  end
55
58
 
59
+ # Return the same EDL, with events renumbered starting from 001
56
60
  def renumbered
57
- renumed = @events.dup
61
+ renumed = self.dup
58
62
  pad = renumed.length.to_s.length
59
63
  pad = 3 if pad < 3
60
64
 
61
65
  (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
66
+ self.class.new(renumed)
65
67
  end
66
68
 
67
69
  # Return the same EDL with all timewarps expanded to native length. Clip length
@@ -69,8 +71,7 @@ module EDL
69
71
  # (so this is best used in concert with the original EDL where record TC is pristine)
70
72
  def without_timewarps
71
73
  self.class.new(
72
- @events.map do | e |
73
-
74
+ map do | e |
74
75
  if e.has_timewarp?
75
76
  repl = e.copy_properties_to(e.class.new)
76
77
  from, to = e.timewarp.actual_src_start_tc, e.timewarp.actual_src_end_tc
@@ -86,7 +87,7 @@ module EDL
86
87
  # Return the same EDL without AX, BL and other GEN events (like slug, text and solids).
87
88
  # Usually used in concert with "without_transitions"
88
89
  def without_generators
89
- self.class.new(@events.reject{|e| e.generator? })
90
+ self.class.new(self.reject{|e| e.generator? })
90
91
  end
91
92
 
92
93
  # Return the list of clips used by this EDL at full capture length
@@ -97,9 +98,9 @@ module EDL
97
98
  # Return the same EDL with the first event starting at 00:00:00:00 and all subsequent events
98
99
  # shifted accordingly
99
100
  def from_zero
100
- shift_by = @events[0].rec_start_tc
101
+ shift_by = self[0].rec_start_tc
101
102
  self.class.new(
102
- @events.map do | original |
103
+ map do | original |
103
104
  e = original.dup
104
105
  e.rec_start_tc = (e.rec_start_tc - shift_by)
105
106
  e.rec_end_tc = (e.rec_end_tc - shift_by)
@@ -111,7 +112,7 @@ module EDL
111
112
  # Return the same EDL with neighbouring clips joined at cuts where applicable (if a clip
112
113
  # is divided in two pieces it will be spliced). Most useful in combination with without_timewarps
113
114
  def spliced
114
- spliced_edl = @events.inject([]) do | spliced, cur |
115
+ spliced_edl = inject([]) do | spliced, cur |
115
116
  latest = spliced[-1]
116
117
  # Append to latest if splicable
117
118
  if latest && (latest.reel == cur.reel) && (cur.src_start_tc == (latest.src_end_tc + 1))
@@ -126,112 +127,7 @@ module EDL
126
127
  end
127
128
  end
128
129
 
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
130
+ #:stopdoc:
235
131
 
236
132
  # A generic matcher
237
133
  class Matcher
@@ -246,7 +142,7 @@ module EDL
246
142
  end
247
143
 
248
144
  def matches?(line)
249
- line =~ @regexp
145
+ !!(line =~ @regexp)
250
146
  end
251
147
 
252
148
  def apply(stack, line)
@@ -254,6 +150,18 @@ module EDL
254
150
  end
255
151
  end
256
152
 
153
+ # EDL clip comment matcher, a generic one
154
+ class CommentMatcher < Matcher
155
+ def initialize
156
+ super(/\* (.+)/)
157
+ end
158
+
159
+ def apply(stack, line)
160
+ stack[-1].comments << line.scan(@regexp).flatten.pop.strip
161
+ end
162
+ end
163
+
164
+ # Clip name matcher
257
165
  class NameMatcher < Matcher
258
166
  def initialize
259
167
  super(/\* FROM CLIP NAME:(\s+)(.+)/)
@@ -261,6 +169,7 @@ module EDL
261
169
 
262
170
  def apply(stack, line)
263
171
  stack[-1].clip_name = line.scan(@regexp).flatten.pop.strip
172
+ CommentMatcher.new.apply(stack, line)
264
173
  end
265
174
  end
266
175
 
@@ -271,6 +180,7 @@ module EDL
271
180
 
272
181
  def apply(stack, line)
273
182
  stack[-1].transition.effect = line.scan(@regexp).flatten.pop.strip
183
+ CommentMatcher.new.apply(stack, line)
274
184
  end
275
185
  end
276
186
 
@@ -297,7 +207,7 @@ module EDL
297
207
  end
298
208
 
299
209
  evt_with_tw = stack.reverse.find{|e| e.src_start_tc == tw_start_source_tc && e.reel == from_reel }
300
-
210
+
301
211
  unless evt_with_tw
302
212
  raise ApplyError, "Cannot find event marked by timewarp", line
303
213
  else
@@ -326,7 +236,7 @@ module EDL
326
236
  def apply(stack, line)
327
237
 
328
238
  matches = line.scan(@regexp).shift
329
- props = {:original_line => line}
239
+ props = {}
330
240
 
331
241
  # FIrst one is the event number
332
242
  props[:num] = matches.shift
@@ -355,28 +265,33 @@ module EDL
355
265
  # Then the timecodes
356
266
  [:src_start_tc, :src_end_tc, :rec_start_tc, :rec_end_tc].each do | k |
357
267
  begin
358
- PROPS[K] = edl::pARSER.TIMECODE_FROM_LINE_ELEMENTS(MATCHES, @FPS)
268
+ props[k] = EDL::Parser.timecode_from_line_elements(matches, @fps)
359
269
  rescue Timecode::Error => e
360
270
  raise ApplyError, "Cannot parse timecode - #{e}", line
361
271
  end
362
272
  end
363
273
 
364
- evt = Clip.new
274
+ evt = Event.new
365
275
  transition_idx = props.delete(:transition)
366
276
  evt.transition = case transition_idx
367
277
  when 'D'
368
278
  d = Dissolve.new
369
- d.duration = props.delete(:duration)
279
+ d.duration = props.delete(:duration).to_i
370
280
  d
371
- when /W/
281
+ when /W(\d+)/
372
282
  w = Wipe.new
373
- w.duration = props.delete(:duration)
283
+ w.duration = props.delete(:duration).to_i
374
284
  w.smpte_wipe_index = transition_idx.gsub(/W/, '')
375
285
  w
376
286
  else
377
287
  nil
378
288
  end
379
289
 
290
+ # Give a hint on the incoming clip as well
291
+ if evt.transition && stack[-1]
292
+ stack[-1].outgoing_transition_duration = evt.transition.duration
293
+ end
294
+
380
295
  props.each_pair { | k, v | evt.send("#{k}=", v) }
381
296
 
382
297
  stack << evt
@@ -384,6 +299,9 @@ module EDL
384
299
  end
385
300
  end
386
301
 
302
+ #:startdoc:
303
+
304
+ # Is used to parse an EDL
387
305
  class Parser
388
306
 
389
307
  attr_reader :fps
@@ -394,12 +312,15 @@ module EDL
394
312
  @fps = with_fps
395
313
  end
396
314
 
397
- def get_matchers
398
- [ EventMatcher.new(@fps), EffectMatcher.new, NameMatcher.new, TimewarpMatcher.new(@fps) ]
315
+ def get_matchers #:nodoc:
316
+ [ EventMatcher.new(@fps), EffectMatcher.new, NameMatcher.new, TimewarpMatcher.new(@fps), CommentMatcher.new ]
399
317
  end
400
318
 
319
+ # Parse a passed File or IO object line by line, or the whole string
401
320
  def parse(io)
402
- stack, matchers = [], get_matchers
321
+ return parse(StringIO.new(io.to_s)) unless io.respond_to?(:eof?)
322
+
323
+ stack, matchers = List.new, get_matchers
403
324
  until io.eof?
404
325
  current_line = io.gets.strip
405
326
  matchers.each do | matcher |
@@ -412,15 +333,14 @@ module EDL
412
333
  end
413
334
  end
414
335
  end
415
-
416
- return List.new(stack)
336
+ stack
417
337
  end
418
338
 
419
- def self.timecode_from_line_elements(elements, fps)
420
- args = (0..3).map{|_| elements.shift.to_i} + [fps]
339
+ # Init a Timecode object from the passed elements with the passed framerate
340
+ def self.timecode_from_line_elements(elements, fps) #:nodoc:
341
+ args = (0..3).map{|_| elements.shift.to_i} + [fps.to_f]
421
342
  Timecode.at(*args)
422
343
  end
423
344
  end
424
-
425
-
345
+
426
346
  end
@@ -1,6 +1,6 @@
1
1
  module EDL
2
2
  # Can chop an offline edit into events according to the EDL
3
- class Cutter
3
+ class Cutter #:nodoc:
4
4
  def initialize(source_path)
5
5
  @source_path = source_path
6
6
  end
@@ -18,7 +18,7 @@ module EDL
18
18
  end
19
19
  end
20
20
 
21
- class FFMpegCutter < Cutter
21
+ class FFMpegCutter < Cutter #:nodoc:
22
22
  def cut_segment(evt, start_at, end_at)
23
23
  source_dir, source_file = File.dirname(@source_path), File.basename(@source_path)
24
24
  dest_segment = File.join(source_dir, ('%s_%s' % [evt.num, source_file]))
@@ -0,0 +1,132 @@
1
+ module EDL
2
+ # Represents an edit event
3
+ class Event
4
+ # Event number as in the EDL
5
+ attr_accessor :num
6
+
7
+ # Reel name as in the EDL
8
+ attr_accessor :reel
9
+
10
+ # Event tracks as in the EDL
11
+ attr_accessor :track
12
+
13
+ # Source start timecode of the start frame as in the EDL,
14
+ # no timewarps or dissolves included
15
+ attr_accessor :src_start_tc
16
+
17
+ # Source end timecode of the last frame as in the EDL,
18
+ # no timewarps or dissolves included
19
+ attr_accessor :src_end_tc
20
+
21
+ # Record start timecode of the event in the master as in the EDL
22
+ attr_accessor :rec_start_tc
23
+
24
+ # Record end timecode of the event in the master as in the EDL,
25
+ # outgoing transition is not included
26
+ attr_accessor :rec_end_tc
27
+
28
+ # Array of comment lines verbatim (all comments are included)
29
+ attr_accessor :comments
30
+
31
+ # Clip name contained in FROM CLIP NAME: comment
32
+ attr_accessor :clip_name
33
+
34
+ # Timewarp metadata (an EDL::Timewarp), or nil if no retime is made
35
+ attr_accessor :timewarp
36
+
37
+ # Incoming transition metadata (EDL::Transition), or nil if no transition is used
38
+ attr_accessor :transition
39
+
40
+ # How long is the incoming transition on the next event
41
+ attr_accessor :outgoing_transition_duration
42
+
43
+ def initialize(opts = {})
44
+ opts.each_pair{|k,v| send("#{k}=", v) }
45
+ yield(self) if block_given?
46
+ end
47
+
48
+ # Output a textual description (will not work as an EDL line!)
49
+ def to_s
50
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).map{|a| self.send(a).to_s}.join(" ")
51
+ end
52
+
53
+ def inspect
54
+ to_s
55
+ end
56
+
57
+ def comments #:nodoc:
58
+ @comments ||= []
59
+ @comments
60
+ end
61
+
62
+ def outgoing_transition_duration #:nodoc:
63
+ @outgoing_transition_duration || 0
64
+ end
65
+
66
+ # Is the clip reversed in the edit?
67
+ def reverse?
68
+ (timewarp && timewarp.reverse?)
69
+ end
70
+ alias_method :reversed?, :reverse?
71
+
72
+ def copy_properties_to(evt)
73
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).each do | k |
74
+ evt.send("#{k}=", send(k)) if evt.respond_to?(k)
75
+ end
76
+ evt
77
+ end
78
+
79
+ # Returns true if the clip starts with a transiton (not a jump cut)
80
+ def has_transition?
81
+ !transition.nil?
82
+ end
83
+
84
+ # Returns true if the clip ends with a transition (if the next clip starts with a transition)
85
+ def ends_with_transition?
86
+ outgoing_transition_duration > 0
87
+ end
88
+
89
+ # Returns true if the clip has a timewarp (speed ramp, motion memory, you name it)
90
+ def has_timewarp?
91
+ !timewarp.nil?
92
+ end
93
+
94
+ # Is this a black slug?
95
+ def black?
96
+ reel == 'BL'
97
+ end
98
+ alias_method :slug?, :black?
99
+
100
+ # Get the record length of the event (how long it occupies in the EDL without an eventual outgoing transition)
101
+ def rec_length
102
+ (rec_end_tc.to_i - rec_start_tc.to_i).to_i
103
+ end
104
+
105
+ # Get the record length of the event (how long it occupies in the EDL without an eventual outgoing transition)
106
+ def rec_length_with_transition
107
+ rec_length + outgoing_transition_duration.to_i
108
+ end
109
+
110
+ # How long does the capture need to be to complete this event including timewarps and transitions
111
+ def src_length
112
+ @timewarp ? @timewarp.actual_length_of_source : rec_length_with_transition
113
+ end
114
+
115
+ alias_method :capture_length, :src_length
116
+
117
+ # Capture from (and including!) this timecode to complete this event including timewarps and transitions
118
+ def capture_from_tc
119
+ timewarp ? timewarp.source_used_from : src_start_tc
120
+ end
121
+
122
+ # Capture up to (but not including!) this timecode to complete this event including timewarps and transitions
123
+ def capture_to_tc
124
+ timewarp ? timewarp.source_used_upto : (src_start_tc + rec_length_with_transition)
125
+ end
126
+
127
+ # Returns true if this event is a generator
128
+ def generator?
129
+ black? || (%(AX GEN).include?(reel))
130
+ end
131
+ end
132
+ end