Event reimplementation

Implementation of events and edits
This commit is contained in:
Jamie Hardt
2018-12-24 13:25:21 -08:00
parent e0b7025fff
commit 8d3bef2c09
4 changed files with 121 additions and 76 deletions

View File

@@ -11,54 +11,16 @@ The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and it
* Remark or comment fields with common recognized forms are read and * Remark or comment fields with common recognized forms are read and
available to the client, including clip name and source file data. available to the client, including clip name and source file data.
* Symbolically decodes transitions * Symbolically decodes transitions
* Does not parse or validate timecodes, does not enforce framerates, does not
parameterize timecode or framerates in any way. This makes the parser more
tolerant of EDLs with mixed rates.
## Usage ## Usage
``` ```
>>> import pycmx
>>> result = pycmx.parse_cmx3600("STP R1 v082517.edl")
>>> print(result[0:3])
[CmxEvent(title='STP_Reel 1_082517',number=1,
clip_name='FKI_LEADER_HEAD_1920X1080.MOV',
source_name='FKI_LEADER_HEAD_1920X1080.MOV',
channels=CmxChannelMap(v=True, audio_channels=set()),
transition=CmxTransition(transition='C',operand=''),
source_start='01:00:00:00',source_finish='01:00:08:00',
record_start='01:00:00:00',record_finish='01:00:08:00',
fcm_drop=False,remarks=[],line_number=2),
CmxEvent(title='STP_Reel 1_082517',number=2,
clip_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
source_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
channels=CmxChannelMap(v=True, audio_channels=set()),
transition=CmxTransition(transition='C',operand=''),
source_start='01:00:00:00',source_finish='01:00:14:23',
record_start='01:00:00:00',record_finish='01:00:23:00',
fcm_drop=False,remarks=[],line_number=5),
CmxEvent(title='STP_Reel 1_082517',number=3,
clip_name='V4L-1*',
source_name='B116C001_150514_R0UR',
channels=CmxChannelMap(v=True, audio_channels=set()),
transition=CmxTransition(transition='C',operand=''),
source_start='16:37:29:06',source_finish='16:37:40:22',
record_start='16:37:29:06',record_finish='01:00:50:09',
fcm_drop=False,remarks=[],line_number=8)]
``` ```
## Known Issues/Roadmap
To be addressed:
* Does not decode "M2" speed changes.
* Does not decode repair notes, audio notes or other Avid-specific notes.
* Does not decode Avid marker list.
May not be addressed:
* Does not parse source list at end of EDL.
Probably beyond the scope of this module:
* Does not parse timecode entries.
* Does not parse color correction notes. For this functionality we refer you to [pycdl](https://pypi.org/project/pycdl/) or [cdl-convert](https://pypi.org/project/cdl-convert/).
## Should I Use This? ## Should I Use This?
At this time, this is (at best) alpha software and the interface will be At this time, this is (at best) alpha software and the interface will be

View File

@@ -2,7 +2,7 @@
# (c) 2018 Jamie Hardt # (c) 2018 Jamie Hardt
from .parse_cmx_statements import (parse_cmx3600_statements, from .parse_cmx_statements import (parse_cmx3600_statements,
StmtEvent, StmtFCM, StmtTitle) StmtEvent, StmtFCM, StmtTitle, StmtClipName, StmtSourceFile, StmtAudioExt)
from collections import namedtuple from collections import namedtuple
@@ -20,48 +20,113 @@ class EditList:
'The title of the edit list' 'The title of the edit list'
return self.title_statement.title return self.title_statement.title
@property
def events(self): def events(self):
'Each event in the edit list' 'A generator for all the events in the edit list'
def events_p(statements_rest, curr_event_num, is_drop = None
statements_event, events, is_drop): current_event_num = None
event_statements = []
stmt = statements_rest[0] for stmt in self.event_statements:
rem = statements_rest[1:] if type(stmt) is StmtFCM:
is_drop = stmt.drop
elif type(stmt) is StmtEvent:
if type(stmt) is StmtEvent: if current_event_num is None:
if stmt.event == curr_event_num: current_event_num = stmt.event
return ( rem,curr_event_num,statements_event + [stmt],events,is_drop) event_statements.append(stmt)
else: else:
new_event = Event(statements_event) if current_event_num != stmt.event:
return ( rem,stmt.event, [stmt], events + [new_event],is_drop ) yield Event(statements=event_statements)
event_statements = [stmt]
current_event_num = stmt.event
else:
event_statements.append(stmt)
elif type(stmt) is StmtFCM:
return ( rem, curr_event_num, statements_event, events,stmt.drop)
else: else:
return ( rem, curr_event_num, statements_event + [stmt],events, is_drop) event_statements.append(stmt)
result = (self.event_statements, None, [], [], False) yield Event(statements=event_statements)
while True:
if len(result[0]) == 0:
return result[3]
else:
result = events_p(*result)
class Edit:
def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement):
self.edit_statement = edit_statement
self.audio_ext = audio_ext_statement
self.clip_name_statement = clip_name_statement
self.source_file_statement = source_file_statement
@property
def source(self):
return self.edit_statement.source
@property
def clip_name(self):
if self.clip_name_statement != None:
return self.clip_name_statement.name
else:
return None
Edit = namedtuple("Edit","channels transition source_ref source_start source_finish record_start record_finish")
class Event: class Event:
def __init__(self, statements): def __init__(self, statements):
self.statements = statements self.statements = statements
def number(): @property
return statements[0].event def number(self):
return self._edit_statements()[0].event
@property
def edits(self):
edits_audio = list( self._statements_with_audio_ext() )
clip_names = self._clip_name_statements()
source_files= self._source_file_statements()
the_zip = [edits_audio]
if len(edits_audio) == 2:
cn = [None, None]
for clip_name in clip_names:
if clip_name.affect == 'from':
cn[0] = clip_name
elif clip_name.affect == 'to':
cn[1] = clip_name
the_zip.append(cn)
else:
if len(edits_audio) == len(clip_names):
the_zip.append(clip_names)
else:
the_zip.append([None] * len(edits_audio) )
if len(edits_audio) == len(source_files):
the_zip.append(source_files)
elif len(source_files) == 1:
the_zip.append( source_files * len(edits_audio) )
else:
the_zip.append([None] * len(edits_audio) )
return [ Edit(e1[0],e1[1],n1,s1) for (e1,n1,s1) in zip(*the_zip) ]
def _edit_statements(self):
return [s for s in self.statements if type(s) is StmtEvent]
def _clip_name_statements(self):
return [s for s in self.statements if type(s) is StmtClipName]
def _source_file_statements(self):
return [s for s in self.statements if type(s) is StmtSourceFile]
def _statements_with_audio_ext(self):
for (s1, s2) in zip(self.statements, self.statements[1:]):
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
yield (s1,s2)
elif type(s1) is StmtEvent:
yield (s1, None)
def edits():
for statement in self.statements:

View File

@@ -13,7 +13,7 @@ StmtFCM = namedtuple("FCM",["drop","line_number"])
StmtEvent = namedtuple("Event",["event","source","channels","trans",\ StmtEvent = namedtuple("Event",["event","source","channels","trans",\
"trans_op","source_in","source_out","record_in","record_out","line_number"]) "trans_op","source_in","source_out","record_in","record_out","line_number"])
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"]) StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
StmtClipName = namedtuple("ClipName",["name","line_number"]) StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
StmtRemark = namedtuple("Remark",["text","line_number"]) StmtRemark = namedtuple("Remark",["text","line_number"])
StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
@@ -109,7 +109,9 @@ def parse_extended_audio_channels(line, line_number):
def parse_remark(line, line_number): def parse_remark(line, line_number):
if line.startswith("FROM CLIP NAME:"): if line.startswith("FROM CLIP NAME:"):
return StmtClipName(name=line[15:].strip() , line_number=line_number) return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
elif line.startswith("TO CLIP NAME:"):
return StmtClipName(name=line[13:].strip(), affect="to", line_number=line_number)
elif line.startswith("SOURCE FILE:"): elif line.startswith("SOURCE FILE:"):
return StmtSourceFile(filename=line[12:].strip() , line_number=line_number) return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
else: else:

View File

@@ -16,8 +16,24 @@ class TestParse(TestCase):
for fn, count in zip(files, counts): for fn, count in zip(files, counts):
edl = pycmx.parse_cmx3600(f"tests/edls/{fn}" ) edl = pycmx.parse_cmx3600(f"tests/edls/{fn}" )
actual = len(edl.events()) actual = len(list(edl.events ))
self.assertTrue( actual == count , f"expected {count} but found {actual}") self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}")
def test_events(self):
edl = pycmx.parse_cmx3600("tests/edls/TEST.edl")
events = list( edl.events )
self.assertEqual( int(events[0].number) , 1)
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
self.assertEqual( int(events[42].number) , 43)
self.assertEqual( events[42].edits[0].source , "TC_R1_V1")
self.assertEqual( events[42].edits[0].clip_name , "TC R1 V1.2 TEMP1 FX ST.WAV")
self.assertEqual( events[42].edits[1].source , "TC_R1_V6")
self.assertEqual( events[42].edits[1].clip_name , "TC R1 V6 TEMP2 ST FX.WAV")