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.
- data/History.txt +39 -0
- data/Manifest.txt +23 -0
- data/README.txt +57 -0
- data/Rakefile +18 -0
- data/SPECS.txt +96 -0
- data/illustr/edl-explain.ai +1182 -2
- data/lib/edl.rb +319 -0
- data/lib/edl/cutter.rb +34 -0
- data/lib/edl/event.rb +146 -0
- data/lib/edl/grabber.rb +29 -0
- data/lib/edl/timewarp.rb +45 -0
- data/lib/edl/transition.rb +23 -0
- data/test/samples/45S_SAMPLE.EDL +48 -0
- data/test/samples/FCP_REVERSE.EDL +9 -0
- data/test/samples/REVERSE.EDL +3 -0
- data/test/samples/SIMPLE_DISSOLVE.EDL +9 -0
- data/test/samples/SPEEDUP_AND_FADEOUT.EDL +11 -0
- data/test/samples/SPEEDUP_REVERSE_AND_FADEOUT.EDL +11 -0
- data/test/samples/SPLICEME.EDL +5 -0
- data/test/samples/TIMEWARP.EDL +5 -0
- data/test/samples/TIMEWARP_HALF.EDL +5 -0
- data/test/samples/TRAILER_EDL.edl +0 -0
- data/test/test_edl.rb +664 -0
- metadata +115 -0
data/lib/edl/grabber.rb
ADDED
@@ -0,0 +1,29 @@
|
|
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 #:nodoc:
|
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.with_frames_as_fraction} -vframes 1 -y #{to_file}%d.jpg"
|
26
|
+
`#{cmd}`
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/edl/timewarp.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module EDL
|
2
|
+
# Represents a timewarp. Will be placed in EDL::Event#timewarp
|
3
|
+
# For a reversed clip, the source start we get from the EDL in the src start
|
4
|
+
# is the LAST used frame. For a pos rate clip, the source start is the bona fide source start. Nice eh?
|
5
|
+
class Timewarp
|
6
|
+
|
7
|
+
# What is the actual framerate of the clip (float)
|
8
|
+
attr_accessor :actual_framerate
|
9
|
+
attr_accessor :clip #:nodoc:
|
10
|
+
|
11
|
+
# Does this timewarp reverse the clip?
|
12
|
+
def reverse?
|
13
|
+
@actual_framerate < 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the speed in percent
|
17
|
+
def speed_in_percent
|
18
|
+
(@actual_framerate / @clip.rec_start_tc.fps) * 100
|
19
|
+
end
|
20
|
+
alias_method :speed, :speed_in_percent
|
21
|
+
|
22
|
+
# Compute the length of the clip we need to capture. The length is computed in frames and
|
23
|
+
# is always rounded up (better one frame more than one frame less!)
|
24
|
+
def actual_length_of_source
|
25
|
+
# First, get the length of the clip including a transition. This is what we are scaled to.
|
26
|
+
target_len = @clip.rec_length_with_transition.to_f
|
27
|
+
# Determine the framerate scaling factor, this is the speed
|
28
|
+
factor = @actual_framerate / @clip.rec_start_tc.fps
|
29
|
+
(target_len * factor).ceil.abs
|
30
|
+
end
|
31
|
+
|
32
|
+
# What is the starting frame for the captured clip? If we are a reverse, then the src start of the
|
33
|
+
# clip is our LAST frame, otherwise it's the first
|
34
|
+
def source_used_from
|
35
|
+
# TODO: account for the 2 frame deficiency which is suspicious
|
36
|
+
compensation = 2
|
37
|
+
reverse? ? (@clip.src_start_tc - actual_length_of_source + compensation) : @clip.src_start_tc
|
38
|
+
end
|
39
|
+
|
40
|
+
# Where to end the capture? This is also dependent on whether we are a reverse or not
|
41
|
+
def source_used_upto
|
42
|
+
source_used_from + actual_length_of_source
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EDL
|
2
|
+
# Represents a transition. We currently only support dissolves and SMPTE wipes
|
3
|
+
# Will be avilable as EDL::Clip#transition
|
4
|
+
class Transition
|
5
|
+
|
6
|
+
# Length of the transition in frames
|
7
|
+
attr_accessor :duration
|
8
|
+
|
9
|
+
# Which effect is used (like CROSS DISSOLVE)
|
10
|
+
attr_accessor :effect
|
11
|
+
end
|
12
|
+
|
13
|
+
# Represents a dissolve
|
14
|
+
class Dissolve < Transition
|
15
|
+
end
|
16
|
+
|
17
|
+
# Represents an SMPTE wipe
|
18
|
+
class Wipe < Transition
|
19
|
+
|
20
|
+
# Which SMPTE wipe is needed
|
21
|
+
attr_accessor :smpte_wipe_index
|
22
|
+
end
|
23
|
+
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,11 @@
|
|
1
|
+
TITLE: SPEEDUP_AND_FADEOUT
|
2
|
+
FCM: NON-DROP FRAME
|
3
|
+
|
4
|
+
001 FOO V C 01:00:00:00 01:00:27:14 01:00:00:00 01:00:27:14
|
5
|
+
001 BL V D 025 00:00:00:00 00:00:01:00 01:00:27:14 01:00:28:14
|
6
|
+
* EFFECT NAME: CROSS DISSOLVE
|
7
|
+
* FROM CLIP NAME: BARS_40S.MOV
|
8
|
+
* COMMENT:
|
9
|
+
M2 FOO 035.0 01:00:00:00
|
10
|
+
|
11
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
TITLE: TESTREV
|
2
|
+
FCM: NON-DROP FRAME
|
3
|
+
|
4
|
+
001 FOO V C 01:00:39:23 01:01:07:12 01:00:00:00 01:00:27:14
|
5
|
+
001 BL V D 025 00:00:00:00 00:00:01:00 01:00:27:14 01:00:28:14
|
6
|
+
* EFFECT NAME: CROSS DISSOLVE
|
7
|
+
* FROM CLIP NAME: BARS_40S.MOV
|
8
|
+
* COMMENT:
|
9
|
+
M2 FOO -035.0 01:00:39:23
|
10
|
+
|
11
|
+
|
Binary file
|
data/test/test_edl.rb
ADDED
@@ -0,0 +1,664 @@
|
|
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 'test/spec'
|
8
|
+
require 'flexmock'
|
9
|
+
require 'flexmock/test_unit'
|
10
|
+
|
11
|
+
TRAILER_EDL = File.dirname(__FILE__) + '/samples/TRAILER_EDL.edl'
|
12
|
+
SIMPLE_DISSOLVE = File.dirname(__FILE__) + '/samples/SIMPLE_DISSOLVE.EDL'
|
13
|
+
SPLICEME = File.dirname(__FILE__) + '/samples/SPLICEME.EDL'
|
14
|
+
SIMPLE_TIMEWARP = File.dirname(__FILE__) + '/samples/TIMEWARP.EDL'
|
15
|
+
SLOMO_TIMEWARP = File.dirname(__FILE__) + '/samples/TIMEWARP_HALF.EDL'
|
16
|
+
FORTY_FIVER = File.dirname(__FILE__) + '/samples/45S_SAMPLE.EDL'
|
17
|
+
AVID_REVERSE = File.dirname(__FILE__) + '/samples/REVERSE.EDL'
|
18
|
+
SPEEDUP_AND_FADEOUT = File.dirname(__FILE__) + '/samples/SPEEDUP_AND_FADEOUT.EDL'
|
19
|
+
SPEEDUP_REVERSE_AND_FADEOUT = File.dirname(__FILE__) + '/samples/SPEEDUP_REVERSE_AND_FADEOUT.EDL'
|
20
|
+
FCP_REVERSE = File.dirname(__FILE__) + '/samples/FCP_REVERSE.EDL'
|
21
|
+
|
22
|
+
class String
|
23
|
+
def tc(fps = Timecode::DEFAULT_FPS)
|
24
|
+
Timecode.parse(self, fps)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "An Event should" do
|
29
|
+
specify "define the needed attributes" do
|
30
|
+
evt = EDL::Event.new
|
31
|
+
%w( num reel track src_start_tc src_end_tc rec_start_tc rec_end_tc ).each do | em |
|
32
|
+
evt.should.respond_to em
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
specify "support hash initialization" do
|
37
|
+
evt = EDL::Event.new(:src_start_tc => "01:00:00:00".tc)
|
38
|
+
evt.src_start_tc.should.equal "01:00:00:00".tc
|
39
|
+
end
|
40
|
+
|
41
|
+
specify "support block initialization" do
|
42
|
+
evt = EDL::Event.new do | e |
|
43
|
+
e.src_start_tc = "01:00:00:04".tc
|
44
|
+
end
|
45
|
+
evt.src_start_tc.should.equal "01:00:00:04".tc
|
46
|
+
end
|
47
|
+
|
48
|
+
specify "respond to ends_with_transition? with false if outgoing_transition_duration is zero" do
|
49
|
+
evt = EDL::Event.new
|
50
|
+
evt.outgoing_transition_duration = 0
|
51
|
+
evt.ends_with_transition?.should.equal false
|
52
|
+
end
|
53
|
+
|
54
|
+
specify "respond to ends_with_transition? with true if outgoing_transition_duration set above zero" do
|
55
|
+
evt = EDL::Event.new
|
56
|
+
evt.outgoing_transition_duration = 24
|
57
|
+
evt.ends_with_transition?.should.equal true
|
58
|
+
end
|
59
|
+
|
60
|
+
specify "respond to has_timewarp? with false if no timewarp assigned" do
|
61
|
+
evt = EDL::Event.new(:timewarp => nil)
|
62
|
+
evt.has_timewarp?.should.equal false
|
63
|
+
end
|
64
|
+
|
65
|
+
specify "respond to has_timewarp? with true if a timewarp is assigned" do
|
66
|
+
evt = EDL::Event.new(:timewarp => true)
|
67
|
+
evt.has_timewarp?.should.equal true
|
68
|
+
end
|
69
|
+
|
70
|
+
specify "report rec_length as a difference of record timecodes" do
|
71
|
+
evt = EDL::Event.new(:rec_start_tc => "1h".tc, :rec_end_tc => "1h 10s 2f".tc )
|
72
|
+
evt.rec_length.should.equal "10s 2f".tc.to_i
|
73
|
+
end
|
74
|
+
|
75
|
+
specify "report rec_length_with_transition as a difference of record timecodes if no transition set" do
|
76
|
+
evt = EDL::Event.new(:rec_start_tc => "1h".tc, :rec_end_tc => "1h 10s 2f".tc, :outgoing_transition_duration => 0)
|
77
|
+
evt.rec_length_with_transition.should.equal "10s 2f".tc.to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
specify "add transition length to rec_length_with_transition if a transition is set" do
|
81
|
+
evt = EDL::Event.new(:rec_start_tc => "1h".tc, :rec_end_tc => "1h 10s 2f".tc, :outgoing_transition_duration => 10)
|
82
|
+
evt.rec_length_with_transition.should.equal("10s 2f".tc.to_i + 10)
|
83
|
+
end
|
84
|
+
|
85
|
+
specify "return a default array for comments" do
|
86
|
+
EDL::Event.new.comments.should.be.kind_of Enumerable
|
87
|
+
end
|
88
|
+
|
89
|
+
specify "respond false to has_transition? if incoming transition is set" do
|
90
|
+
EDL::Event.new(:transition => nil).has_transition?.should.equal false
|
91
|
+
end
|
92
|
+
|
93
|
+
specify "respond true to has_transition? if incoming transition is set" do
|
94
|
+
EDL::Event.new(:transition => true).has_transition?.should.equal true
|
95
|
+
end
|
96
|
+
|
97
|
+
specify "respond true to black? if reel is BL" do
|
98
|
+
EDL::Event.new(:reel => "BL").should.black
|
99
|
+
EDL::Event.new(:reel => "001").should.not.black
|
100
|
+
end
|
101
|
+
|
102
|
+
specify "respond true to generator? if reel is BL or AX" do
|
103
|
+
EDL::Event.new(:reel => "BL").should.generator
|
104
|
+
EDL::Event.new(:reel => "AX").should.generator
|
105
|
+
EDL::Event.new(:reel => "001").should.not.generator
|
106
|
+
end
|
107
|
+
|
108
|
+
specify "report src_length as rec_length_with_transition" do
|
109
|
+
e = EDL::Event.new(:rec_start_tc => "2h".tc, :rec_end_tc => "2h 2s".tc)
|
110
|
+
e.src_length.should.equal "2s".tc.to_i
|
111
|
+
end
|
112
|
+
|
113
|
+
specify "support line_number" do
|
114
|
+
EDL::Event.new.line_number.should.be.nil
|
115
|
+
EDL::Event.new(:line_number => 3).line_number.should.equal 3
|
116
|
+
end
|
117
|
+
|
118
|
+
specify "support capture_length as an alias to src_length" do
|
119
|
+
tw = flexmock
|
120
|
+
tw.should_receive(:actual_length_of_source).and_return(:something)
|
121
|
+
e = EDL::Event.new(:timewarp => tw)
|
122
|
+
e.src_length.should.equal e.capture_length
|
123
|
+
end
|
124
|
+
|
125
|
+
specify "delegate src_length to the timewarp if it is there" do
|
126
|
+
tw = flexmock
|
127
|
+
tw.should_receive(:actual_length_of_source).and_return(:something).once
|
128
|
+
e = EDL::Event.new(:timewarp => tw)
|
129
|
+
e.src_length.should.equal :something
|
130
|
+
end
|
131
|
+
|
132
|
+
specify "report reverse? and reversed? based on the timewarp" do
|
133
|
+
e = EDL::Event.new(:timewarp => nil)
|
134
|
+
e.should.not.be.reverse
|
135
|
+
e.should.not.be.reversed
|
136
|
+
|
137
|
+
tw = flexmock
|
138
|
+
tw.should_receive(:reverse?).and_return(true)
|
139
|
+
|
140
|
+
e = EDL::Event.new(:timewarp => tw)
|
141
|
+
e.should.be.reverse
|
142
|
+
e.should.be.reversed
|
143
|
+
end
|
144
|
+
|
145
|
+
specify "report speed as 100 percent without a timewarp" do
|
146
|
+
e = EDL::Event.new
|
147
|
+
e.speed.should.be.kind_of Float
|
148
|
+
e.speed.should.equal 100.0
|
149
|
+
end
|
150
|
+
|
151
|
+
specify "consult the timewarp for speed" do
|
152
|
+
tw = flexmock
|
153
|
+
tw.should_receive(:speed).and_return(:something)
|
154
|
+
|
155
|
+
e = EDL::Event.new(:timewarp => tw)
|
156
|
+
e.speed.should.equal :something
|
157
|
+
end
|
158
|
+
|
159
|
+
specify "report false for starts_with_transition? if transision is nil" do
|
160
|
+
e = EDL::Event.new
|
161
|
+
e.should.respond_to :starts_with_transition?
|
162
|
+
e.should.not.be.starts_with_transition
|
163
|
+
end
|
164
|
+
|
165
|
+
specify "report zero for incoming_transition_duration if transision is nil" do
|
166
|
+
e = EDL::Event.new
|
167
|
+
e.should.respond_to :incoming_transition_duration
|
168
|
+
e.incoming_transition_duration.should.zero
|
169
|
+
end
|
170
|
+
|
171
|
+
specify "report true for starts_with_transition? if transision is not nil" do
|
172
|
+
e = EDL::Event.new :transition => true
|
173
|
+
e.should.respond_to :starts_with_transition?
|
174
|
+
e.should.starts_with_transition
|
175
|
+
end
|
176
|
+
|
177
|
+
specify "consult the transition for incoming_transition_duration if it's present" do
|
178
|
+
tr = flexmock
|
179
|
+
tr.should_receive(:duration).and_return(:something)
|
180
|
+
|
181
|
+
e = EDL::Event.new(:transition => tr)
|
182
|
+
e.should.respond_to :incoming_transition_duration
|
183
|
+
e.incoming_transition_duration.should.equal :something
|
184
|
+
end
|
185
|
+
|
186
|
+
specify "report capture_from_tc as the source start without a timewarp" do
|
187
|
+
e = EDL::Event.new(:src_start_tc => "1h".tc)
|
188
|
+
e.capture_from_tc.should.equal "1h".tc
|
189
|
+
end
|
190
|
+
|
191
|
+
specify "consult the timewarp for capture_from_tc if a timewarp is there" do
|
192
|
+
tw = flexmock
|
193
|
+
tw.should_receive(:source_used_from).and_return(:something)
|
194
|
+
|
195
|
+
e = EDL::Event.new(:timewarp => tw)
|
196
|
+
e.capture_from_tc.should.equal :something
|
197
|
+
end
|
198
|
+
|
199
|
+
specify "report capture_to_tc as record length plus transition when no timewarp present" do
|
200
|
+
e = EDL::Event.new(:src_end_tc => "1h 10s".tc, :outgoing_transition_duration => 2 )
|
201
|
+
e.capture_to_tc.should.equal "1h 10s 2f".tc
|
202
|
+
end
|
203
|
+
|
204
|
+
specify "consult the timewarp for capture_to_tc if timewarp is present" do
|
205
|
+
tw = flexmock
|
206
|
+
tw.should_receive(:source_used_upto).and_return(:something)
|
207
|
+
|
208
|
+
e = EDL::Event.new(:timewarp => tw)
|
209
|
+
e.capture_to_tc.should.equal :something
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
context "A Parser should" do
|
216
|
+
|
217
|
+
specify "store the passed framerate" do
|
218
|
+
p = EDL::Parser.new(45)
|
219
|
+
p.should.respond_to :fps
|
220
|
+
p.fps.should.equal 45
|
221
|
+
end
|
222
|
+
|
223
|
+
specify "return matchers tuned with the passed framerate" do
|
224
|
+
p = EDL::Parser.new(30)
|
225
|
+
matchers = p.get_matchers
|
226
|
+
event_matcher = matchers.find{|e| e.is_a?(EDL::EventMatcher) }
|
227
|
+
event_matcher.fps.should.equal 30
|
228
|
+
end
|
229
|
+
|
230
|
+
specify "create a Timecode from stringified elements" do
|
231
|
+
elems = ["08", "04", "24", "24"]
|
232
|
+
lambda{ @tc = EDL::Parser.timecode_from_line_elements(elems, 30) }.should.not.raise
|
233
|
+
|
234
|
+
@tc.should.be.kind_of Timecode
|
235
|
+
@tc.should.equal "08:04:24:24".tc(30)
|
236
|
+
|
237
|
+
elems.length.should.equal 0
|
238
|
+
end
|
239
|
+
|
240
|
+
specify "parse from a String" do
|
241
|
+
p = EDL::Parser.new
|
242
|
+
lambda{ @edl = p.parse File.read(SIMPLE_DISSOLVE) }.should.not.raise
|
243
|
+
|
244
|
+
@edl.should.be.kind_of EDL::List
|
245
|
+
@edl.length.should.equal 2
|
246
|
+
end
|
247
|
+
|
248
|
+
specify "parse from a File/IOish" do
|
249
|
+
p = EDL::Parser.new
|
250
|
+
lambda{ @edl = p.parse File.open(SIMPLE_DISSOLVE) }.should.not.raise
|
251
|
+
|
252
|
+
@edl.should.be.kind_of EDL::List
|
253
|
+
@edl.length.should.equal 2
|
254
|
+
end
|
255
|
+
|
256
|
+
specify "properly parse a dissolve" do
|
257
|
+
# TODO: reformulate
|
258
|
+
p = EDL::Parser.new
|
259
|
+
lambda{ @edl = p.parse File.open(SIMPLE_DISSOLVE) }.should.not.raise
|
260
|
+
|
261
|
+
@edl.should.be.kind_of EDL::List
|
262
|
+
@edl.length.should.equal 2
|
263
|
+
|
264
|
+
first, second = @edl
|
265
|
+
first.should.be.kind_of EDL::Event
|
266
|
+
second.should.be.kind_of EDL::Event
|
267
|
+
|
268
|
+
second.has_transition?.should.equal true
|
269
|
+
first.ends_with_transition?.should.equal true
|
270
|
+
second.ends_with_transition?.should.equal false
|
271
|
+
|
272
|
+
no_trans = @edl.without_transitions
|
273
|
+
|
274
|
+
assert_equal 2, no_trans.length
|
275
|
+
target_tc = (Timecode.parse('01:00:00:00') + 43)
|
276
|
+
assert_equal target_tc, no_trans[0].rec_end_tc,
|
277
|
+
"The incoming clip should have been extended by the length of the dissolve"
|
278
|
+
|
279
|
+
target_tc = Timecode.parse('01:00:00:00')
|
280
|
+
assert_equal target_tc, no_trans[1].rec_start_tc
|
281
|
+
"The outgoing clip should have been left in place"
|
282
|
+
end
|
283
|
+
|
284
|
+
specify "return a spliced EDL if the sources allow" do
|
285
|
+
lambda{ @spliced = EDL::Parser.new.parse(File.open(SPLICEME)).spliced }.should.not.raise
|
286
|
+
|
287
|
+
@spliced.length.should.equal 1
|
288
|
+
@spliced[0].src_start_tc.should.equal '06:42:50:18'.tc
|
289
|
+
@spliced[0].src_end_tc.should.equal '06:42:52:16'.tc
|
290
|
+
end
|
291
|
+
|
292
|
+
specify "not apply any Matchers if a match is found" do
|
293
|
+
p = EDL::Parser.new
|
294
|
+
m1 = flexmock
|
295
|
+
m1.should_receive(:matches?).with("plop").once.and_return(true)
|
296
|
+
m1.should_receive(:apply).once
|
297
|
+
|
298
|
+
flexmock(p).should_receive(:get_matchers).once.and_return([m1, m1])
|
299
|
+
result = p.parse("plop")
|
300
|
+
result.should.be.empty
|
301
|
+
end
|
302
|
+
|
303
|
+
specify "register line numbers of the detected events" do
|
304
|
+
p = EDL::Parser.new
|
305
|
+
events = p.parse(File.open(SPLICEME))
|
306
|
+
|
307
|
+
events[0].line_number.should.not.be.nil
|
308
|
+
events[0].line_number.should.equal 4
|
309
|
+
|
310
|
+
events[1].line_number.should.not.be.nil
|
311
|
+
events[1].line_number.should.equal 5
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context "A TimewarpMatcher should" do
|
316
|
+
|
317
|
+
specify "not create any extra events when used within a Parser" do
|
318
|
+
@edl = EDL::Parser.new.parse(File.open(SIMPLE_TIMEWARP))
|
319
|
+
@edl.length.should.equal 1
|
320
|
+
end
|
321
|
+
|
322
|
+
specify "properly describe a speedup" do
|
323
|
+
clip = EDL::Parser.new.parse(File.open(SIMPLE_TIMEWARP)).pop
|
324
|
+
|
325
|
+
tw = clip.timewarp
|
326
|
+
|
327
|
+
tw.should.be.kind_of EDL::Timewarp
|
328
|
+
tw.source_used_upto.should.be > clip.src_end_tc
|
329
|
+
|
330
|
+
tw.source_used_from.should.equal clip.src_start_tc
|
331
|
+
clip.timewarp.actual_length_of_source.should.equal 124
|
332
|
+
|
333
|
+
tw.reverse?.should.be false
|
334
|
+
end
|
335
|
+
|
336
|
+
specify "properly describe a slomo" do
|
337
|
+
clip = EDL::Parser.new.parse(File.open(SLOMO_TIMEWARP)).pop
|
338
|
+
|
339
|
+
clip.rec_length.should.equal 10
|
340
|
+
clip.src_length.should.equal 5
|
341
|
+
|
342
|
+
tw = clip.timewarp
|
343
|
+
tw.should.be.kind_of EDL::Timewarp
|
344
|
+
|
345
|
+
tw.source_used_upto.should.be < clip.src_end_tc
|
346
|
+
tw.source_used_upto.should.equal "03:03:19:24".tc
|
347
|
+
tw.speed_in_percent.to_i.should.equal 50
|
348
|
+
tw.actual_length_of_source.should.equal 5
|
349
|
+
|
350
|
+
tw.should.not.be.reverse
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
context "A reverse timewarp EDL coming from Avid should" do
|
356
|
+
|
357
|
+
specify "be parsed properly" do
|
358
|
+
|
359
|
+
clip = EDL::Parser.new.parse(File.open(AVID_REVERSE)).pop
|
360
|
+
|
361
|
+
clip.rec_length.should.equal 52
|
362
|
+
|
363
|
+
tw = clip.timewarp
|
364
|
+
tw.actual_framerate.to_i.should.equal -25
|
365
|
+
|
366
|
+
tw.should.be.reverse
|
367
|
+
|
368
|
+
tw.actual_length_of_source.should.equal 52
|
369
|
+
|
370
|
+
assert_equal 52, clip.src_length, "The src length should be computed the same as its just a reverse"
|
371
|
+
assert_equal -100.0, clip.timewarp.speed
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
context "A Final Cut Pro originating reverse should" do
|
376
|
+
|
377
|
+
specify "be interpreted properly" do
|
378
|
+
e = EDL::Parser.new.parse(File.open(FCP_REVERSE)).pop
|
379
|
+
|
380
|
+
e.rec_length.should.equal 1000
|
381
|
+
e.src_length.should.equal 1000
|
382
|
+
|
383
|
+
e.rec_start_tc.should.equal "1h".tc
|
384
|
+
e.rec_end_tc.should.equal "1h 40s".tc
|
385
|
+
|
386
|
+
e.should.be.reverse
|
387
|
+
e.timewarp.should.not.be nil
|
388
|
+
|
389
|
+
tw = e.timewarp
|
390
|
+
|
391
|
+
tw.speed.should.equal -100.0
|
392
|
+
e.speed.should.equal -100.0
|
393
|
+
|
394
|
+
tw.source_used_from.should.equal "1h".tc
|
395
|
+
tw.source_used_upto.should.equal "1h 40s".tc
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
context "EventMatcher should" do
|
400
|
+
|
401
|
+
EVT_PATTERNS = [
|
402
|
+
'020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17',
|
403
|
+
'021 009 V C 00:39:04:21 00:39:05:09 01:00:26:17 01:00:27:05',
|
404
|
+
'022 008C V C 08:08:01:23 08:08:02:18 01:00:27:05 01:00:28:00',
|
405
|
+
'023 008C V C 08:07:30:02 08:07:30:21 01:00:28:00 01:00:28:19',
|
406
|
+
'024 AX V C 00:00:00:00 00:00:01:00 01:00:28:19 01:00:29:19',
|
407
|
+
'025 BL V C 00:00:00:00 00:00:00:00 01:00:29:19 01:00:29:19',
|
408
|
+
'025 GEN V D 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20',
|
409
|
+
]
|
410
|
+
|
411
|
+
specify "produce an Event" do
|
412
|
+
m = EDL::EventMatcher.new(25)
|
413
|
+
|
414
|
+
clip = m.apply([],
|
415
|
+
'020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17'
|
416
|
+
)
|
417
|
+
|
418
|
+
clip.should.be.kind_of EDL::Event
|
419
|
+
|
420
|
+
clip.num.should.equal "020"
|
421
|
+
clip.reel.should.equal "008C"
|
422
|
+
clip.track.should.equal "V"
|
423
|
+
|
424
|
+
clip.src_start_tc.should.equal '08:04:24:24'.tc
|
425
|
+
|
426
|
+
clip.src_end_tc.should.equal '08:04:25:19'.tc
|
427
|
+
clip.rec_start_tc.should.equal '01:00:25:22'.tc
|
428
|
+
clip.rec_end_tc.should.equal '01:00:26:17'.tc
|
429
|
+
|
430
|
+
clip.transition.should.be nil
|
431
|
+
clip.timewarp.should.be nil
|
432
|
+
clip.outgoing_transition_duration.should.be.zero
|
433
|
+
|
434
|
+
end
|
435
|
+
|
436
|
+
specify "produce an Event with dissolve" do
|
437
|
+
m = EDL::EventMatcher.new(25)
|
438
|
+
|
439
|
+
dissolve = m.apply([],
|
440
|
+
'025 GEN V D 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20'
|
441
|
+
)
|
442
|
+
dissolve.should.be.kind_of EDL::Event
|
443
|
+
|
444
|
+
dissolve.num.should.equal "025"
|
445
|
+
dissolve.reel.should.equal "GEN"
|
446
|
+
dissolve.track.should.equal "V"
|
447
|
+
|
448
|
+
dissolve.should.be.has_transition
|
449
|
+
|
450
|
+
tr = dissolve.transition
|
451
|
+
|
452
|
+
tr.should.be.kind_of EDL::Dissolve
|
453
|
+
tr.duration.should.equal 25
|
454
|
+
end
|
455
|
+
|
456
|
+
specify "set flag on the previous event in the stack when a dissolve is encountered" do
|
457
|
+
m = EDL::EventMatcher.new(25)
|
458
|
+
previous_evt = flexmock
|
459
|
+
previous_evt.should_receive(:outgoing_transition_duration=).with(25).once
|
460
|
+
|
461
|
+
m.apply([previous_evt],
|
462
|
+
'025 GEN V D 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20'
|
463
|
+
)
|
464
|
+
end
|
465
|
+
|
466
|
+
specify "generate a Wipe" do
|
467
|
+
m = EDL::EventMatcher.new(25)
|
468
|
+
wipe = m.apply([],
|
469
|
+
'025 GEN V W001 025 00:00:55:10 00:00:58:11 01:00:29:19 01:00:32:20'
|
470
|
+
)
|
471
|
+
|
472
|
+
tr = wipe.transition
|
473
|
+
tr.should.be.kind_of EDL::Wipe
|
474
|
+
tr.duration.should.equal 25
|
475
|
+
tr.smpte_wipe_index.should.equal '001'
|
476
|
+
end
|
477
|
+
|
478
|
+
specify "match the widest range of patterns" do
|
479
|
+
EVT_PATTERNS.each do | pat |
|
480
|
+
assert EDL::EventMatcher.new(25).matches?(pat), "EventMatcher should match #{pat}"
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
specify "pass the framerate that it received upon instantiation to the Timecodes being created" do
|
485
|
+
|
486
|
+
m = EDL::EventMatcher.new(30)
|
487
|
+
clip = m.apply([],
|
488
|
+
'020 008C V C 08:04:24:24 08:04:25:19 01:00:25:22 01:00:26:17'
|
489
|
+
)
|
490
|
+
clip.rec_start_tc.fps.should.equal 30
|
491
|
+
clip.rec_end_tc.fps.should.equal 30
|
492
|
+
clip.src_start_tc.fps.should.equal 30
|
493
|
+
clip.src_end_tc.fps.should.equal 30
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
context "CommentMatcher should" do
|
498
|
+
specify "match a comment" do
|
499
|
+
line = "* COMMENT: PURE BULLSHIT"
|
500
|
+
assert EDL::CommentMatcher.new.matches?(line)
|
501
|
+
end
|
502
|
+
|
503
|
+
specify "apply the comment to the last clip on the stack" do
|
504
|
+
line = "* COMMENT: PURE BULLSHIT"
|
505
|
+
|
506
|
+
comments = []
|
507
|
+
mok_evt = flexmock
|
508
|
+
|
509
|
+
2.times { mok_evt.should_receive(:comments).and_return(comments) }
|
510
|
+
2.times { EDL::CommentMatcher.new.apply([mok_evt], line) }
|
511
|
+
|
512
|
+
mok_evt.comments.should.equal ["* COMMENT: PURE BULLSHIT", "* COMMENT: PURE BULLSHIT"]
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
context "FallbackMatcher should" do
|
517
|
+
specify "match anything" do
|
518
|
+
line = "SOME"
|
519
|
+
EDL::FallbackMatcher.new.matches?(line).should.equal true
|
520
|
+
|
521
|
+
line = "OR ANOTHER "
|
522
|
+
EDL::FallbackMatcher.new.matches?(line).should.equal true
|
523
|
+
end
|
524
|
+
|
525
|
+
specify "not match whitespace" do
|
526
|
+
line = "\s\s\s\r\n\r"
|
527
|
+
EDL::FallbackMatcher.new.matches?(line).should.equal false
|
528
|
+
end
|
529
|
+
|
530
|
+
specify "append the matched content to comments" do
|
531
|
+
e = flexmock
|
532
|
+
cmts = []
|
533
|
+
e.should_receive(:comments).and_return(cmts)
|
534
|
+
|
535
|
+
EDL::FallbackMatcher.new.apply([e], "FOOBAR")
|
536
|
+
cmts.should.equal ["FOOBAR"]
|
537
|
+
|
538
|
+
EDL::FallbackMatcher.new.apply([e], "FINAL CUT PRO REEL: 006-I REPLACED BY: 006I")
|
539
|
+
cmts.should.equal ["FOOBAR", "FINAL CUT PRO REEL: 006-I REPLACED BY: 006I"]
|
540
|
+
end
|
541
|
+
|
542
|
+
specify "raise an ApplyError if no clip is on the stack" do
|
543
|
+
lambda { EDL::FallbackMatcher.new.apply([], "FINAL CUT PRO REEL: 006-I REPLACED BY: 006I") }.should.raise(EDL::Matcher::ApplyError)
|
544
|
+
end
|
545
|
+
|
546
|
+
end
|
547
|
+
|
548
|
+
context "ClipNameMatcher should" do
|
549
|
+
specify "match a clip name" do
|
550
|
+
line = "* FROM CLIP NAME: TAPE_6-10.MOV"
|
551
|
+
EDL::NameMatcher.new.matches?(line).should.equal true
|
552
|
+
end
|
553
|
+
|
554
|
+
specify "not match a simple comment" do
|
555
|
+
line = "* CRAP"
|
556
|
+
EDL::NameMatcher.new.matches?(line).should.equal false
|
557
|
+
end
|
558
|
+
|
559
|
+
specify "apply the name to the last event on the stack" do
|
560
|
+
line = "* FROM CLIP NAME: TAPE_6-10.MOV"
|
561
|
+
|
562
|
+
mok_evt = flexmock
|
563
|
+
comments = []
|
564
|
+
mok_evt.should_receive(:clip_name=).with('TAPE_6-10.MOV').once
|
565
|
+
mok_evt.should_receive(:comments).and_return(comments).once
|
566
|
+
|
567
|
+
EDL::NameMatcher.new.apply([mok_evt], line)
|
568
|
+
comments.should.equal ["* FROM CLIP NAME: TAPE_6-10.MOV"]
|
569
|
+
end
|
570
|
+
|
571
|
+
end
|
572
|
+
|
573
|
+
context "EffectMatcher should" do
|
574
|
+
specify "not match a simple comment" do
|
575
|
+
line = "* STUFF"
|
576
|
+
EDL::EffectMatcher.new.matches?(line).should.equal false
|
577
|
+
end
|
578
|
+
|
579
|
+
specify "match a dissolve name" do
|
580
|
+
line = "* EFFECT NAME: CROSS DISSOLVE"
|
581
|
+
EDL::EffectMatcher.new.matches?(line).should.equal true
|
582
|
+
end
|
583
|
+
|
584
|
+
specify "apply the effect name to the transition of the last event on the stack" do
|
585
|
+
line = "* EFFECT NAME: CROSS DISSOLVE"
|
586
|
+
mok_evt, mok_transition = flexmock, flexmock
|
587
|
+
cmt = []
|
588
|
+
|
589
|
+
mok_evt.should_receive(:transition).once.and_return(mok_transition)
|
590
|
+
mok_evt.should_receive(:comments).once.and_return(cmt)
|
591
|
+
|
592
|
+
mok_transition.should_receive(:effect=).with("CROSS DISSOLVE").once
|
593
|
+
|
594
|
+
EDL::EffectMatcher.new.apply([mok_evt], line)
|
595
|
+
|
596
|
+
cmt.should.equal ["* EFFECT NAME: CROSS DISSOLVE"]
|
597
|
+
end
|
598
|
+
|
599
|
+
end
|
600
|
+
|
601
|
+
context "A complex EDL passed via Parser should" do
|
602
|
+
specify "parse without errors" do
|
603
|
+
assert_nothing_raised { EDL::Parser.new.parse(File.open(FORTY_FIVER)) }
|
604
|
+
end
|
605
|
+
|
606
|
+
# TODO: this does not belong here
|
607
|
+
specify "be properly rewritten from zero" do
|
608
|
+
complex = EDL::Parser.new.parse(File.open(FORTY_FIVER))
|
609
|
+
from_zero = complex.from_zero
|
610
|
+
|
611
|
+
# Should have the same number of events
|
612
|
+
from_zero.length.should.equal complex.length
|
613
|
+
|
614
|
+
from_zero[0].rec_start_tc.should.be.zero
|
615
|
+
from_zero[-1].rec_end_tc.should.equal '00:00:42:16'.tc
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
context "A FinalCutPro speedup with fade at the end should" do
|
620
|
+
specify "be parsed cleanly" do
|
621
|
+
list = EDL::Parser.new.parse(File.open(SPEEDUP_AND_FADEOUT))
|
622
|
+
|
623
|
+
list.length.should.equal 2
|
624
|
+
|
625
|
+
first_evt = list[0]
|
626
|
+
|
627
|
+
tw = first_evt.timewarp
|
628
|
+
tw.should.be.kind_of EDL::Timewarp
|
629
|
+
|
630
|
+
first_evt.rec_length.should.equal 689
|
631
|
+
first_evt.rec_length_with_transition.should.equal 714
|
632
|
+
|
633
|
+
tw.actual_length_of_source.should.equal 1000
|
634
|
+
tw.speed.should.equal 140
|
635
|
+
|
636
|
+
assert_equal 1000, first_evt.src_length
|
637
|
+
|
638
|
+
assert_equal "01:00:00:00", first_evt.capture_from_tc.to_s
|
639
|
+
assert_equal "01:00:40:00", first_evt.capture_to_tc.to_s
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
context "In the trailer EDL the event 4 should" do
|
644
|
+
specify "not have too many comments" do
|
645
|
+
evts = EDL::Parser.new.parse(File.open(TRAILER_EDL))
|
646
|
+
evt = evts[6]
|
647
|
+
evt.comments.length.should.equal(5)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
context "A FinalCutPro speedup and reverse with fade at the end should" do
|
652
|
+
specify "parse cleanly" do
|
653
|
+
first_evt = EDL::Parser.new.parse(File.open(SPEEDUP_REVERSE_AND_FADEOUT)).shift
|
654
|
+
|
655
|
+
first_evt.should.be.reverse
|
656
|
+
|
657
|
+
first_evt.rec_length.should.equal 689
|
658
|
+
first_evt.rec_length_with_transition.should.equal 714
|
659
|
+
|
660
|
+
tw = first_evt.timewarp
|
661
|
+
tw.source_used_from.should.equal "1h 1f".tc
|
662
|
+
tw.source_used_upto.should.equal "1h 40s".tc
|
663
|
+
end
|
664
|
+
end
|