1
# ##### BEGIN GPL LICENSE BLOCK #####
3
# This program is free software; you can redistribute it and/or
4
# modify it under the terms of the GNU General Public License
5
# as published by the Free Software Foundation; either version 2
6
# of the License, or (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software Foundation,
15
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
# ##### END GPL LICENSE BLOCK #####
22
This is a pure python module (no blender deps),
23
that parses EDL files and could be used outside of blender.
29
also supports conversion from other time strings used by EDL
39
def __init__(self, data, fps):
42
self.from_string(data)
43
frame = self.as_frame()
44
self.from_frame(frame)
48
def from_string(self, text):
50
# No dropframe support yet
52
if text.lower().endswith("mps"): # 5.2mps
53
return self.from_frame(int(float(text[:-3]) * self.fps))
54
elif text.lower().endswith("s"): # 5.2s
55
return self.from_frame(int(float(text[:-1]) * self.fps))
56
elif text.isdigit(): # 1234
57
return self.from_frame(int(text))
58
elif ":" in text: # hh:mm:ss:ff
59
text = text.replace(";", ":").replace(",", ":").replace(".", ":")
60
text = text.split(":")
62
self.hours = int(text[0])
63
self.minutes = int(text[1])
64
self.seconds = int(text[2])
65
self.frame = int(text[3])
68
print("ERROR: could not convert this into timecode %r" % text)
71
def from_frame(self, frame):
85
self.hours = int(frame / fph)
91
self.minutes = int(frame / fpm)
97
self.seconds = int(frame / self.fps)
98
frame = frame % self.fps
103
self.frame = -self.frame
104
self.seconds = -self.seconds
105
self.minutes = -self.minutes
106
self.hours = -self.hours
111
abs_frame = self.frame
112
abs_frame += self.seconds * self.fps
113
abs_frame += self.minutes * 60 * self.fps
114
abs_frame += self.hours * 60 * 60 * self.fps
119
self.from_frame(int(self))
120
return "%.2d:%.2d:%.2d:%.2d" % (self.hours, self.minutes, self.seconds, self.frame)
123
return self.as_string()
125
# Numeric stuff, may as well have this
127
return TimeCode(-int(self), self.fps)
130
return self.as_frame()
132
def __sub__(self, other):
133
return TimeCode(int(self) - int(other), self.fps)
135
def __add__(self, other):
136
return TimeCode(int(self) + int(other), self.fps)
138
def __mul__(self, other):
139
return TimeCode(int(self) * int(other), self.fps)
141
def __div__(self, other):
142
return TimeCode(int(self) // int(other), self.fps)
145
return TimeCode(abs(int(self)), self.fps)
147
def __iadd__(self, other):
148
return self.from_frame(int(self) + int(other))
150
def __imul__(self, other):
151
return self.from_frame(int(self) * int(other))
153
def __idiv__(self, other):
154
return self.from_frame(int(self) // int(other))
159
Comments can appear at the beginning of the EDL file (header) or between the edit lines in the EDL. The first block of comments in the file is defined to be the header comments and they are associated with the EDL as a whole. Subsequent comments in the EDL file are associated with the first edit line that appears after them.
161
<filename|tag> <EditMode> <TransitionType>[num] [duration] [srcIn] [srcOut] [recIn] [recOut]
163
* <filename|tag>: Filename or tag value. Filename can be for an MPEG file, Image file, or Image file template. Image file templates use the same pattern matching as for command line glob, and can be used to specify images to encode into MPEG. i.e. /usr/data/images/image*.jpg
164
* <EditMode>: 'V' | 'A' | 'VA' | 'B' | 'v' | 'a' | 'va' | 'b' which equals Video, Audio, Video_Audio edits (note B or b can be used in place of VA or va).
165
* <TransitonType>: 'C' | 'D' | 'E' | 'FI' | 'FO' | 'W' | 'c' | 'd' | 'e' | 'fi' | 'fo' | 'w'. which equals Cut, Dissolve, Effect, FadeIn, FadeOut, Wipe.
166
* [num]: if TransitionType = Wipe, then a wipe number must be given. At the moment only wipe 'W0' and 'W1' are supported.
167
* [duration]: if the TransitionType is not equal to Cut, then an effect duration must be given. Duration is in frames.
168
* [srcIn]: Src in. If no srcIn is given, then it defaults to the first frame of the video or the first frame in the image pattern. If srcIn isn't specified, then srcOut, recIn, recOut can't be specified.
169
* [srcOut]: Src out. If no srcOut is given, then it defaults to the last frame of the video - or last image in the image pattern. if srcOut isn't given, then recIn and recOut can't be specified.
170
* [recIn]: Rec in. If no recIn is given, then it is calculated based on its position in the EDL and the length of its input.
171
[recOut]: Rec out. If no recOut is given, then it is calculated based on its position in the EDL and the length of its input. first frame of the video.
173
For srcIn, srcOut, recIn, recOut, the values can be specified as either timecode, frame number, seconds, or mps seconds. i.e.
174
[tcode | fnum | sec | mps], where:
176
* tcode : SMPTE timecode in hh:mm:ss:ff
177
* fnum : frame number (the first decodable frame in the video is taken to be frame 0).
178
* sec : seconds with 's' suffix (e.g. 5.2s)
179
* mps : seconds with 'mps' suffix (e.g. 5.2mps). This corresponds to the 'seconds' value displayed by Windows MediaPlayer.
187
TRANSITION_UNKNOWN = enum
188
TRANSITION_CUT = enum
190
TRANSITION_DISSOLVE = enum
192
TRANSITION_EFFECT = enum
194
TRANSITION_FADEIN = enum
196
TRANSITION_FADEOUT = enum
198
TRANSITION_WIPE = enum
200
TRANSITION_KEY = enum
205
"d": TRANSITION_DISSOLVE,
206
"e": TRANSITION_EFFECT,
207
"fi": TRANSITION_FADEIN,
208
"fo": TRANSITION_FADEOUT,
209
"w": TRANSITION_WIPE,
214
EDIT_UNKNOWN = 1 << enum
216
EDIT_VIDEO = 1 << enum
218
EDIT_AUDIO = 1 << enum
220
EDIT_AUDIO_STEREO = 1 << enum
222
EDIT_VIDEO_AUDIO = 1 << enum
226
"none": 0, # TODO, investigate this more.
229
"aa": EDIT_AUDIO_STEREO,
230
"va": EDIT_VIDEO_AUDIO,
231
"b": EDIT_VIDEO_AUDIO,
246
KEY_IN = enum # This is assumed if no second type is set
261
Non-dropframe: 1:00:00:00 - colon in last position
262
Dropframe: 1:00:00;00 - semicolon in last position
263
PAL/SECAM: 1:00:00:00 - colon in last position
266
Non-dropframe: 1:00:00.00 - period in last position
267
Dropframe: 1:00:00,00 - comma in last position
268
PAL/SECAM: 1:00:00.00 - period in last position
272
t = abs(timecode('-124:-12:-43:-22', 25))
281
"transition_duration",
297
def edit_flags_to_text(flag):
298
return "/".join([item for item, val in EDIT_DICT.items() if val & flag])
301
def strip_digits(text):
302
return "".join(filter(lambda x: not x.isdigit(), text))
304
def __init__(self, text=None, fps=25):
307
self.reel = "" # Uniqie name for this 'file' but not filename, when BL signifies black
308
self.transition_duration = 0
309
self.edit_type = EDIT_UNKNOWN
310
self.transition_type = TRANSITION_UNKNOWN
311
self.wipe_type = WIPE_UNKNOWN
312
self.key_type = KEY_UNKNOWN
313
self.key_fade = -1 # true/false
314
self.srcIn = None # Where on the original field recording the event begins
315
self.srcOut = None # Where on the original field recording the event ends
316
self.recIn = None # Beginning of the original event in the edited program
317
self.recOut = None # End of the original event in the edited program
318
self.m2 = None # fps set by the m2 command
321
self.custom_data = [] # use for storing any data you want (blender strip for eg)
327
txt = "num: %d, " % self.number
328
txt += "reel: %s, " % self.reel
330
txt += EditDecision.edit_flags_to_text(self.edit_type) + ", "
332
txt += "trans_type: "
333
for item, val in TRANSITION_DICT.items():
334
if val == self.transition_type:
340
txt += "%g" % float(self.m2.fps)
347
txt += "recIn: " + str(self.recIn) + ", "
348
txt += "recOut: " + str(self.recOut) + ", "
349
txt += "srcIn: " + str(self.srcIn) + ", "
350
txt += "srcOut: " + str(self.srcOut) + ", "
354
def read(self, line, fps):
357
self.number = int(line[index])
359
self.reel = line[index].lower()
362
# AA/V can be an edit type
364
for edit_type in line[index].lower().split("/"):
365
# stripping digits is done because we don't do 'a1, a2...'
366
self.edit_type |= EDIT_DICT[EditDecision.strip_digits(edit_type)]
369
tx_name = "".join([c for c in line[index].lower() if not c.isdigit()])
370
self.transition_type = TRANSITION_DICT[tx_name] # advance the index later
372
if self.transition_type == TRANSITION_WIPE:
373
tx_num = "".join([c for c in line[index].lower() if c.isdigit()])
379
self.wipe_type = tx_num
381
elif self.transition_type == TRANSITION_KEY: # UNTESTED
383
val = line[index + 1].lower()
386
self.key_type = KEY_BG
389
self.key_type = KEY_OUT
392
self.key_type = KEY_IN # if no args given
394
# there may be an (F) after, eg 'K B (F)'
395
# in the docs this should only be after K B but who knows, it may be after K O also?
396
val = line[index + 1].lower()
401
self.key_fade = False
405
if self.transition_type in {TRANSITION_DISSOLVE, TRANSITION_EFFECT, TRANSITION_FADEIN, TRANSITION_FADEOUT, TRANSITION_WIPE}:
406
self.transition_duration = TimeCode(line[index], fps)
409
if index < len(line):
410
self.srcIn = TimeCode(line[index], fps)
412
if index < len(line):
413
self.srcOut = TimeCode(line[index], fps)
416
if index < len(line):
417
self.recIn = TimeCode(line[index], fps)
419
if index < len(line):
420
self.recOut = TimeCode(line[index], fps)
424
self.edits.sort(key=lambda e: int(e.recIn))
425
for i, edit in enumerate(self.edits):
437
for k, v in TRANSITION_DICT.items():
438
if v == self.transition_type:
442
return "%d_%s_%s" % (self.number, self.reel, cut_type)
464
def read(self, line, fps):
466
# M2 TAPEC 050.5 00:08:11:08
469
self.reel = words[1].lower()
470
self.fps = float(words[2])
471
self.time = TimeCode(words[3], fps)
486
def parse(self, filename, fps):
488
file = open(filename, "r", encoding="utf-8")
493
edits_m2 = [] # edits with m2's
498
line = " ".join(line.split())
500
if not line or line.startswith(("*", "#")):
502
elif line.startswith("TITLE:"):
503
self.title = " ".join(line.split()[1:])
504
elif line.split()[0].lower() == "m2":
509
elif not line.split()[0].isdigit():
510
print("Ignoring:", line)
512
self.edits.append(EditDecision(line, fps))
513
edits_m2.append(self.edits[-1])
518
for item in edits_m2:
519
if isinstance(item, M2):
526
# Set total group indexes
527
for item in reversed(edits_m2):
528
if isinstance(item, M2):
530
tot_m2 = item.index + 1
537
for i, item in enumerate(edits_m2):
538
if isinstance(item, M2):
539
# make a list of all items that match the m2's reel name
540
edits_m2_tmp = [item_tmp for item_tmp in edits_m2 if (isinstance(item, M2) or item_tmp.reel == item.reel)]
543
i_tmp = edits_m2_tmp.index(item)
545
# Seek back to get the edit.
546
edit = edits_m2[i_tmp - item.tot]
548
# Note, docs say time should also match with edit start time
549
# but from final cut pro, this seems not to be the case
550
if not isinstance(edit, EditDecision):
551
print("ERROR!", "M2 incorrect")
558
def overlap_test(self, edit_test):
559
recIn = int(edit_test.recIn)
560
recOut = int(edit_test.recOut)
562
for edit in self.edits:
563
if edit is edit_test:
566
recIn_other = int(edit.recIn)
567
recOut_other = int(edit.recOut)
569
if recIn_other < recIn < recOut_other:
571
if recIn_other < recOut < recOut_other:
574
if recIn < recIn_other < recOut:
576
if recIn < recOut_other < recOut:
581
def reels_as_dict(self):
583
for edit in self.edits:
584
reels.setdefault(edit.reel, []).append(edit)