julik-edl 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,319 @@
1
+ require "rubygems"
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'
8
+ require File.dirname(__FILE__) + '/edl/parser'
9
+
10
+ # A simplistic EDL parser
11
+ module EDL
12
+ VERSION = "0.0.8"
13
+ DEFAULT_FPS = 25.0
14
+
15
+ # Represents an EDL, is returned from the parser. Traditional operation is functional style, i.e.
16
+ # edl.renumbered.without_transitions.without_generators
17
+ class List < Array
18
+
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
22
+ end
23
+
24
+ # Return the same EDL with all dissolves stripped and replaced by the clips underneath
25
+ def without_transitions
26
+ # Find dissolves
27
+ cpy = []
28
+ each_with_index do | e, i |
29
+ # A dissolve always FOLLOWS the incoming clip
30
+ if e.ends_with_transition?
31
+ dissolve = self[i+1]
32
+ len = dissolve.transition.duration.to_i
33
+
34
+ # The dissolve contains the OUTGOING clip, we are the INCOMING. Extend the
35
+ # incoming clip by the length of the dissolve, that's the whole mission actually
36
+ incoming = e.copy_properties_to(e.class.new)
37
+ incoming.src_end_tc += len
38
+ incoming.rec_end_tc += len
39
+
40
+ outgoing = dissolve.copy_properties_to(Event.new)
41
+
42
+ # Add the A suffix to the ex-dissolve
43
+ outgoing.num += 'A'
44
+
45
+ # Take care to join the two if they overlap - TODO
46
+ cpy << incoming
47
+ cpy << outgoing
48
+ elsif e.has_transition?
49
+ # Skip, already handled on the previous clip
50
+ else
51
+ cpy << e.dup
52
+ end
53
+ end
54
+ # Do not renumber events
55
+ # (0...cpy.length).map{|e| cpy[e].num = "%03d" % e }
56
+ self.class.new(cpy)
57
+ end
58
+
59
+ # Return the same EDL, with events renumbered starting from 001
60
+ def renumbered
61
+ renumed = self.dup
62
+ pad = renumed.length.to_s.length
63
+ pad = 3 if pad < 3
64
+
65
+ (0...renumed.length).map{|e| renumed[e].num = "%0#{pad}d" % (e+1) }
66
+ self.class.new(renumed)
67
+ end
68
+
69
+ # Return the same EDL with all timewarps expanded to native length. Clip length
70
+ # changes have rippling effect on footage that comes after the timewarped clip
71
+ # (so this is best used in concert with the original EDL where record TC is pristine)
72
+ def without_timewarps
73
+ self.class.new(
74
+ map do | e |
75
+ if e.has_timewarp?
76
+ repl = e.copy_properties_to(e.class.new)
77
+ from, to = e.timewarp.actual_src_start_tc, e.timewarp.actual_src_end_tc
78
+ repl.src_start_tc, repl.src_end_tc, repl.timewarp = from, to, nil
79
+ repl
80
+ else
81
+ e
82
+ end
83
+ end
84
+ )
85
+ end
86
+
87
+ # Return the same EDL without AX, BL and other GEN events (like slug, text and solids).
88
+ # Usually used in concert with "without_transitions"
89
+ def without_generators
90
+ self.class.new(self.reject{|e| e.generator? })
91
+ end
92
+
93
+ # Return the list of clips used by this EDL at full capture length
94
+ def capture_list
95
+ without_generators.without_timewarps.spliced.from_zero
96
+ end
97
+
98
+ # Return the same EDL with the first event starting at 00:00:00:00 and all subsequent events
99
+ # shifted accordingly
100
+ def from_zero
101
+ shift_by = self[0].rec_start_tc
102
+ self.class.new(
103
+ map do | original |
104
+ e = original.dup
105
+ e.rec_start_tc = (e.rec_start_tc - shift_by)
106
+ e.rec_end_tc = (e.rec_end_tc - shift_by)
107
+ e
108
+ end
109
+ )
110
+ end
111
+
112
+ # Return the same EDL with neighbouring clips joined at cuts where applicable (if a clip
113
+ # is divided in two pieces it will be spliced). Most useful in combination with without_timewarps
114
+ def spliced
115
+ spliced_edl = inject([]) do | spliced, cur |
116
+ latest = spliced[-1]
117
+ # Append to latest if splicable
118
+ if latest && (latest.reel == cur.reel) && (cur.src_start_tc == (latest.src_end_tc + 1))
119
+ latest.src_end_tc = cur.src_end_tc
120
+ latest.rec_end_tc = cur.rec_end_tc
121
+ else
122
+ spliced << cur.dup
123
+ end
124
+ spliced
125
+ end
126
+ self.class.new(spliced_edl)
127
+ end
128
+ end
129
+
130
+ #:stopdoc:
131
+
132
+ # A generic matcher
133
+ class Matcher
134
+ class ApplyError < RuntimeError
135
+ def initialize(msg, line)
136
+ super("%s - offending line was '%s'" % [msg, line])
137
+ end
138
+ end
139
+
140
+ def initialize(with_regexp)
141
+ @regexp = with_regexp
142
+ end
143
+
144
+ def matches?(line)
145
+ !!(line =~ @regexp)
146
+ end
147
+
148
+ def apply(stack, line)
149
+ STDERR.puts "Skipping #{line}"
150
+ end
151
+ end
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.push("* %s" % line.scan(@regexp).flatten.pop.strip)
161
+ end
162
+ end
163
+
164
+ # Fallback matcher for things like FINAL CUT PRO REEL
165
+ class FallbackMatcher < Matcher
166
+ def initialize
167
+ super(/^(\w)(.+)/)
168
+ end
169
+
170
+ def apply(stack, line)
171
+ begin
172
+ stack[-1].comments << line.scan(@regexp).flatten.join.strip
173
+ rescue NoMethodError
174
+ raise ApplyError.new("Line can only be a comment but no event was on the stack", line)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Clip name matcher
180
+ class NameMatcher < Matcher
181
+ def initialize
182
+ super(/\* FROM CLIP NAME:(\s+)(.+)/)
183
+ end
184
+
185
+ def apply(stack, line)
186
+ stack[-1].clip_name = line.scan(@regexp).flatten.pop.strip
187
+ CommentMatcher.new.apply(stack, line)
188
+ end
189
+ end
190
+
191
+ class EffectMatcher < Matcher
192
+ def initialize
193
+ super(/\* EFFECT NAME:(\s+)(.+)/)
194
+ end
195
+
196
+ def apply(stack, line)
197
+ stack[-1].transition.effect = line.scan(@regexp).flatten.pop.strip
198
+ CommentMatcher.new.apply(stack, line)
199
+ end
200
+ end
201
+
202
+ class TimewarpMatcher < Matcher
203
+
204
+ attr_reader :fps
205
+
206
+ def initialize(fps)
207
+ @fps = fps
208
+ @regexp = /M2(\s+)(\w+)(\s+)(\-?\d+\.\d+)(\s+)(\d{1,2}):(\d{1,2}):(\d{1,2}):(\d{1,2})/
209
+ end
210
+
211
+ def apply(stack, line)
212
+ matches = line.scan(@regexp).flatten.map{|e| e.strip}.reject{|e| e.nil? || e.empty?}
213
+
214
+ from_reel = matches.shift
215
+ fps = matches.shift
216
+
217
+ begin
218
+ # FIXME
219
+ tw_start_source_tc = Parser.timecode_from_line_elements(matches, @fps)
220
+ rescue Timecode::Error => e
221
+ raise ApplyError, "Invalid TC in timewarp (#{e})", line
222
+ end
223
+
224
+ evt_with_tw = stack.reverse.find{|e| e.src_start_tc == tw_start_source_tc && e.reel == from_reel }
225
+
226
+ unless evt_with_tw
227
+ raise ApplyError, "Cannot find event marked by timewarp", line
228
+ else
229
+ tw = Timewarp.new
230
+ tw.actual_framerate, tw.clip = fps.to_f, evt_with_tw
231
+ evt_with_tw.timewarp = tw
232
+ end
233
+ end
234
+ end
235
+
236
+ # Drop frame goodbye
237
+ TC = /(\d{1,2}):(\d{1,2}):(\d{1,2}):(\d{1,2})/
238
+
239
+ class EventMatcher < Matcher
240
+
241
+ # 021 009 V C 00:39:04:21 00:39:05:09 01:00:26:17 01:00:27:05
242
+ EVENT_PAT = /(\d+)(\s+)(\w+)(\s+)(\w+)(\s+)(\w+)(\s+)((\w+\s+)?)#{TC} #{TC} #{TC} #{TC}/
243
+
244
+ attr_reader :fps
245
+
246
+ def initialize(some_fps)
247
+ super(EVENT_PAT)
248
+ @fps = some_fps
249
+ end
250
+
251
+ def apply(stack, line)
252
+
253
+ matches = line.scan(@regexp).shift
254
+ props = {}
255
+
256
+ # FIrst one is the event number
257
+ props[:num] = matches.shift
258
+ matches.shift
259
+
260
+ # Then the reel
261
+ props[:reel] = matches.shift
262
+ matches.shift
263
+
264
+ # Then the track
265
+ props[:track] = matches.shift
266
+ matches.shift
267
+
268
+ # Then the type
269
+ props[:transition] = matches.shift
270
+ matches.shift
271
+
272
+ # Then the optional generator group - skip for now
273
+ if props[:transition] != 'C'
274
+ props[:duration] = matches.shift.strip
275
+ else
276
+ matches.shift
277
+ end
278
+ matches.shift
279
+
280
+ # Then the timecodes
281
+ [:src_start_tc, :src_end_tc, :rec_start_tc, :rec_end_tc].each do | k |
282
+ begin
283
+ props[k] = EDL::Parser.timecode_from_line_elements(matches, @fps)
284
+ rescue Timecode::Error => e
285
+ raise ApplyError, "Cannot parse timecode - #{e}", line
286
+ end
287
+ end
288
+
289
+ evt = Event.new
290
+ transition_idx = props.delete(:transition)
291
+ evt.transition = case transition_idx
292
+ when 'D'
293
+ d = Dissolve.new
294
+ d.duration = props.delete(:duration).to_i
295
+ d
296
+ when /W(\d+)/
297
+ w = Wipe.new
298
+ w.duration = props.delete(:duration).to_i
299
+ w.smpte_wipe_index = transition_idx.gsub(/W/, '')
300
+ w
301
+ else
302
+ nil
303
+ end
304
+
305
+ # Give a hint on the incoming clip as well
306
+ if evt.transition && stack[-1]
307
+ stack[-1].outgoing_transition_duration = evt.transition.duration
308
+ end
309
+
310
+ props.each_pair { | k, v | evt.send("#{k}=", v) }
311
+
312
+ stack << evt
313
+ evt # FIXME - we dont need to return this is only used by tests
314
+ end
315
+ end
316
+
317
+ #:startdoc:
318
+
319
+ end
@@ -0,0 +1,34 @@
1
+ module EDL
2
+ # Can chop an offline edit into events according to the EDL
3
+ class Cutter #:nodoc:
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 #:nodoc:
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,146 @@
1
+ module EDL
2
+ # Represents an edit event (or, more specifically, an EDL line denoting a clip being part of an EDL 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
+ # Where is this event located in the original file
44
+ attr_accessor :line_number
45
+
46
+ def initialize(opts = {})
47
+ opts.each_pair{|k,v| send("#{k}=", v) }
48
+ yield(self) if block_given?
49
+ end
50
+
51
+ # Output a textual description (will not work as an EDL line!)
52
+ def to_s
53
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).map{|a| self.send(a).to_s}.join(" ")
54
+ end
55
+
56
+ def inspect
57
+ to_s
58
+ end
59
+
60
+ def comments #:nodoc:
61
+ @comments ||= []
62
+ @comments
63
+ end
64
+
65
+ def outgoing_transition_duration #:nodoc:
66
+ @outgoing_transition_duration ||= 0
67
+ end
68
+
69
+ # Is the clip reversed in the edit?
70
+ def reverse?
71
+ (timewarp && timewarp.reverse?)
72
+ end
73
+ alias_method :reversed?, :reverse?
74
+
75
+ def copy_properties_to(evt)
76
+ %w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc).each do | k |
77
+ evt.send("#{k}=", send(k)) if evt.respond_to?(k)
78
+ end
79
+ evt
80
+ end
81
+
82
+ # Returns true if the clip starts with a transiton (not a jump cut)
83
+ def has_transition?
84
+ !!@transition
85
+ end
86
+ alias_method :starts_with_transition?, :has_transition?
87
+
88
+ # The duration of the incoming transition, or 0 if no transition is used
89
+ def incoming_transition_duration
90
+ @transition ? @transition.duration : 0
91
+ end
92
+
93
+ # Returns true if the clip ends with a transition (if the next clip starts with a transition)
94
+ def ends_with_transition?
95
+ outgoing_transition_duration > 0
96
+ end
97
+
98
+ # Returns true if the clip has a timewarp (speed ramp, motion memory, you name it)
99
+ def has_timewarp?
100
+ !timewarp.nil?
101
+ end
102
+
103
+ # Is this a black slug?
104
+ def black?
105
+ reel == 'BL'
106
+ end
107
+ alias_method :slug?, :black?
108
+
109
+ # Get the record length of the event (how long it occupies in the EDL without an eventual outgoing transition)
110
+ def rec_length
111
+ (rec_end_tc.to_i - rec_start_tc.to_i).to_i
112
+ end
113
+
114
+ # Get the record length of the event (how long it occupies in the EDL with an eventual outgoing transition)
115
+ def rec_length_with_transition
116
+ rec_length + outgoing_transition_duration.to_i
117
+ end
118
+
119
+ # How long does the capture need to be to complete this event including timewarps and transitions
120
+ def src_length
121
+ @timewarp ? @timewarp.actual_length_of_source : rec_length_with_transition
122
+ end
123
+
124
+ alias_method :capture_length, :src_length
125
+
126
+ # Capture from (and including!) this timecode to complete this event including timewarps and transitions
127
+ def capture_from_tc
128
+ @timewarp ? @timewarp.source_used_from : src_start_tc
129
+ end
130
+
131
+ # Capture up to (but not including!) this timecode to complete this event including timewarps and transitions
132
+ def capture_to_tc
133
+ @timewarp ? @timewarp.source_used_upto : (src_end_tc + outgoing_transition_duration)
134
+ end
135
+
136
+ # Speed of this clip in percent relative to the source speed. 100 for non-timewarped events
137
+ def speed
138
+ @timewarp ? @timewarp.speed : 100.0
139
+ end
140
+
141
+ # Returns true if this event is a generator
142
+ def generator?
143
+ black? || (%(AX GEN).include?(reel))
144
+ end
145
+ end
146
+ end