Skip to content

Pattern

Pattern

Bases: _FLObject

Source code in pyflp/pattern/pattern.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class Pattern(_FLObject):
    NOTE_SIZE = 24

    @enum.unique
    class EventID(enum.IntEnum):
        """Events used by `Pattern`."""

        New = WORD + 1
        """Marks the beginning of a new pattern, **twice**."""

        # _Data = WORD + 4
        Color = DWORD + 22
        """See `Pattern.color`. Default event is not stored."""

        Name = TEXT + 1
        """See `Pattern.color`. Default event deos not exist."""

        # _157 = DWORD + 29   # FL 12.5+
        # _158 = DWORD + 30   # default: -1
        # _164 = DWORD + 36   # default: 0
        Controllers = DATA + 15
        """See `Pattern.controllers`."""

        Notes = DATA + 16
        """See `Pattern.notes`."""

    # * Properties
    name: Optional[str] = _StrProperty()

    color: Optional[colour.Color] = _ColorProperty()

    @property
    def index(self) -> int:
        """Pattern index. Begins from 1.

        Occurs twice, once near the top with note data and once with rest of
        the metadata. The first time a `PatternEvent.New` occurs, it is
        parsed by `parse_index1()`. Empty patterns are not stored at the top
        since they don't have any note data. They come later on.

        See `Parser.__parse_pattern()` for implemntation."""
        return getattr(self, "_index", None)

    @index.setter
    def index(self, value: int):
        self._events["index"].dump(value)
        self._index = value
        self._events["index (metadata)"].dump(value)

    @property
    def notes(self) -> List[PatternNote]:
        """Contains the list of MIDI events (notes) of all channels."""
        return getattr(self, "_notes", [])

    @property
    def controllers(self) -> List[PatternController]:
        """Pattern control events, size is equal to pattern length."""
        return getattr(self, "_controllers", [])

    # * Parsing logic
    def parse_index1(self, e: WordEventType):
        """Thanks to FL for storing data of a single pattern at 2 different places."""
        self._events["index (metadata)"] = e

    def _parse_word_event(self, e: WordEventType):
        if e.id_ == Pattern.EventID.New:
            self._parse_H(e, "index")

    def _parse_dword_event(self, e: DWordEventType):
        if e.id_ == Pattern.EventID.Color:
            self._parse_color(e, "color")

    def _parse_text_event(self, e: TextEventType):
        if e.id_ == Pattern.EventID.Name:
            self._parse_s(e, "name")

    def _parse_data_event(self, e: DataEventType):
        if e.id_ == Pattern.EventID.Notes:
            if TYPE_CHECKING:
                if not isinstance(e, PatternNotesEvent):
                    raise TypeError
            self._events["notes"] = e
            self._notes = e.notes

        elif e.id_ == Pattern.EventID.Controllers:
            if TYPE_CHECKING:
                if not isinstance(e, PatternControllersEvent):
                    raise TypeError
            self._events["controllers"] = e
            self._controllers = e.controllers

    def _save(self) -> List[EventType]:
        # Note events
        notes = self.notes
        notes_ev = self._events.get("notes")
        if notes and notes_ev:
            notes_data = io.BytesIO()
            for note in notes:
                notes_data.write(note._save())
            notes_data.seek(0)
            notes_ev.dump(notes_data.read())

        # Controller events
        ctrls = self.controllers
        ctrls_ev = self._events.get("controllers")
        if ctrls and ctrls_ev:
            ctrls_data = io.BytesIO()
            for ctrl in ctrls:
                ctrls_data.write(ctrl._save())
            ctrls_data.seek(0)
            ctrls_ev.dump(ctrls_data.read())

        return super()._save()

    # * Utility methods
    def is_empty(self) -> bool:
        """Whether pattern has note data."""
        return "notes" in self._events

    def __init__(self):
        super().__init__()
        self._notes: List[PatternNote] = []

color: Optional[colour.Color] = _ColorProperty() class-attribute

name: Optional[str] = _StrProperty() class-attribute

EventID

Bases: enum.IntEnum

Events used by Pattern.

Source code in pyflp/pattern/pattern.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@enum.unique
class EventID(enum.IntEnum):
    """Events used by `Pattern`."""

    New = WORD + 1
    """Marks the beginning of a new pattern, **twice**."""

    # _Data = WORD + 4
    Color = DWORD + 22
    """See `Pattern.color`. Default event is not stored."""

    Name = TEXT + 1
    """See `Pattern.color`. Default event deos not exist."""

    # _157 = DWORD + 29   # FL 12.5+
    # _158 = DWORD + 30   # default: -1
    # _164 = DWORD + 36   # default: 0
    Controllers = DATA + 15
    """See `Pattern.controllers`."""

    Notes = DATA + 16
    """See `Pattern.notes`."""

Color = DWORD + 22 class-attribute

See Pattern.color. Default event is not stored.

Controllers = DATA + 15 class-attribute

See Pattern.controllers.

Name = TEXT + 1 class-attribute

See Pattern.color. Default event deos not exist.

New = WORD + 1 class-attribute

Marks the beginning of a new pattern, twice.

Notes = DATA + 16 class-attribute

See Pattern.notes.

index() writable property

Pattern index. Begins from 1.

Occurs twice, once near the top with note data and once with rest of the metadata. The first time a PatternEvent.New occurs, it is parsed by parse_index1(). Empty patterns are not stored at the top since they don't have any note data. They come later on.

See Parser.__parse_pattern() for implemntation.

Source code in pyflp/pattern/pattern.py
67
68
69
70
71
72
73
74
75
76
77
@property
def index(self) -> int:
    """Pattern index. Begins from 1.

    Occurs twice, once near the top with note data and once with rest of
    the metadata. The first time a `PatternEvent.New` occurs, it is
    parsed by `parse_index1()`. Empty patterns are not stored at the top
    since they don't have any note data. They come later on.

    See `Parser.__parse_pattern()` for implemntation."""
    return getattr(self, "_index", None)

notes() property

Contains the list of MIDI events (notes) of all channels.

Source code in pyflp/pattern/pattern.py
85
86
87
88
@property
def notes(self) -> List[PatternNote]:
    """Contains the list of MIDI events (notes) of all channels."""
    return getattr(self, "_notes", [])

is_empty()

Whether pattern has note data.

Source code in pyflp/pattern/pattern.py
151
152
153
def is_empty(self) -> bool:
    """Whether pattern has note data."""
    return "notes" in self._events

PatternNote

Event information

Size: A divisible of 24.

Attention

This event wasn't always a divisible of 24. I found a FL 3.0.0 file where it is a divisible of 20.

Structure:

Parameter Offset Type
position 0 I
flags 4 H
rack_channel 6 H
duration 8 I
key 12 I
fine_pitch 16 b
unknown 17
release 18 B
midi_channel 19 B
pan 20 b
velocity 21 B
mod_x 22 B
mod_y 23 B

Bases: _FLObject

Represents a note (MIDI event) in a Pattern.

Source code in pyflp/pattern/note.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class PatternNote(_FLObject):
    """Represents a note (MIDI event) in a `Pattern`."""

    # * Properties
    position: int = _UIntProperty()
    """Position from pattern start (i.e. 0).
    Proportional to project PPQ. See `Misc.ppq`."""

    flags: int = _UIntProperty()
    """Miscellaneous note related flags. TODO"""

    rack_channel: int = _UIntProperty()
    """Which `Channel` this note is for; since, a single
    pattern can hold notes for multiple channels."""

    duration: int = _UIntProperty()
    """Duration. Proportional to project PPQ. See `Misc.ppq`."""

    key: int = _UIntProperty()
    """The note itself. Single notes: 0-131 (for C0-B10).
    Yet needs 4 bytes, to save stamped chords/scales."""

    fine_pitch: int = _IntProperty(min_=-128, max_=127)
    """Min: -128 (-100 cents), Max: 127 (+100 cents)."""

    u1: int = _IntProperty()
    """TODO: Unknown parameter."""

    release: int = _UIntProperty(max_=128)
    """Min: 0, Max: 128."""

    # See issue #8
    midi_channel: int = _UIntProperty()
    """For note colors, min: 0, max: 15.

    128 for MIDI dragged into the piano roll.
    """

    pan: int = _IntProperty(min_=-64, max_=64)
    """Min: -64, Max: 64."""

    velocity: int = _UIntProperty(max_=128)
    """Min: 0, Max: 128."""

    mod_x: int = _UIntProperty()
    """Mod X. Most probably filter cutoff."""

    mod_y: int = _UIntProperty()
    """Mod Y. Most probably filter resonance."""

    def _save(self) -> bytes:
        return struct.pack(
            "I2H2I2b2Bb3B",
            self.position,
            self.flags,
            self.rack_channel,
            self.duration,
            self.key,
            self.fine_pitch,
            self.u1,
            self.release,
            self.midi_channel,
            self.pan,
            self.velocity,
            self.mod_x,
            self.mod_y,
        )

duration: int = _UIntProperty() class-attribute

Duration. Proportional to project PPQ. See Misc.ppq.

fine_pitch: int = _IntProperty(min_=-128, max_=127) class-attribute

Min: -128 (-100 cents), Max: 127 (+100 cents).

flags: int = _UIntProperty() class-attribute

Miscellaneous note related flags. TODO

midi_channel: int = _UIntProperty() class-attribute

For note colors, min: 0, max: 15.

128 for MIDI dragged into the piano roll.

key: int = _UIntProperty() class-attribute

The note itself. Single notes: 0-131 (for C0-B10). Yet needs 4 bytes, to save stamped chords/scales.

mod_x: int = _UIntProperty() class-attribute

Mod X. Most probably filter cutoff.

mod_y: int = _UIntProperty() class-attribute

Mod Y. Most probably filter resonance.

pan: int = _IntProperty(min_=-64, max_=64) class-attribute

Min: -64, Max: 64.

position: int = _UIntProperty() class-attribute

Position from pattern start (i.e. 0). Proportional to project PPQ. See Misc.ppq.

rack_channel: int = _UIntProperty() class-attribute

Which Channel this note is for; since, a single pattern can hold notes for multiple channels.

release: int = _UIntProperty(max_=128) class-attribute

Min: 0, Max: 128.

velocity: int = _UIntProperty(max_=128) class-attribute

Min: 0, Max: 128.

PatternController

Event information

Size: A divisible of 12.

Structure:

Parameter Offset Type
position 0 I
unknown 4
unknown 5
target_channel 6 B
target_flags 7 B
value 8 f

Bases: _FLObject

Source code in pyflp/pattern/controller.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class PatternController(_FLObject):
    position = _UIntProperty()
    """Position relative to pattern start."""

    target_channel = _UIntProperty()
    """Target channel"""

    target_flags = _UIntProperty()
    """TODO"""

    value = _FloatProperty()
    """Either 0.0 to 1.0 for VST parameters or
    0 to 12800 for FL's internal parameters."""

    u1 = _UIntProperty()
    """TODO"""

    u2 = _UIntProperty()
    """TODO"""

    def __repr__(self) -> str:
        return "<PatternController {}, {}, {}, {}>".format(
            f"position={self.position}",
            f"target_channel={self.target_channel}",
            f"target_flags={self.target_flags}",
            f"value={self.value}",
        )

    def _save(self) -> bytes:
        return struct.pack(
            "I4Bf",
            self.position,
            self.u1,
            self.u2,
            self.target_channel,
            self.target_flags,
            self.value,
        )

position = _UIntProperty() class-attribute

Position relative to pattern start.

target_channel = _UIntProperty() class-attribute

Target channel

target_flags = _UIntProperty() class-attribute

TODO

value = _FloatProperty() class-attribute

Either 0.0 to 1.0 for VST parameters or 0 to 12800 for FL's internal parameters.