julik-edl 0.0.8

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,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