|
1 | 1 | 🏠 Architecture
|
2 | 2 | ================
|
3 | 3 |
|
4 |
| - The content below assumes you have fairly good knowledge of the following: |
| 4 | +.. toctree:: |
5 | 5 |
|
6 |
| - - OOP and descriptors, especially |
7 |
| - - Type annotations |
8 |
| - - Binary data types and streams |
9 |
| - |
10 |
| -.. svgbob:: |
11 |
| - :align: center |
12 |
| - |
13 |
| - ┌──────────────────────────────────────────────────────────────────────────┐ |
14 |
| - │ Events - binary representation - low level API - Stage 1 parser │ |
15 |
| - │ │ |
16 |
| - │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────┐ │ |
17 |
| - │ │ Project-wide / 1-time │ │ Per-instance │ │ Shared │ │ |
18 |
| - │ │┌─────────┐ ┌─────────┐│ │┌─────────┐ ┌─────────┐│ │ ┌─────────┐ │ │ |
19 |
| - │ ││ Event 1 │ │ Event 2 ││ ││ Event 3 │ │ Event 4 ││ │ │ Event 5 │ │ │ |
20 |
| - │ ││ id: 199 │ → │ id: 159 ││→││ id: 64 │ → │ id: 215 ││→│ │ id: 225 │ │ │ |
21 |
| - │ ││ string │ │ integer ││ ││ integer │ │ struct ││ │ │ AoS │ │ │ |
22 |
| - │ │└─────────┘ └─────────┘│ │└─────────┘ └─────────┘│ │ └─────────┘ │ │ |
23 |
| - │ └─────│──────────────│────┘ └─────────────────────────┘ └─────────────┘ │ |
24 |
| - │ │ ╭───────╯ ╭────────╯ │ │ |
25 |
| - │ ┌───────────────┐ ┌───────┬──────────┬──────────────┐ ┌────────────────┐ │ |
26 |
| - │ │ Model A │ │ Model │ Model B1 │ attr_64: int │ │ Model C1: e[0] │ │ |
27 |
| - │ │ attr_199: str │ │ list ├──────────┼──────────────┤ ├────────────────┤ │ |
28 |
| - │ │ attr_159: int │ │ of B │ Model B2 │ attr_215: X │ │ Model C2: e[1] │ │ |
29 |
| - │ └───────────────┘ └───────┴──────────┴──────────────┘ └────────────────┘ │ |
30 |
| - │ │ |
31 |
| - │ Models - PyFLP's representation - high level API - Stage 2 parser │ |
32 |
| - └──────────────────────────────────────────────────────────────────────────┘ |
33 |
| - |
34 |
| -PyFLP provides a high-level and a low-level API. Normally the high-level API |
35 |
| -should get your work done. However, it might be possible that due to a bug or |
36 |
| -super old versions of FLPs the high level API fails to parse. In that case, |
37 |
| -one can use the low-level API. Using it requires a deeper understanding of |
38 |
| -the FLP format and how the GUI hierarchies relate to their underlying events. |
39 |
| - |
40 |
| -.. caution:: |
41 |
| - |
42 |
| - Using the high-level and low-level API simultaneously can cause a loss of |
43 |
| - synchronisation across the state, although it normally shouldn't this |
44 |
| - use-case should be considered untested. |
45 |
| - |
46 |
| -⬇ The sections below are ordered from low-level to high-level concepts. |
47 |
| - |
48 |
| -.. _architecture-event: |
49 |
| - |
50 |
| -Understanding events |
51 |
| --------------------- |
52 |
| - |
53 |
| -.. automodule:: pyflp._events |
54 |
| - :show-inheritance: |
55 |
| - |
56 |
| -The FLP format uses a :wikipedia:`Type-length-value` encoding to store almost |
57 |
| -all of it's data. *It's an incredibly bad format, full of bad design decisions |
58 |
| -AFAIK.* |
59 |
| - |
60 |
| -That being said, all the data except: |
61 |
| - |
62 |
| -- PPQ - :attr:`pyflp.project.Project.ppq` |
63 |
| -- Number of channels - :attr:`pyflp.project.Project.channel_count` |
64 |
| -- Internal file format - :attr:`pyflp.project.Project.format` |
65 |
| - |
66 |
| -is stored in a structure called an **Event**. |
67 |
| - |
68 |
| -❔ What is an **Event**? |
69 |
| -^^^^^^^^^^^^^^^^^^^^^^^^ |
70 |
| - |
71 |
| -.. note:: C terminology |
72 |
| - |
73 |
| - I use some C terminology below like ``struct`` and its data types. |
74 |
| - I recommend you to get acquainted with these topics, however as a |
75 |
| - contributor, I am sure you have an equivalent programming background. |
76 |
| - |
77 |
| -Following can be considered as a pseudo C-style structure of an event: |
78 |
| - |
79 |
| -.. code-block:: c |
80 |
| -
|
81 |
| - typedef struct { |
82 |
| - uint8_t id; |
83 |
| - void* data; |
84 |
| - } event; |
85 |
| -
|
86 |
| -It means that every event begins with an ID (known as the event ID) followed by |
87 |
| -its data. The size of this ``data`` is fixed or variable sized depending on |
88 |
| -``id``. |
89 |
| - |
90 |
| -This table shows how the size of ``data`` is decided: |
91 |
| - |
92 |
| -+----------+------------------------------+-------------------------------+ |
93 |
| -| Event ID | Size of ``data`` (in bytes) | Total event size (in bytes) | |
94 |
| -+==========+==============================+===============================+ |
95 |
| -| 0-63 | 1 | 1 + 1 = **2** | |
96 |
| -+----------+------------------------------+-------------------------------+ |
97 |
| -| 64-127 | 2 | 1 + 2 = **3** | |
98 |
| -+----------+------------------------------+-------------------------------+ |
99 |
| -| 128-191 | 4 | 1 + 4 = **5** | |
100 |
| -+----------+------------------------------+-------------------------------+ |
101 |
| -| 192-255 | ``varint`` | 1 + ``encoded`` + ``decoded`` | |
102 |
| -+----------+------------------------------+-------------------------------+ |
103 |
| - |
104 |
| -Events are the first stage of parsing in PyFLP. The :meth:`pyflp.parse` method |
105 |
| -gathers all events by reading an FLP file as a binary stream. |
106 |
| - |
107 |
| -Representation |
108 |
| -^^^^^^^^^^^^^^ |
109 |
| - |
110 |
| -An event ID is represented in an ``EventEnum`` subclass. |
111 |
| - |
112 |
| -.. autoclass:: _EventEnumMeta |
113 |
| -.. autoclass:: EventEnum |
114 |
| - |
115 |
| -These enums are documented throughout the :doc:`reference`. |
116 |
| - |
117 |
| -For each of the range above, I have created a number of classes to match the |
118 |
| -exact type of ``data`` indicated by its usage. What I mean by this statement |
119 |
| -is, multiple types with different value ranges exist for a single ID range. |
120 |
| - |
121 |
| - For example, |
122 |
| - |
123 |
| - - 4 bytes can represent :wikipedia:`Single-precision_floating-point_format` |
124 |
| - or an :wikipedia:`Integer_(computer_science)` or even a tuple of two |
125 |
| - 2-byte integers. |
126 |
| - - 1 byte can represent a number from -128 to 127 or a number from 0 to 255, |
127 |
| - a boolean or event an :wikipedia:`ASCII` character. |
128 |
| - |
129 |
| - *.. and so on* |
130 |
| - |
131 |
| -.. autoclass:: EventBase |
132 |
| - :private-members: |
133 |
| - :special-members: |
134 |
| - |
135 |
| -Below are the list of classes PyFLP has, grouped according the ID range. |
136 |
| - |
137 |
| -.. dropdown:: 0-63 |
138 |
| - |
139 |
| - .. autoclass:: ByteEventBase |
140 |
| - .. autoclass:: U8Event |
141 |
| - .. autoclass:: BoolEvent |
142 |
| - .. autoclass:: I8Event |
143 |
| - |
144 |
| -.. dropdown:: 64-127 |
145 |
| - |
146 |
| - .. autoclass:: WordEventBase |
147 |
| - .. autoclass:: U16Event |
148 |
| - .. autoclass:: I16Event |
149 |
| - |
150 |
| -.. dropdown:: 128-191 |
151 |
| - |
152 |
| - .. autoclass:: DWordEventBase |
153 |
| - .. autoclass:: U32Event |
154 |
| - .. autoclass:: I32Event |
155 |
| - .. autoclass:: ColorEvent |
156 |
| - .. autoclass:: U16TupleEvent |
157 |
| - |
158 |
| -.. dropdown:: 192-255 |
159 |
| - |
160 |
| - .. autoclass:: VarintEventBase |
161 |
| - .. autoclass:: StrEventBase |
162 |
| - .. autoclass:: AsciiEvent |
163 |
| - .. autoclass:: UnicodeEvent |
164 |
| - .. autoclass:: DataEventBase |
165 |
| - .. autoclass:: UnknownDataEvent |
166 |
| - .. autoclass:: StructEventBase |
167 |
| - .. autoclass:: ListEventBase |
168 |
| - |
169 |
| -Parsing |
170 |
| -^^^^^^^ |
171 |
| - |
172 |
| -Let's understand two terms first: |
173 |
| - |
174 |
| -- Fixed size events: Events with an ``id`` between 0 to 191, basically those |
175 |
| - whose size is only decided by the ``id``. |
176 |
| -- Variable size events: Events with an ``id`` between 192 to 255. The ``data``, |
177 |
| - its size and the existance of these events itself is decided by a number of |
178 |
| - factors, including but not limited to the FL Studio version used to save the |
179 |
| - project file in which these events are saved |
180 |
| - |
181 |
| -Fixed size events are pretty much easy to understand, just by looking at the |
182 |
| -code, so they won't be covered in much depth. They exist for simple things. |
183 |
| - |
184 |
| -Variable size events store their size encoded in a "varint", followed by the |
185 |
| -actual data whose size is equal to the contents of the decoded "varint". This |
186 |
| -is used for strings and custom structures. |
187 |
| - |
188 |
| -Custom structures are very similar to a collection of events collected in a |
189 |
| -single C-style ``struct``. Why so? Event IDs are stored in a single byte, |
190 |
| -which means a maximum of 256 IDs can be used in addition to the constraints |
191 |
| -applied by the ID range itself. |
192 |
| - |
193 |
| - Image-Line, as shortsighted 🔭 it was initially, didn't probably realise |
194 |
| - that they will run out of the available space of 255 events pretty soon. |
195 |
| - There however has an alternative 💡, which wouldn't cause a major breaking |
196 |
| - change to the format itself. |
197 |
| - |
198 |
| - Now I don't work for Image-Line, but they probably thought 🤔: |
199 |
| - |
200 |
| - We already use variable size events for strings. We can use them for |
201 |
| - saving this valuable event ID space as well ❕ |
202 |
| - |
203 |
| -Fast forward many versions later, FL still uses this weird mixture of fixed and |
204 |
| -variable size events to represent what I call a :ref:`model <architecture-model>`. |
205 |
| - |
206 |
| -.. todo:: |
207 |
| - |
208 |
| - Explain different types of "custom structures" (:class:`DataEventBase` |
209 |
| - subclasses). |
210 |
| - |
211 |
| -.. todo:: |
212 |
| - |
213 |
| - Explain :class:`EventTree` |
214 |
| - |
215 |
| -.. _architecture-model: |
216 |
| - |
217 |
| -📦 Understanding models |
218 |
| ------------------------- |
219 |
| - |
220 |
| -A **model** is an entity, or an object, programmatically speaking. |
221 |
| - |
222 |
| - Models are **my** estimations of object hierarchies which mainly mimic FL |
223 |
| - Studio's GUI hierarchy. I figured out that this is the easiest way to |
224 |
| - expose an API programmatically. |
225 |
| - |
226 |
| - The FLP format has no such notion of "models" as it is entirely based on |
227 |
| - the sequence of :doc:`events <./architecture>`. |
228 |
| - |
229 |
| - PyFLP's modules are categorized to follow FL Studio' GUI hierarchy as well. |
230 |
| - Every module *generally* represents a **separate window** in the GUI. |
231 |
| - |
232 |
| -In PyFLP, a model is **composed** of several :ref:`descriptors <architecture-descriptor>`, |
233 |
| -properties and some additional helper methods, optionally. It *might* contain |
234 |
| -additional parsing logic for nested models and collections of models. |
235 |
| - |
236 |
| -A model's internal state is stored in :ref:`events <architecture-event>` and its |
237 |
| -shared state is passed to it via keyword arguments. *For example*, many models |
238 |
| -depend on :attr:`pyflp.project.Project.version` to decide the parsing logic for |
239 |
| -certain properties. This creates a "dependancy" of the model to a "shared" |
240 |
| -property. Such "dependencies" are passed to the model in the form of keyword |
241 |
| -arguments and consumed by the :ref:`descriptors <architecture-descriptor>`. |
242 |
| - |
243 |
| -A model **does NOT cache** its state in any way. This is done, mainly to: |
244 |
| - |
245 |
| -1. Implement lazily evaluated properties and avoid use of private variables. |
246 |
| -2. Keep the property values in sync with the event data. |
247 |
| - |
248 |
| -Implementing a model |
249 |
| -^^^^^^^^^^^^^^^^^^^^ |
250 |
| - |
251 |
| -A look at the **source code** will definitely help, although these are a few |
252 |
| -points that must be kept in mind when Implementing a model: |
253 |
| - |
254 |
| -1. Does the model mimic the hierarchy exposed by FL Studio's GUI? |
255 |
| - |
256 |
| - .. tip:: |
257 |
| - |
258 |
| - Browse through the hierarchies of :class:`pyflp.channel.Channel` |
259 |
| - subclasses to get a very good idea of this. |
260 |
| - |
261 |
| -2. Are ``__dunder__`` methods provided by Python used whenever possible? |
262 |
| -3. Is either :class:`ModelReprMixin` subclassed or ``__repr__`` implemented? |
263 |
| - |
264 |
| -Reference |
265 |
| -^^^^^^^^^ |
266 |
| - |
267 |
| -.. automodule:: pyflp._models |
268 |
| - :show-inheritance: |
269 |
| - :members: |
270 |
| - |
271 |
| -.. _architecture-descriptor: |
272 |
| - |
273 |
| -\ :fas:`bars-staggered` Understanding descriptors |
274 |
| -------------------------------------------------- |
275 |
| - |
276 |
| -.. automodule:: pyflp._descriptors |
277 |
| - :show-inheritance: |
278 |
| - |
279 |
| -A "descriptor" provides low-level managed attribute access, according to |
280 |
| -Python docs. *(slightly rephrased for my convenience)*. |
281 |
| - |
282 |
| - IMO, it allows separation of attribute logic from the class implementation |
283 |
| - itself and this saves a huge amount of repretitive error-prone code. |
284 |
| - |
285 |
| - .. note:: More about descriptors in Python |
286 |
| - |
287 |
| - - `<https://docs.python.org/3/howto/descriptor.html>`_ |
288 |
| - - `<https://realpython.com/python-descriptors/>`_, **especially** the |
289 |
| - `Why use Python descriptors? |
290 |
| - <https://realpython.com/python-descriptors/#why-use-python-descriptors>`_ |
291 |
| - section. |
292 |
| - |
293 |
| -In PyFLP, descriptors are used for attributes of a :ref:`model <architecture-model>`. |
294 |
| -Internally, they access the value of an :ref:`event <architecture-event>` or |
295 |
| -one if its keys for :class:`StructEventBase`. They can be called *stateless* |
296 |
| -because they never cache the value which they fetch and directly dump back into |
297 |
| -the event when their setter is invoked. |
298 |
| - |
299 |
| -Some common descriptors like ``name`` 🔤, ``color`` 🎨 or ``icon`` 🖼 are used by |
300 |
| -multiple different types of models. The descriptors used for these can be |
301 |
| -different depending upon the internal representation inside :ref:`events <architecture-event>`. |
302 |
| - |
303 |
| -Despite all this, they are normal attributes from a type-checker's POV 👁 when |
304 |
| -accessed from an instance. |
305 |
| - |
306 |
| -.. note:: |
307 |
| - |
308 |
| - Throughout the documentation, I have used the term **descriptors** and |
309 |
| - **properties** interchangeably. |
310 |
| - |
311 |
| -Protocols |
312 |
| -^^^^^^^^^ |
313 |
| - |
314 |
| -🙄 Since the ``typing`` module doesn't provide any type for descriptors, I |
315 |
| -needed to create my own: |
316 |
| - |
317 |
| -.. autoprotocol:: ROProperty |
318 |
| -.. autoprotocol:: RWProperty |
319 |
| - |
320 |
| -Descriptors |
321 |
| -^^^^^^^^^^^ |
322 |
| - |
323 |
| -.. autoclass:: PropBase |
324 |
| -.. autoclass:: StructProp |
325 |
| -.. autoclass:: EventProp |
326 |
| -.. autoclass:: FlagProp |
327 |
| -.. autoclass:: NestedProp |
328 |
| -.. autoclass:: KWProp |
329 |
| - |
330 |
| -Helpers |
331 |
| -^^^^^^^ |
332 |
| - |
333 |
| -.. autoclass:: NamedPropMixin |
334 |
| - |
335 |
| -Adapters |
336 |
| -^^^^^^^^ |
337 |
| - |
338 |
| -Adapters used by :class:`construct.Struct` objects. |
339 |
| - |
340 |
| -.. autoclass:: LinearMusical |
341 |
| - :members: |
342 |
| -.. autoclass:: Log2 |
343 |
| - :members: |
344 |
| -.. autoclass:: LogNormal |
345 |
| - :members: |
346 |
| -.. autoclass:: StdEnum |
347 |
| - :members: |
348 |
| - |
349 |
| -Shared models |
350 |
| -^^^^^^^^^^^^^ |
351 |
| - |
352 |
| -.. autoclass:: MusicalTime |
353 |
| - :members: |
| 6 | + 1️⃣ FLP Format & Events <architecture/flp-format> |
| 7 | + 2️⃣ How it works? <architecture/how-it-works> |
| 8 | + 3️⃣ Developer Reference <architecture/reference> |
0 commit comments