pycmx 1.2.3__py3-none-any.whl → 1.4.0__py3-none-any.whl

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.
pycmx/__init__.py CHANGED
@@ -2,13 +2,11 @@
2
2
  """
3
3
  pycmx is a parser for CMX 3600-style EDLs.
4
4
 
5
- This module (c) 2023 Jamie Hardt. For more information on your rights to
5
+ This module (c) 2025 Jamie Hardt. For more information on your rights to
6
6
  copy and reuse this software, refer to the LICENSE file included with the
7
7
  distribution.
8
8
  """
9
9
 
10
- __version__ = '1.2.2'
11
-
12
10
  from .parse_cmx_events import parse_cmx3600
13
11
  from .transition import Transition
14
12
  from .event import Event
pycmx/cdl.py ADDED
@@ -0,0 +1,48 @@
1
+ # pycmx
2
+ # (c) 2025 Jamie Hardt
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Generic, NamedTuple, TypeVar
6
+
7
+ T = TypeVar('T')
8
+
9
+
10
+ @dataclass
11
+ class Rgb(Generic[T]):
12
+ """
13
+ A tuple of three `T`s, where each is the respective red, green and blue
14
+ values of interest.
15
+ """
16
+
17
+ red: T # : Red component
18
+ green: T # : Green component
19
+ blue: T # : Blue component
20
+
21
+
22
+ @dataclass
23
+ class AscSopComponents(Generic[T]):
24
+ """
25
+ Fields in an ASC SOP (Slope-Offset-Power) color transfer function
26
+ statement.
27
+
28
+ The ASC SOP is a transfer function of the form:
29
+
30
+ :math:`y_{color} = (ax_{color} + b)^p`
31
+
32
+ for each color component the source, where the `slope` is `a`, `offset`
33
+ is `b` and `power` is `p`.
34
+ """
35
+
36
+ slope: Rgb[T] # : The linear/slope component `a`
37
+ offset: Rgb[T] # : The constant/offset component `b`
38
+ power: Rgb[T] # : The exponential/power component `p`
39
+
40
+
41
+ class FramecountTriple(NamedTuple):
42
+ """
43
+ Fields in an FRMC statement
44
+ """
45
+
46
+ start: int
47
+ end: int
48
+ duration: int
pycmx/channel_map.py CHANGED
@@ -1,26 +1,27 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
4
  from re import (compile, match)
5
5
  from typing import Dict, Tuple, Generator
6
6
 
7
+
7
8
  class ChannelMap:
8
9
  """
9
10
  Represents a set of all the channels to which an event applies.
10
11
  """
11
12
 
12
- _chan_map : Dict[str, Tuple] = {
13
- "V" : (True, False, False),
14
- "A" : (False, True, False),
15
- "A2" : (False, False, True),
16
- "AA" : (False, True, True),
17
- "B" : (True, True, False),
18
- "AA/V" : (True, True, True),
19
- "A2/V" : (True, False, True)
20
- }
13
+ _chan_map: Dict[str, Tuple] = {
14
+ "V": (True, False, False),
15
+ "A": (False, True, False),
16
+ "A2": (False, False, True),
17
+ "AA": (False, True, True),
18
+ "B": (True, True, False),
19
+ "AA/V": (True, True, True),
20
+ "A2/V": (True, False, True)
21
+ }
21
22
 
22
23
  def __init__(self, v=False, audio_channels=set()):
23
- self._audio_channel_set = audio_channels
24
+ self._audio_channel_set = audio_channels
24
25
  self.v = v
25
26
 
26
27
  @property
@@ -46,7 +47,7 @@ class ChannelMap:
46
47
 
47
48
  @a1.setter
48
49
  def a1(self, val: bool):
49
- self.set_audio_channel(1,val)
50
+ self.set_audio_channel(1, val)
50
51
 
51
52
  @property
52
53
  def a2(self) -> bool:
@@ -55,7 +56,7 @@ class ChannelMap:
55
56
 
56
57
  @a2.setter
57
58
  def a2(self, val: bool):
58
- self.set_audio_channel(2,val)
59
+ self.set_audio_channel(2, val)
59
60
 
60
61
  @property
61
62
  def a3(self) -> bool:
@@ -64,28 +65,28 @@ class ChannelMap:
64
65
 
65
66
  @a3.setter
66
67
  def a3(self, val: bool):
67
- self.set_audio_channel(3,val)
68
-
68
+ self.set_audio_channel(3, val)
69
+
69
70
  @property
70
71
  def a4(self) -> bool:
71
72
  """True if A4 is included"""
72
73
  return self.get_audio_channel(4)
73
74
 
74
75
  @a4.setter
75
- def a4(self,val: bool):
76
- self.set_audio_channel(4,val)
76
+ def a4(self, val: bool):
77
+ self.set_audio_channel(4, val)
77
78
 
78
79
  def get_audio_channel(self, chan_num) -> bool:
79
80
  """True if chan_num is included"""
80
81
  return (chan_num in self._audio_channel_set)
81
82
 
82
- def set_audio_channel(self,chan_num, enabled: bool):
83
+ def set_audio_channel(self, chan_num, enabled: bool):
83
84
  """If enabled is true, chan_num will be included"""
84
85
  if enabled:
85
86
  self._audio_channel_set.add(chan_num)
86
87
  elif self.get_audio_channel(chan_num):
87
88
  self._audio_channel_set.remove(chan_num)
88
-
89
+
89
90
  def _append_event(self, event_str):
90
91
  alt_channel_re = compile(r'^A(\d+)')
91
92
  if event_str in self._chan_map:
@@ -96,7 +97,7 @@ class ChannelMap:
96
97
  else:
97
98
  matchresult = match(alt_channel_re, event_str)
98
99
  if matchresult:
99
- self.set_audio_channel(int( matchresult.group(1)), True )
100
+ self.set_audio_channel(int(matchresult.group(1)), True)
100
101
 
101
102
  def _append_ext(self, audio_ext):
102
103
  self.a3 = audio_ext.audio3
@@ -109,5 +110,4 @@ class ChannelMap:
109
110
  out_v = self.video | other.video
110
111
  out_a = self._audio_channel_set | other._audio_channel_set
111
112
 
112
- return ChannelMap(v=out_v,audio_channels = out_a)
113
-
113
+ return ChannelMap(v=out_v, audio_channels=out_a)
pycmx/edit.py CHANGED
@@ -1,32 +1,60 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
3
-
2
+ # (c) 2018-2025 Jamie Hardt
3
+
4
+ from .cdl import AscSopComponents, FramecountTriple
5
+ from .statements import (
6
+ StmtCdlSat,
7
+ StmtCdlSop,
8
+ StmtFrmc,
9
+ StmtEvent,
10
+ StmtAudioExt,
11
+ StmtClipName,
12
+ StmtSourceFile,
13
+ StmtEffectsName,
14
+ )
4
15
  from .transition import Transition
5
16
  from .channel_map import ChannelMap
6
- # from .parse_cmx_statements import StmtEffectsName
7
17
 
8
18
  from typing import Optional
9
19
 
20
+
10
21
  class Edit:
11
22
  """
12
- An individual source-to-record operation, with a source roll, source and
23
+ An individual source-to-record operation, with a source roll, source and
13
24
  recorder timecode in and out, a transition and channels.
14
25
  """
15
- def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement, trans_name_statement = None):
16
- self.edit_statement = edit_statement
17
- self.audio_ext = audio_ext_statement
18
- self.clip_name_statement = clip_name_statement
19
- self.source_file_statement = source_file_statement
20
- self.trans_name_statement = trans_name_statement
26
+
27
+ def __init__(
28
+ self,
29
+ edit_statement: StmtEvent,
30
+ audio_ext_statement: Optional[StmtAudioExt],
31
+ clip_name_statement: Optional[StmtClipName],
32
+ source_file_statement: Optional[StmtSourceFile],
33
+ trans_name_statement: Optional[StmtEffectsName] = None,
34
+ asc_sop_statement: Optional[StmtCdlSop] = None,
35
+ asc_sat_statement: Optional[StmtCdlSat] = None,
36
+ frmc_statement: Optional[StmtFrmc] = None,
37
+ ) -> None:
38
+ # Assigning types for the attributes explicitly
39
+ self._edit_statement: StmtEvent = edit_statement
40
+ self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
41
+ self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
42
+ self._source_file_statement: Optional[StmtSourceFile] = \
43
+ source_file_statement
44
+ self._trans_name_statement: Optional[StmtEffectsName] = \
45
+ trans_name_statement
46
+ self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
47
+ self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
48
+ self._frmc_statement: Optional[StmtFrmc] = frmc_statement
21
49
 
22
50
  @property
23
51
  def line_number(self) -> int:
24
52
  """
25
53
  Get the line number for the "standard form" statement associated with
26
- this edit. Line numbers a zero-indexed, such that the
54
+ this edit. Line numbers a zero-indexed, such that the
27
55
  "TITLE:" record is line zero.
28
56
  """
29
- return self.edit_statement.line_number
57
+ return self._edit_statement.line_number
30
58
 
31
59
  @property
32
60
  def channels(self) -> ChannelMap:
@@ -34,27 +62,33 @@ class Edit:
34
62
  Get the :obj:`ChannelMap` object associated with this Edit.
35
63
  """
36
64
  cm = ChannelMap()
37
- cm._append_event(self.edit_statement.channels)
38
- if self.audio_ext != None:
39
- cm._append_ext(self.audio_ext)
65
+ cm._append_event(self._edit_statement.channels)
66
+ if self._audio_ext is not None:
67
+ cm._append_ext(self._audio_ext)
40
68
  return cm
41
69
 
42
70
  @property
43
71
  def transition(self) -> Transition:
44
72
  """
45
- Get the :obj:`Transition` object associated with this edit.
73
+ Get the :obj:`Transition` that initiates this edit.
46
74
  """
47
- if self.trans_name_statement:
48
- return Transition(self.edit_statement.trans, self.edit_statement.trans_op, self.trans_name_statement.name)
75
+ if self._trans_name_statement:
76
+ return Transition(
77
+ self._edit_statement.trans,
78
+ self._edit_statement.trans_op,
79
+ self._trans_name_statement.name,
80
+ )
49
81
  else:
50
- return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None)
51
-
82
+ return Transition(
83
+ self._edit_statement.trans, self._edit_statement.trans_op, None
84
+ )
85
+
52
86
  @property
53
87
  def source_in(self) -> str:
54
88
  """
55
89
  Get the source in timecode.
56
90
  """
57
- return self.edit_statement.source_in
91
+ return self._edit_statement.source_in
58
92
 
59
93
  @property
60
94
  def source_out(self) -> str:
@@ -62,7 +96,7 @@ class Edit:
62
96
  Get the source out timecode.
63
97
  """
64
98
 
65
- return self.edit_statement.source_out
99
+ return self._edit_statement.source_out
66
100
 
67
101
  @property
68
102
  def record_in(self) -> str:
@@ -70,7 +104,7 @@ class Edit:
70
104
  Get the record in timecode.
71
105
  """
72
106
 
73
- return self.edit_statement.record_in
107
+ return self._edit_statement.record_in
74
108
 
75
109
  @property
76
110
  def record_out(self) -> str:
@@ -78,7 +112,7 @@ class Edit:
78
112
  Get the record out timecode.
79
113
  """
80
114
 
81
- return self.edit_statement.record_out
115
+ return self._edit_statement.record_out
82
116
 
83
117
  @property
84
118
  def source(self) -> str:
@@ -86,19 +120,21 @@ class Edit:
86
120
  Get the source column. This is the 8, 32 or 128-character string on the
87
121
  event record line, this usually references the tape name of the source.
88
122
  """
89
- return self.edit_statement.source
123
+ return self._edit_statement.source
90
124
 
91
125
  @property
92
126
  def black(self) -> bool:
93
127
  """
94
- Black video or silence should be used as the source for this event.
128
+ The source field for thie edit was "BL". Black video or silence should
129
+ be used as the source for this event.
95
130
  """
96
131
  return self.source == "BL"
97
132
 
98
133
  @property
99
134
  def aux_source(self) -> bool:
100
135
  """
101
- An auxiliary source is the source of this event.
136
+ The source field for this edit was "AX". An auxiliary source is the
137
+ source for this event.
102
138
  """
103
139
  return self.source == "AX"
104
140
 
@@ -108,21 +144,71 @@ class Edit:
108
144
  Get the source file, as attested by a "* SOURCE FILE" remark on the
109
145
  EDL. This will return None if the information is not present.
110
146
  """
111
- if self.source_file_statement is None:
147
+ if self._source_file_statement is None:
112
148
  return None
113
149
  else:
114
- return self.source_file_statement.filename
150
+ return self._source_file_statement.filename
115
151
 
116
152
  @property
117
153
  def clip_name(self) -> Optional[str]:
118
154
  """
119
- Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
155
+ Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
120
156
  NAME" remark on the EDL. This will return None if the information is
121
157
  not present.
122
158
  """
123
- if self.clip_name_statement is None:
159
+ if self._clip_name_statement is None:
124
160
  return None
125
161
  else:
126
- return self.clip_name_statement.name
162
+ return self._clip_name_statement.name
163
+
164
+ @property
165
+ def asc_sop(self) -> Optional[AscSopComponents[float]]:
166
+ """
167
+ Get ASC CDL Slope-Offset-Power color transfer function for the edit,
168
+ if present. The ASC SOP is a transfer function of the form:
169
+
170
+ :math:`y = (ax + b)^p`
171
+
172
+ for each color component the source, where the `slope` is `a`, `offset`
173
+ is `b` and `power` is `p`.
174
+ """
175
+ if self._asc_sop_statement is None:
176
+ return None
177
+
178
+ return self._asc_sop_statement.cdl_sop
179
+
180
+ @property
181
+ def asc_sop_raw(self) -> Optional[str]:
182
+ """
183
+ ASC CDL Slope-Offset-Power statement raw line
184
+ """
185
+ if self._asc_sop_statement is None:
186
+ return None
187
+
188
+ return self._asc_sop_statement.line
189
+
190
+ @property
191
+ def asc_sat(self) -> Optional[float]:
192
+ """
193
+ Get ASC CDL saturation value for clip, if present
194
+ """
195
+ if self._asc_sat_statement is None:
196
+ return None
127
197
 
198
+ return self._asc_sat_statement.value
199
+
200
+ @property
201
+ def framecounts(self) -> Optional[FramecountTriple]:
202
+ """
203
+ Get frame count offset data, if it exists. If an FRMC statement exists
204
+ in the EDL for the event it will give an integer frame count for the
205
+ edit's source in and out times.
206
+ """
207
+ if not self._frmc_statement:
208
+ return None
128
209
 
210
+ return FramecountTriple(
211
+ start=self._frmc_statement.start,
212
+ end=self._frmc_statement.end,
213
+ duration=self._frmc_statement.duration,
214
+ )
pycmx/edit_list.py CHANGED
@@ -1,18 +1,22 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
4
+ from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
5
+ StmtUnrecognized, StmtSourceUMID)
5
6
  from .event import Event
6
7
  from .channel_map import ChannelMap
7
8
 
8
- from typing import Generator
9
+ from typing import Any, Generator
10
+
9
11
 
10
12
  class EditList:
11
13
  """
12
- Represents an entire edit decision list as returned by :func:`~pycmx.parse_cmx3600()`.
14
+ Represents an entire edit decision list as returned by
15
+ :func:`~pycmx.parse_cmx3600()`.
13
16
  """
17
+
14
18
  def __init__(self, statements):
15
- self.title_statement = statements[0]
19
+ self.title_statement: StmtTitle = statements[0]
16
20
  self.event_statements = statements[1:]
17
21
 
18
22
  @property
@@ -20,21 +24,23 @@ class EditList:
20
24
  """
21
25
  The detected format of the EDL. Possible values are: "3600", "File32",
22
26
  "File128", and "unknown".
27
+
28
+ Adobe EDLs with more than 999 events will be reported as "3600".
23
29
  """
24
- first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
30
+ first_event = next(
31
+ (s for s in self.event_statements if type(s) is StmtEvent), None)
25
32
 
26
33
  if first_event:
27
- if first_event.format == 8:
34
+ if first_event.source_field_size == 8:
28
35
  return '3600'
29
- elif first_event.format == 32:
36
+ elif first_event.source_field_size == 32:
30
37
  return 'File32'
31
- elif first_event.format == 128:
38
+ elif first_event.source_field_size == 128:
32
39
  return 'File128'
33
40
  else:
34
41
  return 'unknown'
35
42
  else:
36
43
  return 'unknown'
37
-
38
44
 
39
45
  @property
40
46
  def channels(self) -> ChannelMap:
@@ -48,7 +54,6 @@ class EditList:
48
54
  retval = retval | edit.channels
49
55
 
50
56
  return retval
51
-
52
57
 
53
58
  @property
54
59
  def title(self) -> str:
@@ -57,27 +62,26 @@ class EditList:
57
62
  """
58
63
  return self.title_statement.title
59
64
 
60
-
61
65
  @property
62
- def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
66
+ def unrecognized_statements(self) -> Generator[Any, None, None]:
63
67
  """
64
- A generator for all the unrecognized statements in the list.
68
+ A generator for all the unrecognized statements and
69
+ corrupt remarks in the list.
70
+
71
+ :yields: either a :class:`StmtUnrecognized` or
72
+ :class:`StmtCorruptRemark`
65
73
  """
66
74
  for s in self.event_statements:
67
- if type(s) is StmtUnrecognized:
75
+ if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
68
76
  yield s
69
-
70
77
 
71
78
  @property
72
79
  def events(self) -> Generator[Event, None, None]:
73
80
  'A generator for all the events in the edit list'
74
- is_drop = None
75
81
  current_event_num = None
76
82
  event_statements = []
77
83
  for stmt in self.event_statements:
78
- if type(stmt) is StmtFCM:
79
- is_drop = stmt.drop
80
- elif type(stmt) is StmtEvent:
84
+ if type(stmt) is StmtEvent:
81
85
  if current_event_num is None:
82
86
  current_event_num = stmt.event
83
87
  event_statements.append(stmt)
@@ -89,8 +93,6 @@ class EditList:
89
93
  else:
90
94
  event_statements.append(stmt)
91
95
 
92
- elif type(stmt) is StmtSourceUMID:
93
- break
94
96
  else:
95
97
  event_statements.append(stmt)
96
98
 
@@ -101,9 +103,7 @@ class EditList:
101
103
  """
102
104
  A generator for all of the sources in the list
103
105
  """
104
-
106
+
105
107
  for stmt in self.event_statements:
106
108
  if type(stmt) is StmtSourceUMID:
107
109
  yield stmt
108
-
109
-
pycmx/event.py CHANGED
@@ -1,19 +1,22 @@
1
1
  # pycmx
2
- # (c) 2023 Jamie Hardt
2
+ # (c) 2023-2025 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName)
5
- from .edit import Edit
4
+ from .statements import (StmtFrmc, StmtEvent, StmtClipName, StmtSourceFile,
5
+ StmtAudioExt, StmtUnrecognized, StmtEffectsName,
6
+ StmtCdlSop, StmtCdlSat)
7
+ from .edit import Edit
6
8
 
7
9
  from typing import List, Generator, Optional, Tuple, Any
8
10
 
11
+
9
12
  class Event:
10
13
  """
11
- Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same event number.
12
- """
14
+ Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
15
+ event number. """
13
16
 
14
17
  def __init__(self, statements):
15
18
  self.statements = statements
16
-
19
+
17
20
  @property
18
21
  def number(self) -> int:
19
22
  """
@@ -28,10 +31,10 @@ class Event:
28
31
  will have multiple edits when a dissolve, wipe or key transition needs
29
32
  to be performed.
30
33
  """
31
- edits_audio = list( self._statements_with_audio_ext() )
32
- clip_names = self._clip_name_statements()
33
- source_files= self._source_file_statements()
34
-
34
+ edits_audio = list(self._statements_with_audio_ext())
35
+ clip_names = self._clip_name_statements()
36
+ source_files = self._source_file_statements()
37
+
35
38
  the_zip: List[List[Any]] = [edits_audio]
36
39
 
37
40
  if len(edits_audio) == 2:
@@ -45,19 +48,19 @@ class Event:
45
48
  end_name = clip_name
46
49
 
47
50
  the_zip.append([start_name, end_name])
48
- else:
51
+ else:
49
52
  if len(edits_audio) == len(clip_names):
50
53
  the_zip.append(clip_names)
51
54
  else:
52
- the_zip.append([None] * len(edits_audio) )
55
+ the_zip.append([None] * len(edits_audio))
53
56
 
54
57
  if len(edits_audio) == len(source_files):
55
58
  the_zip.append(source_files)
56
59
  elif len(source_files) == 1:
57
- the_zip.append( source_files * len(edits_audio) )
60
+ the_zip.append(source_files * len(edits_audio))
58
61
  else:
59
- the_zip.append([None] * len(edits_audio) )
60
-
62
+ the_zip.append([None] * len(edits_audio))
63
+
61
64
  # attach trans name to last event
62
65
  try:
63
66
  trans_statement = self._trans_name_statements()[0]
@@ -65,23 +68,28 @@ class Event:
65
68
  trans_names.append(trans_statement)
66
69
  the_zip.append(trans_names)
67
70
  except IndexError:
68
- the_zip.append([None] * len(edits_audio) )
69
-
70
- return [ Edit(edit_statement=e1[0],
71
- audio_ext_statement=e1[1],
72
- clip_name_statement=n1,
73
- source_file_statement=s1,
74
- trans_name_statement=u1) for (e1,n1,s1,u1) in zip(*the_zip) ]
75
-
71
+ the_zip.append([None] * len(edits_audio))
72
+
73
+ return [Edit(edit_statement=e1[0],
74
+ audio_ext_statement=e1[1],
75
+ clip_name_statement=n1,
76
+ source_file_statement=s1,
77
+ trans_name_statement=u1,
78
+ asc_sop_statement=self._asc_sop_statement(),
79
+ asc_sat_statement=self._asc_sat_statement(),
80
+ frmc_statement=self._frmc_statement())
81
+ for (e1, n1, s1, u1) in zip(*the_zip)]
82
+
76
83
  @property
77
- def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
84
+ def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
85
+ None]:
78
86
  """
79
87
  A generator for all the unrecognized statements in the event.
80
88
  """
81
89
  for s in self.statements:
82
90
  if type(s) is StmtUnrecognized:
83
91
  yield s
84
-
92
+
85
93
  def _trans_name_statements(self) -> List[StmtEffectsName]:
86
94
  return [s for s in self.statements if type(s) is StmtEffectsName]
87
95
 
@@ -90,15 +98,25 @@ class Event:
90
98
 
91
99
  def _clip_name_statements(self) -> List[StmtClipName]:
92
100
  return [s for s in self.statements if type(s) is StmtClipName]
93
-
101
+
94
102
  def _source_file_statements(self) -> List[StmtSourceFile]:
95
103
  return [s for s in self.statements if type(s) is StmtSourceFile]
96
-
97
- def _statements_with_audio_ext(self) -> Generator[Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
104
+
105
+ def _statements_with_audio_ext(self) -> Generator[
106
+ Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
98
107
  for (s1, s2) in zip(self.statements, self.statements[1:]):
99
108
  if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
100
- yield (s1,s2)
109
+ yield (s1, s2)
101
110
  elif type(s1) is StmtEvent:
102
111
  yield (s1, None)
103
112
 
104
-
113
+ def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
114
+ return next((s for s in self.statements if type(s) is StmtCdlSop),
115
+ None)
116
+
117
+ def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
118
+ return next((s for s in self.statements if type(s) is StmtCdlSat),
119
+ None)
120
+
121
+ def _frmc_statement(self) -> Optional[StmtFrmc]:
122
+ return next((s for s in self.statements if type(s) is StmtFrmc), None)
pycmx/parse_cmx_events.py CHANGED
@@ -1,20 +1,18 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
- # from collections import namedtuple
4
+ from typing import TextIO
5
5
 
6
6
  from .parse_cmx_statements import (parse_cmx3600_statements)
7
7
  from .edit_list import EditList
8
8
 
9
- from typing import TextIO
10
9
 
11
- def parse_cmx3600(f: TextIO):
10
+ def parse_cmx3600(f: TextIO) -> EditList:
12
11
  """
13
12
  Parse a CMX 3600 EDL.
14
13
 
15
- :param TextIO f: a file-like object, anything that's readlines-able.
14
+ :param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
16
15
  :returns: An :class:`pycmx.edit_list.EditList`.
17
16
  """
18
17
  statements = parse_cmx3600_statements(f)
19
18
  return EditList(statements)
20
-
@@ -1,169 +1,223 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
4
  import re
5
- import sys
6
- from collections import namedtuple
7
- from itertools import count
8
5
  from typing import TextIO, List
9
6
 
7
+ from .cdl import AscSopComponents, Rgb
10
8
 
9
+ from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
10
+ StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
11
+ StmtAudioExt, StmtClipName, StmtEffectsName,
12
+ StmtEvent, StmtSourceFile, StmtSplitEdit)
11
13
  from .util import collimate
12
14
 
13
- StmtTitle = namedtuple("Title",["title","line_number"])
14
- StmtFCM = namedtuple("FCM",["drop","line_number"])
15
- StmtEvent = namedtuple("Event",["event","source","channels","trans",\
16
- "trans_op","source_in","source_out","record_in","record_out","format","line_number"])
17
- StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
18
- StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
19
- StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
20
- StmtRemark = namedtuple("Remark",["text","line_number"])
21
- StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
22
- StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
23
- StmtSplitEdit = namedtuple("SplitEdit",["video","magnitude", "line_number"])
24
- StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
25
- StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
26
-
27
15
 
28
16
  def parse_cmx3600_statements(file: TextIO) -> List[object]:
29
17
  """
30
18
  Return a list of every statement in the file argument.
31
19
  """
32
20
  lines = file.readlines()
33
- line_numbers = count()
34
- return [_parse_cmx3600_line(line.strip(), line_number) \
35
- for (line, line_number) in zip(lines,line_numbers)]
36
-
37
- def _edl_column_widths(event_field_length, source_field_length):
38
- return [event_field_length,2, source_field_length,1,
39
- 4,2, # chans
40
- 4,1, # trans
41
- 3,1, # trans op
42
- 11,1,
43
- 11,1,
44
- 11,1,
45
- 11]
46
-
47
- def _edl_m2_column_widths():
48
- return [2, # "M2"
49
- 3,3, #
50
- 8,8,1,4,2,1,4,13,3,1,1]
51
-
52
-
53
- def _parse_cmx3600_line(line, line_number):
54
- long_event_num_p = re.compile("^[0-9]{6} ")
55
- short_event_num_p = re.compile("^[0-9]{3} ")
56
-
57
- if isinstance(line,str):
58
- if line.startswith("TITLE:"):
59
- return _parse_title(line,line_number)
60
- elif line.startswith("FCM:"):
61
- return _parse_fcm(line, line_number)
62
- elif long_event_num_p.match(line) != None:
63
- length_file_128 = sum(_edl_column_widths(6,128))
64
- if len(line) < length_file_128:
65
- return _parse_long_standard_form(line, 32, line_number)
66
- else:
67
- return _parse_long_standard_form(line, 128, line_number)
68
- elif short_event_num_p.match(line) != None:
69
- return _parse_standard_form(line, line_number)
70
- elif line.startswith("AUD"):
71
- return _parse_extended_audio_channels(line,line_number)
72
- elif line.startswith("*"):
73
- return _parse_remark( line[1:].strip(), line_number)
74
- elif line.startswith(">>> SOURCE"):
75
- return _parse_source_umid_statement(line, line_number)
76
- elif line.startswith("EFFECTS NAME IS"):
77
- return _parse_effects_name(line, line_number)
78
- elif line.startswith("SPLIT:"):
79
- return _parse_split(line, line_number)
80
- elif line.startswith("M2"):
81
- return _parse_motion_memory(line, line_number)
82
- else:
83
- return _parse_unrecognized(line, line_number)
21
+ return [_parse_cmx3600_line(line.strip(), line_number)
22
+ for (line_number, line) in enumerate(lines)]
23
+
24
+
25
+ def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
26
+ return [event_field_length, 2, source_field_length, 1,
27
+ 4, 2, # chans
28
+ 4, 1, # trans
29
+ 3, 1, # trans op
30
+ 11, 1,
31
+ 11, 1,
32
+ 11, 1,
33
+ 11]
34
+
35
+ # def _edl_m2_column_widths():
36
+ # return [2, # "M2"
37
+ # 3,3, #
38
+ # 8,8,1,4,2,1,4,13,3,1,1]
84
39
 
85
-
86
- def _parse_title(line, line_num):
40
+
41
+ def _parse_cmx3600_line(line: str, line_number: int) -> object:
42
+ """
43
+ Parses a single CMX EDL line.
44
+
45
+ :param line: A single EDL line.
46
+ :param line_number: The index of this line in the file.
47
+ """
48
+ event_num_p = re.compile(r"^(\d+) ")
49
+ line_matcher = event_num_p.match(line)
50
+
51
+ if line.startswith("TITLE:"):
52
+ return _parse_title(line, line_number)
53
+ if line.startswith("FCM:"):
54
+ return _parse_fcm(line, line_number)
55
+ if line_matcher is not None:
56
+ event_field_len = len(line_matcher.group(1))
57
+ source_field_len = len(line) - (event_field_len + 65)
58
+ return _parse_columns_for_standard_form(line, event_field_len,
59
+ source_field_len, line_number)
60
+ if line.startswith("AUD"):
61
+ return _parse_extended_audio_channels(line, line_number)
62
+ if line.startswith("*"):
63
+ return _parse_remark(line[1:].strip(), line_number)
64
+ if line.startswith(">>> SOURCE"):
65
+ return _parse_source_umid_statement(line, line_number)
66
+ if line.startswith("EFFECTS NAME IS"):
67
+ return _parse_effects_name(line, line_number)
68
+ if line.startswith("SPLIT:"):
69
+ return _parse_split(line, line_number)
70
+ if line.startswith("M2"):
71
+ pass
72
+ # return _parse_motion_memory(line, line_number)
73
+
74
+ return _parse_unrecognized(line, line_number)
75
+
76
+
77
+ def _parse_title(line, line_num) -> StmtTitle:
87
78
  title = line[6:].strip()
88
- return StmtTitle(title=title,line_number=line_num)
79
+ return StmtTitle(title=title, line_number=line_num)
80
+
89
81
 
90
- def _parse_fcm(line, line_num):
82
+ def _parse_fcm(line, line_num) -> StmtFCM:
91
83
  val = line[4:].strip()
92
84
  if val == "DROP FRAME":
93
- return StmtFCM(drop= True, line_number=line_num)
94
- else:
95
- return StmtFCM(drop= False, line_number=line_num)
96
-
97
- def _parse_long_standard_form(line,source_field_length, line_number):
98
- return _parse_columns_for_standard_form(line, 6, source_field_length, line_number)
99
-
100
- def _parse_standard_form(line, line_number):
101
- return _parse_columns_for_standard_form(line, 3, 8, line_number)
102
-
85
+ return StmtFCM(drop=True, line_number=line_num)
86
+
87
+ return StmtFCM(drop=False, line_number=line_num)
88
+
89
+
103
90
  def _parse_extended_audio_channels(line, line_number):
104
91
  content = line.strip()
105
- if content == "AUD 3":
106
- return StmtAudioExt(audio3=True, audio4=False, line_number=line_number)
107
- elif content == "AUD 4":
108
- return StmtAudioExt(audio3=False, audio4=True, line_number=line_number)
109
- elif content == "AUD 3 4":
110
- return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
92
+ audio3 = "3" in content
93
+ audio4 = "4" in content
94
+
95
+ if audio3 or audio4:
96
+ return StmtAudioExt(audio3, audio4, line_number)
111
97
  else:
112
- return StmtUnrecognized(content=line, line_number=line_number)
113
-
98
+ return StmtUnrecognized(line, line_number)
99
+
100
+
114
101
  def _parse_remark(line, line_number) -> object:
115
102
  if line.startswith("FROM CLIP NAME:"):
116
- return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
103
+ return StmtClipName(name=line[15:].strip(), affect="from",
104
+ line_number=line_number)
117
105
  elif line.startswith("TO CLIP NAME:"):
118
- return StmtClipName(name=line[13:].strip(), affect="to", line_number=line_number)
106
+ return StmtClipName(name=line[13:].strip(), affect="to",
107
+ line_number=line_number)
119
108
  elif line.startswith("SOURCE FILE:"):
120
- return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
109
+ return StmtSourceFile(filename=line[12:].strip(),
110
+ line_number=line_number)
111
+ elif line.startswith("ASC_SOP"):
112
+ group_patterns: list[str] = re.findall(r'\((.*?)\)', line)
113
+
114
+ v1: list[list[tuple[str, str]]] = \
115
+ [re.findall(r'(-?\d+(\.\d+)?)', a) for a in group_patterns]
116
+
117
+ v: list[list[str]] = [[a[0] for a in b] for b in v1]
118
+
119
+ if len(v) != 3 or any([len(a) != 3 for a in v]):
120
+ return StmtRemark(line, line_number)
121
+
122
+ else:
123
+ try:
124
+ return StmtCdlSop(line=line,
125
+ cdl_sop=AscSopComponents(
126
+ slope=Rgb(red=float(v[0][0]),
127
+ green=float(v[0][1]),
128
+ blue=float(v[0][2])),
129
+ offset=Rgb(red=float(v[1][0]),
130
+ green=float(v[1][1]),
131
+ blue=float(v[1][2])),
132
+ power=Rgb(red=float(v[2][0]),
133
+ green=float(v[2][1]),
134
+ blue=float(v[2][2]))
135
+ ),
136
+ line_number=line_number)
137
+
138
+ except ValueError as e:
139
+ return StmtCorruptRemark('ASC_SOP', e, line_number)
140
+
141
+ elif line.startswith("ASC_SAT"):
142
+ value = re.findall(r'(-?\d+(\.\d+)?)', line)
143
+
144
+ if len(value) != 1:
145
+ return StmtRemark(line, line_number)
146
+
147
+ else:
148
+ try:
149
+ return StmtCdlSat(value=float(value[0][0]),
150
+ line_number=line_number)
151
+
152
+ except ValueError as e:
153
+ return StmtCorruptRemark('ASC_SAT', e, line_number)
154
+
155
+ elif line.startswith("FRMC"):
156
+ match = re.match(r'^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)'
157
+ r'\s+FRMC DURATION:\s*(\d+)', line, re.IGNORECASE)
158
+
159
+ if match is None:
160
+ return StmtCorruptRemark('FRMC', None, line_number)
161
+
162
+ else:
163
+ try:
164
+ return StmtFrmc(start=int(match.group(1)),
165
+ end=int(match.group(2)),
166
+ duration=int(match.group(3)),
167
+ line_number=line_number)
168
+ except ValueError as e:
169
+ return StmtCorruptRemark('FRMC', e, line_number)
170
+
121
171
  else:
122
172
  return StmtRemark(text=line, line_number=line_number)
123
173
 
174
+
124
175
  def _parse_effects_name(line, line_number) -> StmtEffectsName:
125
176
  name = line[16:].strip()
126
177
  return StmtEffectsName(name=name, line_number=line_number)
127
178
 
128
- def _parse_split(line, line_number):
129
- split_type = line[10:21]
130
- is_video = False
131
- if split_type.startswith("VIDEO"):
132
- is_video = True
133
179
 
134
- split_mag = line[24:35]
135
- return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number)
180
+ def _parse_split(line: str, line_number):
181
+ split_type = line[10:21]
182
+ is_video = split_type.startswith("VIDEO")
136
183
 
184
+ split_delay = line[24:35]
185
+ return StmtSplitEdit(video=is_video, delay=split_delay,
186
+ line_number=line_number)
137
187
 
138
- def _parse_motion_memory(line, line_number):
139
- return StmtMotionMemory(source = "", fps="")
140
188
 
189
+ # def _parse_motion_memory(line, line_number):
190
+ # return StmtMotionMemory(source="", fps="")
191
+ #
141
192
 
142
193
  def _parse_unrecognized(line, line_number):
143
194
  return StmtUnrecognized(content=line, line_number=line_number)
144
195
 
145
- def _parse_columns_for_standard_form(line, event_field_length, source_field_length, line_number):
196
+
197
+ def _parse_columns_for_standard_form(line: str, event_field_length: int,
198
+ source_field_length: int,
199
+ line_number: int):
146
200
  col_widths = _edl_column_widths(event_field_length, source_field_length)
147
-
201
+
148
202
  if sum(col_widths) > len(line):
149
203
  return StmtUnrecognized(content=line, line_number=line_number)
150
-
151
- column_strings = collimate(line,col_widths)
152
-
153
- return StmtEvent(event=column_strings[0],
154
- source=column_strings[2].strip(),
155
- channels=column_strings[4].strip(),
156
- trans=column_strings[6].strip(),
204
+
205
+ column_strings = collimate(line, col_widths)
206
+
207
+ return StmtEvent(event=column_strings[0],
208
+ source=column_strings[2].strip(),
209
+ channels=column_strings[4].strip(),
210
+ trans=column_strings[6].strip(),
157
211
  trans_op=column_strings[8].strip(),
158
212
  source_in=column_strings[10].strip(),
159
213
  source_out=column_strings[12].strip(),
160
214
  record_in=column_strings[14].strip(),
161
215
  record_out=column_strings[16].strip(),
162
- line_number=line_number,
163
- format=source_field_length)
216
+ line_number=line_number,
217
+ source_field_size=source_field_length)
164
218
 
165
219
 
166
220
  def _parse_source_umid_statement(line, line_number):
167
- trimmed = line[3:].strip()
168
- return StmtSourceUMID(name=None, umid=None, line_number=line_number)
169
-
221
+ # trimmed = line[3:].strip()
222
+ # return StmtSourceUMID(name=None, umid=None, line_number=line_number)
223
+ ...
pycmx/statements.py ADDED
@@ -0,0 +1,104 @@
1
+ # pycmx
2
+ # (c) 2025 Jamie Hardt
3
+
4
+ from typing import Any, NamedTuple
5
+
6
+ from .cdl import AscSopComponents
7
+
8
+ # type str = str
9
+
10
+
11
+ class StmtTitle(NamedTuple):
12
+ title: str
13
+ line_number: int
14
+
15
+
16
+ class StmtFCM(NamedTuple):
17
+ drop: bool
18
+ line_number: int
19
+
20
+
21
+ class StmtEvent(NamedTuple):
22
+ event: int
23
+ source: str
24
+ channels: str
25
+ trans: str
26
+ trans_op: str
27
+ source_in: str
28
+ source_out: str
29
+ record_in: str
30
+ record_out: str
31
+ source_field_size: int
32
+ line_number: int
33
+
34
+
35
+ class StmtAudioExt(NamedTuple):
36
+ audio3: bool
37
+ audio4: bool
38
+ line_number: int
39
+
40
+
41
+ class StmtClipName(NamedTuple):
42
+ name: str
43
+ affect: str
44
+ line_number: int
45
+
46
+
47
+ class StmtSourceFile(NamedTuple):
48
+ filename: str
49
+ line_number: int
50
+
51
+
52
+ class StmtCdlSop(NamedTuple):
53
+ line: str
54
+ cdl_sop: AscSopComponents[float]
55
+ line_number: int
56
+
57
+
58
+ class StmtCdlSat(NamedTuple):
59
+ value: float
60
+ line_number: int
61
+
62
+
63
+ class StmtFrmc(NamedTuple):
64
+ start: int
65
+ end: int
66
+ duration: int
67
+ line_number: int
68
+
69
+
70
+ class StmtRemark(NamedTuple):
71
+ text: str
72
+ line_number: int
73
+
74
+
75
+ class StmtEffectsName(NamedTuple):
76
+ name: str
77
+ line_number: int
78
+
79
+
80
+ class StmtSourceUMID(NamedTuple):
81
+ name: str
82
+ umid: str
83
+ line_number: int
84
+
85
+
86
+ class StmtSplitEdit(NamedTuple):
87
+ video: bool
88
+ delay: str
89
+ line_number: int
90
+
91
+
92
+ class StmtUnrecognized(NamedTuple):
93
+ content: str
94
+ line_number: int
95
+
96
+
97
+ class StmtCorruptRemark(NamedTuple):
98
+ selector: str
99
+ exception: Any
100
+ line_number: int
101
+
102
+
103
+ # StmtMotionMemory = namedtuple(
104
+ # "MotionMemory", ["source", "fps"]) # FIXME needs more fields
pycmx/transition.py CHANGED
@@ -3,11 +3,12 @@
3
3
 
4
4
  from typing import Optional
5
5
 
6
+
6
7
  class Transition:
7
8
  """
8
9
  A CMX transition: a wipe, dissolve or cut.
9
10
  """
10
-
11
+
11
12
  Cut = "C"
12
13
  Dissolve = "D"
13
14
  Wipe = "W"
@@ -41,7 +42,7 @@ class Transition:
41
42
  @property
42
43
  def cut(self) -> bool:
43
44
  "`True` if this transition is a cut."
44
- return self.transition == 'C'
45
+ return self.transition == 'C'
45
46
 
46
47
  @property
47
48
  def dissolve(self) -> bool:
@@ -56,7 +57,7 @@ class Transition:
56
57
  @property
57
58
  def effect_duration(self) -> int:
58
59
  """The duration of this transition, in frames of the record target.
59
-
60
+
60
61
  In the event of a key event, this is the duration of the fade in.
61
62
  """
62
63
  return int(self.operand)
@@ -86,5 +87,3 @@ class Transition:
86
87
  the key foreground and replaced with the key background.
87
88
  """
88
89
  return self.transition == Transition.KeyOut
89
-
90
-
pycmx/util.py CHANGED
@@ -1,31 +1,30 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
4
  # Utility functions
5
5
 
6
- def collimate(a_string, column_widths):
6
+ def collimate(a_string, column_widths):
7
7
  """
8
- Split a list-type thing, like a string, into slices that are column_widths
8
+ Split a list-type thing, like a string, into slices that are column_widths
9
9
  length.
10
-
10
+
11
11
  >>> collimate("a b1 c2345",[2,3,3,2])
12
12
  ['a ','b1 ','c23','45']
13
13
 
14
14
  Args:
15
15
  a_string: The string to split. This parameter can actually be anything
16
16
  sliceable.
17
- column_widths: A list of integers, each one is the length of a column.
17
+ column_widths: A list of integers, each one is the length of a column.
18
18
 
19
19
  Returns:
20
- A list of slices. The len() of the returned list will *always* equal
21
- len(:column_widths:).
20
+ A list of slices. The len() of the returned list will *always* equal
21
+ len(:column_widths:).
22
22
  """
23
-
23
+
24
24
  if len(column_widths) == 0:
25
25
  return []
26
-
26
+
27
27
  width = column_widths[0]
28
28
  element = a_string[:width]
29
29
  rest = a_string[width:]
30
- return [element] + collimate(rest, column_widths[1:])
31
-
30
+ return [element] + collimate(rest, column_widths[1:])
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pycmx
3
- Version: 1.2.3
3
+ Version: 1.4.0
4
4
  Summary: Python CMX 3600 Edit Decision List Parser
5
- Home-page: https://github.com/iluvcapra/pycmx
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Keywords: parser,film,broadcast
8
8
  Author: Jamie Hardt
9
9
  Author-email: jamiehardt@me.com
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Topic :: Multimedia
21
22
  Classifier: Topic :: Multimedia :: Video
22
23
  Classifier: Topic :: Text Processing
@@ -24,6 +25,7 @@ Provides-Extra: doc
24
25
  Requires-Dist: sphinx (>=5.3.0) ; extra == "doc"
25
26
  Requires-Dist: sphinx_rtd_theme (>=1.1.1) ; extra == "doc"
26
27
  Project-URL: Documentation, https://pycmx.readthedocs.io/
28
+ Project-URL: Homepage, https://github.com/iluvcapra/pycmx
27
29
  Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
28
30
  Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
29
31
  Description-Content-Type: text/markdown
@@ -35,15 +37,20 @@ Description-Content-Type: text/markdown
35
37
 
36
38
  # pycmx
37
39
 
38
- The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and its most most common variations.
40
+ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
39
41
 
40
42
  ## Features
41
43
 
42
- * The major variations of the CMX 3600: the standard, "File32" and "File128"
43
- formats are automatically detected and properly read.
44
+ * The major variations of the CMX 3600: the standard, "File32", "File128" and
45
+ long Adobe Premiere event numbers are automatically detected and properly
46
+ read. Event number field and source name field sizes are determined
47
+ dynamically for each statement for a high level of compliance at the expense
48
+ of strictness.
44
49
  * Preserves relationship between events and individual edits/clips.
45
50
  * Remark or comment fields with common recognized forms are read and
46
51
  available to the client, including clip name and source file data.
52
+ * [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
53
+ decoded.
47
54
  * Symbolically decodes transitions and audio channels.
48
55
  * Does not parse or validate timecodes, does not enforce framerates, does not
49
56
  parameterize timecode or framerates in any way. This makes the parser more
@@ -53,6 +60,8 @@ The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and it
53
60
  list and give the client the ability to extend the package with their own
54
61
  parsing code.
55
62
 
63
+ [asc]: https://en.wikipedia.org/wiki/ASC_CDL
64
+
56
65
  ## Usage
57
66
 
58
67
  ### Opening and Parsing EDL Files
@@ -114,5 +123,3 @@ Audio channel 7 is present
114
123
  False
115
124
  ```
116
125
 
117
-
118
-
@@ -0,0 +1,15 @@
1
+ pycmx/__init__.py,sha256=u_VU3f3WltGoZzg_iZBLQZUtFqGFVZgBX3trqM7v3D4,365
2
+ pycmx/cdl.py,sha256=zONbFbvyETNs4u4mi7V-vePhO5VmQ_UE7wthbX0kigo,1041
3
+ pycmx/channel_map.py,sha256=_PIzTudMebzUfMWKNTA3i1egvmp4EsXqLPamCzjIUjk,3174
4
+ pycmx/edit.py,sha256=KmGbAd-wDbxFLmZKYgl0WW-qpYZbkyXeC5ViFiMvk8k,6446
5
+ pycmx/edit_list.py,sha256=sqmFkPG53k4-gQ9RMuSo2UjaR3K7o3akSM3sClTBWwc,3286
6
+ pycmx/event.py,sha256=cJ0XOiJ1upX_Wbo9Vo48FwVwSQWIQ0WSjn05Sp1mkDg,4524
7
+ pycmx/parse_cmx_events.py,sha256=TdM1Gjwi6PD3Bm0aFNDevPlSzZP2aP-uRsCjFmjDAwM,444
8
+ pycmx/parse_cmx_statements.py,sha256=wbheiju57CxfzYsYslykXzLMH-sP_WTMOzzmYZhpd18,7996
9
+ pycmx/statements.py,sha256=J6QRBQzIZ4q6so5-D3-t_2IsahMW6i3xCTPh7WqldgA,1598
10
+ pycmx/transition.py,sha256=IlVz2X3Z7CttJccqzzjhPPUNY2Pe8ZmVMIBnCrVLIG0,2364
11
+ pycmx/util.py,sha256=K1FVj2qptBtX-5RgBivc0RZl3JGdxAQ8x8WNhlBhY2Q,787
12
+ pycmx-1.4.0.dist-info/METADATA,sha256=YF0tm4MxDKn96CTFANOuglsgy_crhmWlHEG0pF7-4v4,4360
13
+ pycmx-1.4.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
14
+ pycmx-1.4.0.dist-info/licenses/LICENSE,sha256=JS087ZFloGaV7LHygeUaXmT-fGLx0VF30W62wDTLRCc,1056
15
+ pycmx-1.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,13 +0,0 @@
1
- pycmx/__init__.py,sha256=yS4sYKFfw3ScPaCr0_g0qSmhhZRxOLkgwR5kQ9nxYnc,388
2
- pycmx/channel_map.py,sha256=M7P3b0S2cBrmQjfSP5sixFxGv_uBjvie0f9PVdy9cMI,3248
3
- pycmx/edit.py,sha256=sipPmT2W0hPoYyRyZzwohohjO1fqwEU_mxsQheZxWC0,3775
4
- pycmx/edit_list.py,sha256=zZaOTEPgubHxetQF01BhMf6d097jXjvohYrMXtbP1a4,3172
5
- pycmx/event.py,sha256=-wlOdOwOVVk_4Wus0D8PVxRiEPBv1XZrRiQwkoRIOY4,3781
6
- pycmx/parse_cmx_events.py,sha256=_KeowR6jm4LlFWtejgZ0SL5jX-sMu8GKgx7ferUuRbE,472
7
- pycmx/parse_cmx_statements.py,sha256=UqS-p1kAFR8cp1pgiiBZLlyIhF-8hOWykYIS6-JuEBg,6755
8
- pycmx/transition.py,sha256=fAbWMqBZmZIcpZUobTlYuTd4WxercHMik_zqp7yK4mA,2378
9
- pycmx/util.py,sha256=alnJaV9vqH-KGtExrWjBiYMTiTILDXnF8Ju0sXWa69A,801
10
- pycmx-1.2.3.dist-info/LICENSE,sha256=JS087ZFloGaV7LHygeUaXmT-fGLx0VF30W62wDTLRCc,1056
11
- pycmx-1.2.3.dist-info/METADATA,sha256=64OBJd-tmpJl9dsgHarsEbLAOMa4mqHZ6PFTaZsTYQI,3999
12
- pycmx-1.2.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
13
- pycmx-1.2.3.dist-info/RECORD,,