pycmx 1.3.0__tar.gz → 1.5.0__tar.gz

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.
@@ -1,31 +1,34 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pycmx
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Python CMX 3600 Edit Decision List Parser
5
- Home-page: https://github.com/iluvcapra/pycmx
6
- License: MIT
7
5
  Keywords: parser,film,broadcast
8
6
  Author: Jamie Hardt
9
- Author-email: jamiehardt@me.com
10
- Requires-Python: >=3.8,<4.0
7
+ Author-email: Jamie Hardt <jamiehardt@me.com>
8
+ License-File: LICENSE
11
9
  Classifier: Development Status :: 5 - Production/Stable
12
10
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Multimedia
12
+ Classifier: Topic :: Multimedia :: Video
13
+ Classifier: Topic :: Text Processing
14
14
  Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  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: Topic :: Multimedia
21
- Classifier: Topic :: Multimedia :: Video
22
- Classifier: Topic :: Text Processing
23
- Provides-Extra: doc
24
- Requires-Dist: sphinx (>=5.3.0) ; extra == "doc"
25
- Requires-Dist: sphinx_rtd_theme (>=1.1.1) ; extra == "doc"
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Dist: pytest ; extra == 'dev'
22
+ Requires-Dist: ruff>=0.14.10 ; extra == 'dev'
23
+ Requires-Dist: sphinx>=5.3.0 ; extra == 'doc'
24
+ Requires-Dist: sphinx-rtd-theme>=1.1.1 ; extra == 'doc'
25
+ Requires-Python: >3.8
26
+ Project-URL: Homepage, https://github.com/iluvcapra/pycmx
26
27
  Project-URL: Documentation, https://pycmx.readthedocs.io/
27
28
  Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
28
29
  Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
30
+ Provides-Extra: dev
31
+ Provides-Extra: doc
29
32
  Description-Content-Type: text/markdown
30
33
 
31
34
  [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg)
@@ -35,17 +38,22 @@ Description-Content-Type: text/markdown
35
38
 
36
39
  # pycmx
37
40
 
38
- The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and
39
- its most most common variations.
41
+ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
40
42
 
41
43
  ## Features
42
44
 
43
- * The major variations of the CMX 3600: the standard, "File32", "File128" and
45
+ * The major variations of the CMX 3600: the standard, "File32", "File128" and
44
46
  long Adobe Premiere event numbers are automatically detected and properly
45
- read.
47
+ read. Event number field and source name field sizes are determined
48
+ dynamically for each statement for a high level of compliance at the expense
49
+ of strictness.
50
+ * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
51
+ use non-standard widths.
46
52
  * Preserves relationship between events and individual edits/clips.
47
53
  * Remark or comment fields with common recognized forms are read and
48
54
  available to the client, including clip name and source file data.
55
+ * [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
56
+ decoded.
49
57
  * Symbolically decodes transitions and audio channels.
50
58
  * Does not parse or validate timecodes, does not enforce framerates, does not
51
59
  parameterize timecode or framerates in any way. This makes the parser more
@@ -55,12 +63,14 @@ its most most common variations.
55
63
  list and give the client the ability to extend the package with their own
56
64
  parsing code.
57
65
 
66
+ [asc]: https://en.wikipedia.org/wiki/ASC_CDL
67
+
58
68
  ## Usage
59
69
 
60
70
  ### Opening and Parsing EDL Files
61
71
  ```
62
72
  >>> import pycmx
63
- >>> with open("tests/edls/TEST.edl") as f
73
+ >>> with open("tests/edls/TEST.edl") as f:
64
74
  ... edl = pycmx.parse_cmx3600(f)
65
75
  ...
66
76
  >>> edl.title
@@ -115,4 +125,3 @@ Audio channel 7 is present
115
125
  >>> events[2].edits[0].channels.video
116
126
  False
117
127
  ```
118
-
@@ -5,17 +5,22 @@
5
5
 
6
6
  # pycmx
7
7
 
8
- The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and
9
- its most most common variations.
8
+ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
10
9
 
11
10
  ## Features
12
11
 
13
- * The major variations of the CMX 3600: the standard, "File32", "File128" and
12
+ * The major variations of the CMX 3600: the standard, "File32", "File128" and
14
13
  long Adobe Premiere event numbers are automatically detected and properly
15
- read.
14
+ read. Event number field and source name field sizes are determined
15
+ dynamically for each statement for a high level of compliance at the expense
16
+ of strictness.
17
+ * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
18
+ use non-standard widths.
16
19
  * Preserves relationship between events and individual edits/clips.
17
20
  * Remark or comment fields with common recognized forms are read and
18
21
  available to the client, including clip name and source file data.
22
+ * [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
23
+ decoded.
19
24
  * Symbolically decodes transitions and audio channels.
20
25
  * Does not parse or validate timecodes, does not enforce framerates, does not
21
26
  parameterize timecode or framerates in any way. This makes the parser more
@@ -25,12 +30,14 @@ its most most common variations.
25
30
  list and give the client the ability to extend the package with their own
26
31
  parsing code.
27
32
 
33
+ [asc]: https://en.wikipedia.org/wiki/ASC_CDL
34
+
28
35
  ## Usage
29
36
 
30
37
  ### Opening and Parsing EDL Files
31
38
  ```
32
39
  >>> import pycmx
33
- >>> with open("tests/edls/TEST.edl") as f
40
+ >>> with open("tests/edls/TEST.edl") as f:
34
41
  ... edl = pycmx.parse_cmx3600(f)
35
42
  ...
36
43
  >>> edl.title
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "pycmx"
3
+ version = "1.5.0"
4
+ description = "Python CMX 3600 Edit Decision List Parser"
5
+ authors = [{name = "Jamie Hardt", email= "jamiehardt@me.com"}]
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ keywords = [
9
+ 'parser',
10
+ 'film',
11
+ 'broadcast'
12
+ ]
13
+ requires-python = '>3.8'
14
+ classifiers = [
15
+ 'Development Status :: 5 - Production/Stable',
16
+ 'License :: OSI Approved :: MIT License',
17
+ 'Topic :: Multimedia',
18
+ 'Topic :: Multimedia :: Video',
19
+ 'Topic :: Text Processing',
20
+ 'Programming Language :: Python :: 3.8',
21
+ 'Programming Language :: Python :: 3.9',
22
+ 'Programming Language :: Python :: 3.10',
23
+ 'Programming Language :: Python :: 3.11',
24
+ 'Programming Language :: Python :: 3.12',
25
+ 'Programming Language :: Python :: 3.13',
26
+ 'Programming Language :: Python :: 3.14'
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ doc = [
31
+ 'sphinx >= 5.3.0',
32
+ 'sphinx_rtd_theme >= 1.1.1',
33
+ ]
34
+ dev = [
35
+ 'pytest',
36
+ 'ruff>=0.14.10'
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/iluvcapra/pycmx"
41
+ Documentation = "https://pycmx.readthedocs.io/"
42
+ Repository = "https://github.com/iluvcapra/pycmx.git"
43
+ Tracker = "https://github.com/iluvcapra/pycmx/issues"
44
+
45
+ [dependency-groups]
46
+ dev = ['ruff', 'pytest']
47
+ doc = ['sphinx', 'sphinx_rtd_theme']
48
+
49
+
50
+ [tool.pyright]
51
+ typeCheckingMode = "basic"
52
+
53
+ [tool.ruff]
54
+ line-length = 88
55
+ indent-width = 4
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "W"]
59
+
60
+ [tool.ruff.format]
61
+ docstring-code-line-length = 88
62
+
63
+ # [tool.pylint]
64
+ # max-line-length = 88
65
+ # disable = [
66
+ # "C0103", # (invalid-name)
67
+ # "C0114", # (missing-module-docstring)
68
+ # "C0115", # (missing-class-docstring)
69
+ # "C0116", # (missing-function-docstring)
70
+ # "R0903", # (too-few-public-methods)
71
+ # "R0913", # (too-many-arguments)
72
+ # "W0105", # (pointless-string-statement)
73
+ # ]
74
+ #
75
+
76
+
77
+
78
+ [build-system]
79
+ requires = ["uv_build>=0.9.18,<0.10.0"]
80
+ build-backend = "uv_build"
@@ -11,3 +11,5 @@ from .parse_cmx_events import parse_cmx3600
11
11
  from .transition import Transition
12
12
  from .event import Event
13
13
  from .edit import Edit
14
+
15
+ __all__ = ("parse_cmx3600", "Transition", "Event", "Edit")
@@ -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
@@ -1,5 +1,5 @@
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
@@ -0,0 +1,213 @@
1
+ # pycmx
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
+ )
15
+ from .transition import Transition
16
+ from .channel_map import ChannelMap
17
+
18
+ from typing import Optional
19
+
20
+
21
+ class Edit:
22
+ """
23
+ An individual source-to-record operation, with a source roll, source and
24
+ recorder timecode in and out, a transition and channels.
25
+ """
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
+ self._edit_statement: StmtEvent = edit_statement
39
+ self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
40
+ self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
41
+ self._source_file_statement: Optional[StmtSourceFile] = \
42
+ source_file_statement
43
+ self._trans_name_statement: Optional[StmtEffectsName] = \
44
+ trans_name_statement
45
+ self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
46
+ self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
47
+ self._frmc_statement: Optional[StmtFrmc] = frmc_statement
48
+
49
+ @property
50
+ def line_number(self) -> int:
51
+ """
52
+ Get the line number for the "standard form" statement associated with
53
+ this edit. Line numbers a zero-indexed, such that the "TITLE:" record
54
+ is line zero.
55
+ """
56
+ return self._edit_statement.line_number
57
+
58
+ @property
59
+ def channels(self) -> ChannelMap:
60
+ """
61
+ Get the :obj:`ChannelMap` object associated with this Edit.
62
+ """
63
+ cm = ChannelMap()
64
+ cm._append_event(self._edit_statement.channels)
65
+ if self._audio_ext is not None:
66
+ cm._append_ext(self._audio_ext)
67
+ return cm
68
+
69
+ @property
70
+ def transition(self) -> Transition:
71
+ """
72
+ Get the :obj:`Transition` that initiates this edit.
73
+ """
74
+ if self._trans_name_statement:
75
+ return Transition(
76
+ self._edit_statement.trans,
77
+ self._edit_statement.trans_op,
78
+ self._trans_name_statement.name,
79
+ )
80
+ else:
81
+ return Transition(
82
+ self._edit_statement.trans, self._edit_statement.trans_op, None
83
+ )
84
+
85
+ @property
86
+ def source_in(self) -> str:
87
+ """
88
+ Get the source in timecode.
89
+ """
90
+ return self._edit_statement.source_in
91
+
92
+ @property
93
+ def source_out(self) -> str:
94
+ """
95
+ Get the source out timecode.
96
+ """
97
+
98
+ return self._edit_statement.source_out
99
+
100
+ @property
101
+ def record_in(self) -> str:
102
+ """
103
+ Get the record in timecode.
104
+ """
105
+
106
+ return self._edit_statement.record_in
107
+
108
+ @property
109
+ def record_out(self) -> str:
110
+ """
111
+ Get the record out timecode.
112
+ """
113
+
114
+ return self._edit_statement.record_out
115
+
116
+ @property
117
+ def source(self) -> str:
118
+ """
119
+ Get the source column. This is the 8, 32 or 128-character string on the
120
+ event record line, this usually references the tape name of the source.
121
+ """
122
+ return self._edit_statement.source
123
+
124
+ @property
125
+ def black(self) -> bool:
126
+ """
127
+ The source field for thie edit was "BL". Black video or silence should
128
+ be used as the source for this event.
129
+ """
130
+ return self.source == "BL"
131
+
132
+ @property
133
+ def aux_source(self) -> bool:
134
+ """
135
+ The source field for this edit was "AX". An auxiliary source is the
136
+ source for this event.
137
+ """
138
+ return self.source == "AX"
139
+
140
+ @property
141
+ def source_file(self) -> Optional[str]:
142
+ """
143
+ Get the source file, as attested by a "* SOURCE FILE" remark on the
144
+ EDL. This will return None if the information is not present.
145
+ """
146
+ if self._source_file_statement is None:
147
+ return None
148
+ else:
149
+ return self._source_file_statement.filename
150
+
151
+ @property
152
+ def clip_name(self) -> Optional[str]:
153
+ """
154
+ Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
155
+ NAME" remark on the EDL. This will return None if the information is
156
+ not present.
157
+ """
158
+ if self._clip_name_statement is None:
159
+ return None
160
+ else:
161
+ return self._clip_name_statement.name
162
+
163
+ @property
164
+ def asc_sop(self) -> Optional[AscSopComponents[float]]:
165
+ """
166
+ Get ASC CDL Slope-Offset-Power color transfer function for the edit,
167
+ if present. The ASC SOP is a transfer function of the form:
168
+
169
+ :math:`y = (ax + b)^p`
170
+
171
+ for each color component the source, where the `slope` is `a`, `offset`
172
+ is `b` and `power` is `p`.
173
+ """
174
+ if self._asc_sop_statement is None:
175
+ return None
176
+
177
+ return self._asc_sop_statement.cdl_sop
178
+
179
+ @property
180
+ def asc_sop_raw(self) -> Optional[str]:
181
+ """
182
+ ASC CDL Slope-Offset-Power statement raw line.
183
+ """
184
+ if self._asc_sop_statement is None:
185
+ return None
186
+
187
+ return self._asc_sop_statement.line
188
+
189
+ @property
190
+ def asc_sat(self) -> Optional[float]:
191
+ """
192
+ Get ASC CDL saturation value for clip, if present.
193
+ """
194
+ if self._asc_sat_statement is None:
195
+ return None
196
+
197
+ return self._asc_sat_statement.value
198
+
199
+ @property
200
+ def framecounts(self) -> Optional[FramecountTriple]:
201
+ """
202
+ Get frame count offset data, if it exists. If an FRMC statement exists
203
+ in the EDL for the event it will give an integer frame count for the
204
+ edit's source in and out times.
205
+ """
206
+ if not self._frmc_statement:
207
+ return None
208
+
209
+ return FramecountTriple(
210
+ start=self._frmc_statement.start,
211
+ end=self._frmc_statement.end,
212
+ duration=self._frmc_statement.duration,
213
+ )
@@ -1,22 +1,22 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2018-2025 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (
5
- StmtUnrecognized, StmtEvent, StmtSourceUMID)
4
+ from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
5
+ StmtUnrecognized, StmtSourceUMID)
6
6
  from .event import Event
7
7
  from .channel_map import ChannelMap
8
8
 
9
- from typing import Generator
9
+ from typing import Any, Generator
10
10
 
11
11
 
12
12
  class EditList:
13
13
  """
14
14
  Represents an entire edit decision list as returned by
15
- :func:`~pycmx.parse_cmx3600()`.
15
+ :func:`~pycmx.parse_cmx_events.parse_cmx3600()`.
16
16
  """
17
17
 
18
- def __init__(self, statements):
19
- self.title_statement = statements[0]
18
+ def __init__(self, statements: list):
19
+ self.title_statement: StmtTitle = statements[0]
20
20
  self.event_statements = statements[1:]
21
21
 
22
22
  @property
@@ -31,11 +31,11 @@ class EditList:
31
31
  (s for s in self.event_statements if type(s) is StmtEvent), None)
32
32
 
33
33
  if first_event:
34
- if first_event.format == 8:
34
+ if first_event.source_field_size == 8:
35
35
  return '3600'
36
- elif first_event.format == 32:
36
+ elif first_event.source_field_size == 32:
37
37
  return 'File32'
38
- elif first_event.format == 128:
38
+ elif first_event.source_field_size == 128:
39
39
  return 'File128'
40
40
  else:
41
41
  return 'unknown'
@@ -63,13 +63,16 @@ class EditList:
63
63
  return self.title_statement.title
64
64
 
65
65
  @property
66
- def unrecognized_statements(self) -> Generator[StmtUnrecognized,
67
- None, None]:
66
+ def unrecognized_statements(self) -> Generator[Any, None, None]:
68
67
  """
69
- 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`
70
73
  """
71
74
  for s in self.event_statements:
72
- if type(s) is StmtUnrecognized:
75
+ if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
73
76
  yield s
74
77
 
75
78
  @property
@@ -90,8 +93,6 @@ class EditList:
90
93
  else:
91
94
  event_statements.append(stmt)
92
95
 
93
- elif type(stmt) is StmtSourceUMID:
94
- break
95
96
  else:
96
97
  event_statements.append(stmt)
97
98
 
@@ -1,9 +1,9 @@
1
1
  # pycmx
2
- # (c) 2023 Jamie Hardt
2
+ # (c) 2023-2025 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (
5
- StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized,
6
- StmtEffectsName)
4
+ from .statements import (StmtFrmc, StmtEvent, StmtClipName, StmtSourceFile,
5
+ StmtAudioExt, StmtUnrecognized, StmtEffectsName,
6
+ StmtCdlSop, StmtCdlSat)
7
7
  from .edit import Edit
8
8
 
9
9
  from typing import List, Generator, Optional, Tuple, Any
@@ -31,12 +31,27 @@ class Event:
31
31
  will have multiple edits when a dissolve, wipe or key transition needs
32
32
  to be performed.
33
33
  """
34
+
35
+ # FTR this is a totall bonkers way of doing this, I wrote this when
36
+ # I was still learning Python and I'm sure there's easier ways to do
37
+ # it. The job is complicated because multiple edits can occur in one
38
+ # event and then other statements can modify the event in different
39
+ # ways.
40
+
34
41
  edits_audio = list(self._statements_with_audio_ext())
35
42
  clip_names = self._clip_name_statements()
36
43
  source_files = self._source_file_statements()
37
44
 
45
+ # We first get the edit events combined with their extra audio
46
+ # channel statements, if any.
47
+
48
+ # The list the_zip contains one element for each initialization
49
+ # parameter in Edit()
38
50
  the_zip: List[List[Any]] = [edits_audio]
39
51
 
52
+ # If there are two Clip Name statements and two edits, we look for
53
+ # "FROM" and "TO" clip name lines. Otherwise we just look for on
54
+ # each per edit.
40
55
  if len(edits_audio) == 2:
41
56
  start_name: Optional[StmtClipName] = None
42
57
  end_name: Optional[StmtClipName] = None
@@ -54,6 +69,10 @@ class Event:
54
69
  else:
55
70
  the_zip.append([None] * len(edits_audio))
56
71
 
72
+ # if there's one source file statemnent per clip, we allocate them to
73
+ # each edit in order. Otherwise if there's only one, we assign the one
74
+ # to all the edits. If there's no source_file statements, we provide
75
+ # None.
57
76
  if len(edits_audio) == len(source_files):
58
77
  the_zip.append(source_files)
59
78
  elif len(source_files) == 1:
@@ -61,7 +80,7 @@ class Event:
61
80
  else:
62
81
  the_zip.append([None] * len(edits_audio))
63
82
 
64
- # attach trans name to last event
83
+ # attach effects name to last event
65
84
  try:
66
85
  trans_statement = self._trans_name_statements()[0]
67
86
  trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
@@ -74,7 +93,10 @@ class Event:
74
93
  audio_ext_statement=e1[1],
75
94
  clip_name_statement=n1,
76
95
  source_file_statement=s1,
77
- trans_name_statement=u1)
96
+ trans_name_statement=u1,
97
+ asc_sop_statement=self._asc_sop_statement(),
98
+ asc_sat_statement=self._asc_sat_statement(),
99
+ frmc_statement=self._frmc_statement())
78
100
  for (e1, n1, s1, u1) in zip(*the_zip)]
79
101
 
80
102
  @property
@@ -101,8 +123,24 @@ class Event:
101
123
 
102
124
  def _statements_with_audio_ext(self) -> Generator[
103
125
  Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
104
- for (s1, s2) in zip(self.statements, self.statements[1:]):
105
- if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
106
- yield (s1, s2)
107
- elif type(s1) is StmtEvent:
108
- yield (s1, None)
126
+
127
+ if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
128
+ yield (self.statements[0], None)
129
+
130
+ else:
131
+ for (s1, s2) in zip(self.statements, self.statements[1:]):
132
+ if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
133
+ yield (s1, s2)
134
+ elif type(s1) is StmtEvent:
135
+ yield (s1, None)
136
+
137
+ def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
138
+ return next((s for s in self.statements if type(s) is StmtCdlSop),
139
+ None)
140
+
141
+ def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
142
+ return next((s for s in self.statements if type(s) is StmtCdlSat),
143
+ None)
144
+
145
+ def _frmc_statement(self) -> Optional[StmtFrmc]:
146
+ return next((s for s in self.statements if type(s) is StmtFrmc), None)