Skip to content

Parser

FL Studio project file parser.

Source code in pyflp/parser.py
 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
class Parser:
    """FL Studio project file parser."""

    def __init__(self):
        self.__events = EventList()
        self.__channel_count = 0
        self.__max_counts = MaxInstances()
        self.__fl_version: Optional[FLVersion] = None
        self.__uses_unicode = True

        # Timemarkers can occur anywhere before an arrangement
        # In that case, store them here temporarily until an
        # arrangement gets initialised.
        self.__tms: List[TimeMarker] = []

        # See `__parse_pattern` below for details.
        self.__pat_indexes: Set[int] = set()

        # If current plugin default name is "Fruity Wrapper", this is True.
        # `__build_event_store` will instantiate `VSTPluginEvent`.
        self.__cur_plug_is_vst = False

    def __build_event_store(self) -> None:
        """Gathers all events into a single list."""

        def add_dwordevent(id_: EventIDType, buf: bytes) -> None:
            if id_ in COLOR_EVENTS:
                typ = _ColorEvent
            else:
                typ = _DWordEvent
            self.__events.append(typ, id_, buf)

        def add_textevent(id_: EventIDType, buf: bytes) -> None:
            if id_ == Misc.EventID.Version:
                self.__fl_version = FLVersion(_TextEvent.as_ascii(buf))
                if self.__fl_version.as_float() < 11.5:
                    self.__uses_unicode = False
            ev = self.__events.create(_TextEvent, id_, buf, self.__uses_unicode)
            if (
                id_ in (InsertSlot.EventID.DefaultName, Channel.EventID.DefaultName)
                and ev.to_str() == "Fruity Wrapper"
            ):
                self.__cur_plug_is_vst = True
            self.__events.append_event(ev)

        def add_dataevent(id_: EventIDType, buf: bytes) -> None:
            if id_ == Track.EventID.Data:
                typ = TrackDataEvent
            elif id_ == Channel.EventID.Delay:
                typ = ChannelDelayEvent
            elif id_ == Channel.EventID.Polyphony:
                typ = ChannelPolyphonyEvent
            elif id_ == Channel.EventID.Levels:
                typ = ChannelLevelsEvent
            elif id_ == Channel.EventID.Tracking:
                typ = ChannelTrackingEvent
            elif id_ == Channel.EventID.LevelOffsets:
                typ = ChannelLevelOffsetsEvent
            elif id_ == Channel.EventID.Parameters:
                typ = ChannelParametersEvent
            elif id_ == Channel.EventID.EnvelopeLFO:
                typ = ChannelEnvelopeLFOEvent
            elif id_ in (Channel.EventID.Plugin, InsertSlot.EventID.Plugin):
                if self.__cur_plug_is_vst:
                    typ = VSTPluginEvent
                    self.__cur_plug_is_vst = False
                else:
                    typ = _DataEvent
            elif id_ == Insert.EventID.Parameters:
                typ = InsertParametersEvent
            elif id_ == Pattern.EventID.Controllers:
                typ = PatternControllersEvent
            elif id_ == Pattern.EventID.Notes:
                typ = PatternNotesEvent
            elif id_ == RemoteController.ID:
                typ = RemoteControllerEvent
            else:
                typ = _DataEvent
            self.__events.append(typ, id_, buf)

        while True:
            evid = self.__r.read_B()
            if evid is None:
                break

            if evid in range(BYTE, WORD):
                self.__events.append(_ByteEvent, evid, self.__r.read(1))
            elif evid in range(WORD, DWORD):
                self.__events.append(_WordEvent, evid, self.__r.read(2))
            elif evid in range(DWORD, TEXT):
                add_dwordevent(evid, self.__r.read(4))
            else:
                varint = self.__r.read_v()
                buf = self.__r.read(varint)
                if evid in range(TEXT, DATA) or evid in DATA_TEXT_EVENTS:
                    add_textevent(evid, buf)
                else:
                    add_dataevent(evid, buf)

    def __parse_channel(self, ev: EventType) -> None:
        """Creates and appends `Channel` objects to `Project`.
        Dispatches `ChannelEventID` events for parsing."""

        if ev.id_ == Channel.EventID.New:
            self.__channel_count += 1
            self.__cur_ch = Channel()
            self.__proj.channels.append(self.__cur_ch)
        self.__cur_ch.parse_event(ev)

    def __parse_pattern(self, ev: EventType) -> None:
        """Creates and appends `Pattern` objects to `Project`.
        Dispatches `PatternEventID` events to `Pattern` for parsing."""

        if ev.id_ == Pattern.EventID.New and isinstance(ev, _WordEvent):
            # Occurs twice, once with the note events only and later again
            # for metadata (name, color and a few undiscovered properties)
            # New patterns can occur for metadata as well; they are empty.
            index = ev.to_uint16()
            if index in self.__pat_indexes:
                for pattern in self.__proj.patterns:
                    if pattern.index == index:
                        self.__cur_pat = pattern
                self.__cur_pat.parse_index1(ev)
                return  # Don't let the event be parsed again!
            else:
                self.__pat_indexes.add(index)
                self.__cur_pat = Pattern()
                self.__proj.patterns.append(self.__cur_pat)
        self.__cur_pat.parse_event(ev)

    def __parse_insert(self, ev: EventType) -> None:
        """Creates and appends `Insert` objects to `Project`. Dispatches
        `InsertEvent` and `InsertSlotEventID` events for parsing."""

        self.__cur_ins.parse_event(ev)
        if (
            ev.id_ == Insert.EventID.Output
            and len(self.__proj.inserts) < self.__max_counts.inserts
        ):
            self.__proj.inserts.append(self.__cur_ins)
            self.__cur_ins = Insert(self.__proj, self.__max_counts)

    def __parse_arrangement(self, ev: EventType) -> None:
        """Creates and appends `Arrangement` objects to `Project`. Dispatches
        `ArrangementEventID`, `PlaylistEventID` and `TrackEventID` events
        for parsing."""

        if ev.id_ == Arrangement.EventID.New:
            self.__cur_arr = Arrangement(self.__proj)
            self.__proj.arrangements.append(self.__cur_arr)

        # I have found timemarkers occuring randomly (mixed with channel events)
        # before ArrangementEventID.Index in certains version of FL 20.0-20.1.
        # i.e the order before was TimeMarkers -> Playlist -> Tracks.
        # Now it is in the order: Playlist -> TimeMarkers -> Tracks.
        if ev.id_ == Track.EventID.Data and not self.__cur_arr.timemarkers:
            self.__cur_arr._timemarkers = self.__tms
            self.__tms = []
        self.__cur_arr.parse_event(ev)

    def __parse_filter(self, ev: EventType) -> None:
        """Creates and appends `Filter` objects to `Project`.
        Dispatches `FilterEventID` events for parsing."""

        if ev.id_ == Filter.EventID.Name:
            self.__cur_flt: Filter = Filter()
            self.__proj.filters.append(self.__cur_flt)
        self.__cur_flt.parse_event(ev)

    def __parse_timemarker(self, ev: EventType) -> None:
        if ev.id_ == TimeMarker.EventID.Position:
            self.__cur_tm = TimeMarker()
            self.__tms.append(self.__cur_tm)
        self.__cur_tm.parse_event(ev)

    def get_events(
        self, flp: Union[str, Path, bytes, io.BufferedIOBase]
    ) -> List[EventType]:
        """Just get the events; don't parse

        Why does this method exist?
        - FLP format has changed a lot over the years;
        nobody except IL can parse it properly, PyFLP needs a
        specific event structure.
        - In the event of failure, user can at least get the events.
        - [FLPInspect](https://github.com/demberto/flpinspect) and
        [FLPInfo](https://github.com/demberto/flpinfo) can still
        display some minimal information based on the events.
        """
        # * Argument validation
        self.__proj = proj = Project()
        if isinstance(flp, (Path, str)):
            if isinstance(flp, Path):
                self.__proj.save_path = flp
            else:
                self.__proj.save_path = Path(flp)
            self.__r = BytesIOEx(open(flp, "rb").read())
        elif isinstance(flp, io.BufferedIOBase):
            flp.seek(0)
            self.__r = BytesIOEx(flp.read())
        elif isinstance(flp, bytes):
            self.__r = BytesIOEx(flp)
        else:
            raise TypeError(
                f"Cannot parse a file of type {type(flp)}. \
                Only str, Path or bytes objects are supported."
            )

        r = self.__r
        hdr_magic = r.read(4)
        if hdr_magic != HEADER_MAGIC:
            raise InvalidMagicError(hdr_magic)
        hdr_size = r.read_I()
        if hdr_size != HEADER_SIZE:
            raise InvalidHeaderSizeError(hdr_size)
        proj.misc.format = Misc.Format(r.read_h())
        proj.misc.channel_count = r.read_H()
        proj.misc.ppq = r.read_H()
        data_magic = r.read(4)
        if data_magic != DATA_MAGIC:
            raise InvalidMagicError(data_magic)
        _ = r.read_I()  # Combined size of all events
        self.__build_event_store()

        return self.__events

    def parse(self, flp: Union[str, Path, bytes, io.BufferedIOBase]) -> Project:
        """Parses an FLP. Use `parse_zip` for ZIP looped packages instead."""

        # * Argument validation
        self.__proj.events = self.get_events(flp)

        # * Modify parsing logic as per FL version
        # TODO: This can be as less as 16. Also insert slots were once 8.
        self.__max_counts.inserts = 127 if self.__fl_version.as_float() > 12.89 else 104
        self.__cur_ins = Insert(self.__proj, self.__max_counts)

        # * Build an object model
        # TODO: Parse in multiple layers
        parse_channel = True
        for ev in self.__proj.events:
            if ev.id_ in Misc.EventID.__members__.values():
                self.__proj.misc.parse_event(ev)
            elif ev.id_ in Filter.EventID.__members__.values():
                self.__parse_filter(ev)
            elif ev.id_ == RemoteController.ID:
                controller = RemoteController()
                controller.parse_event(ev)
                self.__proj.controllers.append(controller)
            elif ev.id_ in Pattern.EventID.__members__.values():
                self.__parse_pattern(ev)
            elif ev.id_ in CHANNEL_EVENTS and parse_channel:
                self.__parse_channel(ev)
            elif ev.id_ in TimeMarker.EventID.__members__.values():
                self.__parse_timemarker(ev)
            elif ev.id_ in ARRANGEMENT_EVENTS:
                parse_channel = False
                self.__parse_arrangement(ev)
            elif ev.id_ in INSERT_EVENTS and not parse_channel:
                self.__parse_insert(ev)
            elif ev.id_ == InsertParamsEvent.ID:
                ev.id_ = InsertParamsEvent.ID

                # Append the last insert first
                self.__proj.inserts.append(self.__cur_ins)
                insert_params_ev = InsertParamsEvent(ev)
                if not insert_params_ev.parse(self.__proj.inserts):
                    self.__proj._unparsed_events.append(ev)
            else:
                self.__proj._unparsed_events.append(ev)

        # * Post-parse steps
        # Now move all playlist events to track, Playlist can be empty as well
        # Cannot parse playlist events in arrangement, because certain FL versions
        # dump only used tracks. This is not the case anymore.
        for arrangement in self.__proj.arrangements:
            if arrangement.playlist:
                for idx, track in enumerate(arrangement.tracks):
                    items = arrangement.playlist.items.get(idx)
                    if items:
                        track._items = items
                    arrangement.playlist._items = {}

        # Re-arrange patterns by index
        self.__proj.patterns.sort(key=lambda pat: pat.index - 1)

        return self.__proj

    def parse_zip(self, zip_file, name: str = "") -> Project:
        """Parses an FLP inside a ZIP.

        Args:
            zip_file (Union[zipfile.ZipFile, str, bytes, Path, io.BufferedIOBase]):
                The path to the ZIP file, stream, Path, file-like or a ZipFile
                object.
            name (str, optional): If the ZIP has multiple FLPs, you need
                to specify the name of the FLP to parse.

        Raises:
            TypeError: When `zip_file` points to a ZIP or stream containing no
                files with an extension of .flp.
            TypeError: When `name` is empty and `zip_file` points to a ZIP or
                stream containing more than one FLP.

        Returns:
            Project: The parsed object.
        """

        flp = None

        if isinstance(zip_file, (str, bytes, io.BufferedIOBase, Path)):
            zp = zipfile.ZipFile(zip_file, "r")
        else:
            zp = zip_file

        if name == "":
            # Find the file with .flp extension
            flps = []
            file_names = zp.namelist()
            for file_name in file_names:
                if file_name.endswith(".flp"):
                    flps.append(file_name)
            if not len(flps) == 1:  # pragma: no cover
                if not flps:
                    raise TypeError("No FLP files found inside ZIP.", zp)
                elif len(flps) > 1:
                    raise TypeError(
                        "Optional parameter 'name' cannot be empty "
                        "when more than one FLP exists in ZIP",
                        zp,
                    )
            else:
                name = flps[0]

        flp = zp.open(name, "r").read()
        return self.parse(flp)

__channel_count = 0 instance-attribute

__cur_plug_is_vst = False instance-attribute

__events = EventList() instance-attribute

__fl_version: Optional[FLVersion] = None instance-attribute

__max_counts = MaxInstances() instance-attribute

__pat_indexes: Set[int] = set() instance-attribute

__tms: List[TimeMarker] = [] instance-attribute

__uses_unicode = True instance-attribute

__build_event_store()

Gathers all events into a single list.

Source code in pyflp/parser.py
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def __build_event_store(self) -> None:
    """Gathers all events into a single list."""

    def add_dwordevent(id_: EventIDType, buf: bytes) -> None:
        if id_ in COLOR_EVENTS:
            typ = _ColorEvent
        else:
            typ = _DWordEvent
        self.__events.append(typ, id_, buf)

    def add_textevent(id_: EventIDType, buf: bytes) -> None:
        if id_ == Misc.EventID.Version:
            self.__fl_version = FLVersion(_TextEvent.as_ascii(buf))
            if self.__fl_version.as_float() < 11.5:
                self.__uses_unicode = False
        ev = self.__events.create(_TextEvent, id_, buf, self.__uses_unicode)
        if (
            id_ in (InsertSlot.EventID.DefaultName, Channel.EventID.DefaultName)
            and ev.to_str() == "Fruity Wrapper"
        ):
            self.__cur_plug_is_vst = True
        self.__events.append_event(ev)

    def add_dataevent(id_: EventIDType, buf: bytes) -> None:
        if id_ == Track.EventID.Data:
            typ = TrackDataEvent
        elif id_ == Channel.EventID.Delay:
            typ = ChannelDelayEvent
        elif id_ == Channel.EventID.Polyphony:
            typ = ChannelPolyphonyEvent
        elif id_ == Channel.EventID.Levels:
            typ = ChannelLevelsEvent
        elif id_ == Channel.EventID.Tracking:
            typ = ChannelTrackingEvent
        elif id_ == Channel.EventID.LevelOffsets:
            typ = ChannelLevelOffsetsEvent
        elif id_ == Channel.EventID.Parameters:
            typ = ChannelParametersEvent
        elif id_ == Channel.EventID.EnvelopeLFO:
            typ = ChannelEnvelopeLFOEvent
        elif id_ in (Channel.EventID.Plugin, InsertSlot.EventID.Plugin):
            if self.__cur_plug_is_vst:
                typ = VSTPluginEvent
                self.__cur_plug_is_vst = False
            else:
                typ = _DataEvent
        elif id_ == Insert.EventID.Parameters:
            typ = InsertParametersEvent
        elif id_ == Pattern.EventID.Controllers:
            typ = PatternControllersEvent
        elif id_ == Pattern.EventID.Notes:
            typ = PatternNotesEvent
        elif id_ == RemoteController.ID:
            typ = RemoteControllerEvent
        else:
            typ = _DataEvent
        self.__events.append(typ, id_, buf)

    while True:
        evid = self.__r.read_B()
        if evid is None:
            break

        if evid in range(BYTE, WORD):
            self.__events.append(_ByteEvent, evid, self.__r.read(1))
        elif evid in range(WORD, DWORD):
            self.__events.append(_WordEvent, evid, self.__r.read(2))
        elif evid in range(DWORD, TEXT):
            add_dwordevent(evid, self.__r.read(4))
        else:
            varint = self.__r.read_v()
            buf = self.__r.read(varint)
            if evid in range(TEXT, DATA) or evid in DATA_TEXT_EVENTS:
                add_textevent(evid, buf)
            else:
                add_dataevent(evid, buf)

__init__()

Source code in pyflp/parser.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def __init__(self):
    self.__events = EventList()
    self.__channel_count = 0
    self.__max_counts = MaxInstances()
    self.__fl_version: Optional[FLVersion] = None
    self.__uses_unicode = True

    # Timemarkers can occur anywhere before an arrangement
    # In that case, store them here temporarily until an
    # arrangement gets initialised.
    self.__tms: List[TimeMarker] = []

    # See `__parse_pattern` below for details.
    self.__pat_indexes: Set[int] = set()

    # If current plugin default name is "Fruity Wrapper", this is True.
    # `__build_event_store` will instantiate `VSTPluginEvent`.
    self.__cur_plug_is_vst = False

__parse_arrangement(ev)

Creates and appends Arrangement objects to Project. Dispatches ArrangementEventID, PlaylistEventID and TrackEventID events for parsing.

Source code in pyflp/parser.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def __parse_arrangement(self, ev: EventType) -> None:
    """Creates and appends `Arrangement` objects to `Project`. Dispatches
    `ArrangementEventID`, `PlaylistEventID` and `TrackEventID` events
    for parsing."""

    if ev.id_ == Arrangement.EventID.New:
        self.__cur_arr = Arrangement(self.__proj)
        self.__proj.arrangements.append(self.__cur_arr)

    # I have found timemarkers occuring randomly (mixed with channel events)
    # before ArrangementEventID.Index in certains version of FL 20.0-20.1.
    # i.e the order before was TimeMarkers -> Playlist -> Tracks.
    # Now it is in the order: Playlist -> TimeMarkers -> Tracks.
    if ev.id_ == Track.EventID.Data and not self.__cur_arr.timemarkers:
        self.__cur_arr._timemarkers = self.__tms
        self.__tms = []
    self.__cur_arr.parse_event(ev)

__parse_channel(ev)

Creates and appends Channel objects to Project. Dispatches ChannelEventID events for parsing.

Source code in pyflp/parser.py
194
195
196
197
198
199
200
201
202
def __parse_channel(self, ev: EventType) -> None:
    """Creates and appends `Channel` objects to `Project`.
    Dispatches `ChannelEventID` events for parsing."""

    if ev.id_ == Channel.EventID.New:
        self.__channel_count += 1
        self.__cur_ch = Channel()
        self.__proj.channels.append(self.__cur_ch)
    self.__cur_ch.parse_event(ev)

__parse_filter(ev)

Creates and appends Filter objects to Project. Dispatches FilterEventID events for parsing.

Source code in pyflp/parser.py
255
256
257
258
259
260
261
262
def __parse_filter(self, ev: EventType) -> None:
    """Creates and appends `Filter` objects to `Project`.
    Dispatches `FilterEventID` events for parsing."""

    if ev.id_ == Filter.EventID.Name:
        self.__cur_flt: Filter = Filter()
        self.__proj.filters.append(self.__cur_flt)
    self.__cur_flt.parse_event(ev)

__parse_insert(ev)

Creates and appends Insert objects to Project. Dispatches InsertEvent and InsertSlotEventID events for parsing.

Source code in pyflp/parser.py
225
226
227
228
229
230
231
232
233
234
235
def __parse_insert(self, ev: EventType) -> None:
    """Creates and appends `Insert` objects to `Project`. Dispatches
    `InsertEvent` and `InsertSlotEventID` events for parsing."""

    self.__cur_ins.parse_event(ev)
    if (
        ev.id_ == Insert.EventID.Output
        and len(self.__proj.inserts) < self.__max_counts.inserts
    ):
        self.__proj.inserts.append(self.__cur_ins)
        self.__cur_ins = Insert(self.__proj, self.__max_counts)

__parse_pattern(ev)

Creates and appends Pattern objects to Project. Dispatches PatternEventID events to Pattern for parsing.

Source code in pyflp/parser.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def __parse_pattern(self, ev: EventType) -> None:
    """Creates and appends `Pattern` objects to `Project`.
    Dispatches `PatternEventID` events to `Pattern` for parsing."""

    if ev.id_ == Pattern.EventID.New and isinstance(ev, _WordEvent):
        # Occurs twice, once with the note events only and later again
        # for metadata (name, color and a few undiscovered properties)
        # New patterns can occur for metadata as well; they are empty.
        index = ev.to_uint16()
        if index in self.__pat_indexes:
            for pattern in self.__proj.patterns:
                if pattern.index == index:
                    self.__cur_pat = pattern
            self.__cur_pat.parse_index1(ev)
            return  # Don't let the event be parsed again!
        else:
            self.__pat_indexes.add(index)
            self.__cur_pat = Pattern()
            self.__proj.patterns.append(self.__cur_pat)
    self.__cur_pat.parse_event(ev)

__parse_timemarker(ev)

Source code in pyflp/parser.py
264
265
266
267
268
def __parse_timemarker(self, ev: EventType) -> None:
    if ev.id_ == TimeMarker.EventID.Position:
        self.__cur_tm = TimeMarker()
        self.__tms.append(self.__cur_tm)
    self.__cur_tm.parse_event(ev)

get_events(flp)

Just get the events; don't parse

Why does this method exist? - FLP format has changed a lot over the years; nobody except IL can parse it properly, PyFLP needs a specific event structure. - In the event of failure, user can at least get the events. - FLPInspect and FLPInfo can still display some minimal information based on the events.

Source code in pyflp/parser.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def get_events(
    self, flp: Union[str, Path, bytes, io.BufferedIOBase]
) -> List[EventType]:
    """Just get the events; don't parse

    Why does this method exist?
    - FLP format has changed a lot over the years;
    nobody except IL can parse it properly, PyFLP needs a
    specific event structure.
    - In the event of failure, user can at least get the events.
    - [FLPInspect](https://github.com/demberto/flpinspect) and
    [FLPInfo](https://github.com/demberto/flpinfo) can still
    display some minimal information based on the events.
    """
    # * Argument validation
    self.__proj = proj = Project()
    if isinstance(flp, (Path, str)):
        if isinstance(flp, Path):
            self.__proj.save_path = flp
        else:
            self.__proj.save_path = Path(flp)
        self.__r = BytesIOEx(open(flp, "rb").read())
    elif isinstance(flp, io.BufferedIOBase):
        flp.seek(0)
        self.__r = BytesIOEx(flp.read())
    elif isinstance(flp, bytes):
        self.__r = BytesIOEx(flp)
    else:
        raise TypeError(
            f"Cannot parse a file of type {type(flp)}. \
            Only str, Path or bytes objects are supported."
        )

    r = self.__r
    hdr_magic = r.read(4)
    if hdr_magic != HEADER_MAGIC:
        raise InvalidMagicError(hdr_magic)
    hdr_size = r.read_I()
    if hdr_size != HEADER_SIZE:
        raise InvalidHeaderSizeError(hdr_size)
    proj.misc.format = Misc.Format(r.read_h())
    proj.misc.channel_count = r.read_H()
    proj.misc.ppq = r.read_H()
    data_magic = r.read(4)
    if data_magic != DATA_MAGIC:
        raise InvalidMagicError(data_magic)
    _ = r.read_I()  # Combined size of all events
    self.__build_event_store()

    return self.__events

parse(flp)

Parses an FLP. Use parse_zip for ZIP looped packages instead.

Source code in pyflp/parser.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
def parse(self, flp: Union[str, Path, bytes, io.BufferedIOBase]) -> Project:
    """Parses an FLP. Use `parse_zip` for ZIP looped packages instead."""

    # * Argument validation
    self.__proj.events = self.get_events(flp)

    # * Modify parsing logic as per FL version
    # TODO: This can be as less as 16. Also insert slots were once 8.
    self.__max_counts.inserts = 127 if self.__fl_version.as_float() > 12.89 else 104
    self.__cur_ins = Insert(self.__proj, self.__max_counts)

    # * Build an object model
    # TODO: Parse in multiple layers
    parse_channel = True
    for ev in self.__proj.events:
        if ev.id_ in Misc.EventID.__members__.values():
            self.__proj.misc.parse_event(ev)
        elif ev.id_ in Filter.EventID.__members__.values():
            self.__parse_filter(ev)
        elif ev.id_ == RemoteController.ID:
            controller = RemoteController()
            controller.parse_event(ev)
            self.__proj.controllers.append(controller)
        elif ev.id_ in Pattern.EventID.__members__.values():
            self.__parse_pattern(ev)
        elif ev.id_ in CHANNEL_EVENTS and parse_channel:
            self.__parse_channel(ev)
        elif ev.id_ in TimeMarker.EventID.__members__.values():
            self.__parse_timemarker(ev)
        elif ev.id_ in ARRANGEMENT_EVENTS:
            parse_channel = False
            self.__parse_arrangement(ev)
        elif ev.id_ in INSERT_EVENTS and not parse_channel:
            self.__parse_insert(ev)
        elif ev.id_ == InsertParamsEvent.ID:
            ev.id_ = InsertParamsEvent.ID

            # Append the last insert first
            self.__proj.inserts.append(self.__cur_ins)
            insert_params_ev = InsertParamsEvent(ev)
            if not insert_params_ev.parse(self.__proj.inserts):
                self.__proj._unparsed_events.append(ev)
        else:
            self.__proj._unparsed_events.append(ev)

    # * Post-parse steps
    # Now move all playlist events to track, Playlist can be empty as well
    # Cannot parse playlist events in arrangement, because certain FL versions
    # dump only used tracks. This is not the case anymore.
    for arrangement in self.__proj.arrangements:
        if arrangement.playlist:
            for idx, track in enumerate(arrangement.tracks):
                items = arrangement.playlist.items.get(idx)
                if items:
                    track._items = items
                arrangement.playlist._items = {}

    # Re-arrange patterns by index
    self.__proj.patterns.sort(key=lambda pat: pat.index - 1)

    return self.__proj

parse_zip(zip_file, name='')

Parses an FLP inside a ZIP.

Parameters:

Name Type Description Default
zip_file Union[zipfile.ZipFile, str, bytes, Path, io.BufferedIOBase]

The path to the ZIP file, stream, Path, file-like or a ZipFile object.

required
name str

If the ZIP has multiple FLPs, you need to specify the name of the FLP to parse.

''

Raises:

Type Description
TypeError

When zip_file points to a ZIP or stream containing no files with an extension of .flp.

TypeError

When name is empty and zip_file points to a ZIP or stream containing more than one FLP.

Returns:

Name Type Description
Project Project

The parsed object.

Source code in pyflp/parser.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def parse_zip(self, zip_file, name: str = "") -> Project:
    """Parses an FLP inside a ZIP.

    Args:
        zip_file (Union[zipfile.ZipFile, str, bytes, Path, io.BufferedIOBase]):
            The path to the ZIP file, stream, Path, file-like or a ZipFile
            object.
        name (str, optional): If the ZIP has multiple FLPs, you need
            to specify the name of the FLP to parse.

    Raises:
        TypeError: When `zip_file` points to a ZIP or stream containing no
            files with an extension of .flp.
        TypeError: When `name` is empty and `zip_file` points to a ZIP or
            stream containing more than one FLP.

    Returns:
        Project: The parsed object.
    """

    flp = None

    if isinstance(zip_file, (str, bytes, io.BufferedIOBase, Path)):
        zp = zipfile.ZipFile(zip_file, "r")
    else:
        zp = zip_file

    if name == "":
        # Find the file with .flp extension
        flps = []
        file_names = zp.namelist()
        for file_name in file_names:
            if file_name.endswith(".flp"):
                flps.append(file_name)
        if not len(flps) == 1:  # pragma: no cover
            if not flps:
                raise TypeError("No FLP files found inside ZIP.", zp)
            elif len(flps) > 1:
                raise TypeError(
                    "Optional parameter 'name' cannot be empty "
                    "when more than one FLP exists in ZIP",
                    zp,
                )
        else:
            name = flps[0]

    flp = zp.open(name, "r").read()
    return self.parse(flp)