Skip to content

Commit 24b88a9

Browse files
committed
docs: split architecture documentation
1 parent ba88591 commit 24b88a9

File tree

10 files changed

+352
-373
lines changed

10 files changed

+352
-373
lines changed

docs/architecture.rst

Lines changed: 4 additions & 349 deletions
Original file line numberDiff line numberDiff line change
@@ -1,353 +1,8 @@
11
🏠 Architecture
22
================
33

4-
The content below assumes you have fairly good knowledge of the following:
4+
.. toctree::
55

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

Comments
 (0)