GithubHelp home page GithubHelp logo

demberto / pyflp Goto Github PK

View Code? Open in Web Editor NEW
109.0 11.0 12.0 5.63 MB

FL Studio project file parser

Home Page: https://pyflp.rtfd.io

License: GNU General Public License v3.0

Python 100.00%
python flp binary-format deserialization libraries serialization

pyflp's Introduction

PyFLP

PyFLP is an unofficial parser for FL Studio project and preset files written in Python.

CI Documentation Build Status pre-commit-ci
PyPI PyPI - Package Version PyPI - Supported Python Versions PyPI - Supported Implementations PyPI - Wheel
Activity Maintenance PyPI - Downloads
QA codecov CodeFactor Grade Checked with mypy pre-commit Security Status
Other License GitHub top language Code Style: Black covenant

From a very general point-of-view, this is the state of what is currently implemented. Click on a link to go to the documentation for that feature.

Group Feature Issues
Arrangements
open arrangement-general issues closed arrangement-general issues
๐ŸŽผ Playlist open arrangement-playlist issues closed arrangement-playlist issues
๐ŸŽž๏ธ Tracks open arrangement-track issues closed arrangement-track issues
Channel Rack
open channel-general issues closed channel-general issues
๐Ÿ“ˆ Automations open channel-automation issues closed channel-automation issues
๐ŸŽน Instruments channel-instrument issues closed channel-instrument issues
๐Ÿ“š Layer open channel-layer issues closed channel-layer issues
๐Ÿ“ Sampler open channel-sampler issues closed channel-sampler issues
Mixer
open mixer-general issues closed mixer-general issues
๐ŸŽš๏ธ Inserts open mixer-insert issues closed mixer-insert issues
๐ŸŽฐ Effect slots open mixer-slot issues closed mixer-slot issues
๐ŸŽถ Patterns
open pattern-general issues closed pattern-general issues
๐ŸŽ› Controllers open pattern-controller issues closed pattern-controller issues
๐ŸŽต Notes open pattern-note issues closed pattern-note issues
๐Ÿšฉ Timemarkers open timemarker issues closed timemarker issues
Plugins Native - 8 effects, 1 synth open plugin-native issues closed plugin-native issues
VST 2/3 plugin-3rdparty issues closed plugin-3rdparty issues
Project - Settings and song metadata open project-general issues closed project-general issues

โฌ Installation

CPython 3.8+ / PyPy 3.8+ required.

python -m pip install -U pyflp

โ–ถ Usage

Load a project file:

import pyflp
project = pyflp.parse("/path/to/parse.flp")

If you get any sort of errors or warnings while doing this, please open an issue.

Save the project:

pyflp.save(project, "/path/to/save.flp")

It is advised to do a backup of your projects before doing any changes. It is also recommended to open the modified project in FL Studio to ensure that it works as intended.

Check the reference for a complete list of useable features.

๐Ÿ™ Acknowledgements

โœจ Contributors

All Contributors

Thanks goes to these wonderful people:


nickberry17

๐Ÿ’ป

zacanger

๐Ÿ› ๐Ÿ“–

Tim

๐Ÿ“– ๐Ÿ’ป ๐Ÿšง

This project follows the all-contributors specification. Contributions of any kind are welcome!

Please see the contributor's guide for more information about contributing.

๐Ÿ“ง Contact

You can contact me either via issues and discussions or through email via demberto(at)proton(dot)me.

ยฉ License

The code in this project has been licensed under the GNU Public License v3.

pyflp's People

Contributors

allcontributors[bot] avatar anandsshah avatar demberto avatar dependabot[bot] avatar imgbot[bot] avatar imgbotapp avatar mips171 avatar pre-commit-ci[bot] avatar ttaschke avatar zacanger avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pyflp's Issues

๐Ÿž `U16TupleEvent.value` is broken

PyFLP/pyflp/_events.py

Lines 301 to 310 in 2ff2d3c

class U16TupleEvent(DWordEventBase[Tuple[int, int]]):
"""An event used for storing a two-tuple of 2 byte unsigned integers."""
@property
def value(self) -> tuple[int, int]:
return UInt.unpack(self._raw)
@value.setter
def value(self, value: tuple[int, int]):
self._raw = UInt.pack(*value)

This happens when testing isn't enough :(

๐Ÿž Tracks don't get assigned their playlist items

PyFLP/pyflp/arrangement.py

Lines 408 to 424 in f71c62b

@property
def tracks(self) -> Iterator[Track]:
count = 0
event = None
max_idx = 499 if dataclasses.astuple(self._kw["version"]) >= (12, 9, 1) else 198
if ArrangementID.Playlist in self._events:
event = cast(PlaylistEvent, self._events[ArrangementID.Playlist][0])
for events in self._collect_events(TrackID):
items: list[_PlaylistItemStruct] = []
if event is not None:
for item in event.items:
idx = item["track_index"]
if max_idx - idx == count:
items.append(cast(_PlaylistItemStruct, item))
yield Track(*events, items=items)

๐Ÿค” Whether `ArrangementsID.Current` marks the end of all arrangement events

PyFLP/pyflp/arrangement.py

Lines 439 to 463 in ceff918

# TODO Verify ArrangementsID.Current is the end
# FL changed event ordering a lot, the latest being the most easiest to
# parse; it contains ArrangementID.New event followed by TimeMarker events
# followed by 500 TrackID events. TimeMarkers occured before new arrangement
# event in initial versions of FL20, making them harder to group.
def __iter__(self) -> Iterator[Arrangement]:
first = True
events: list[AnyEvent] = []
def make_arr():
return Arrangement(*events, version=self._kw["version"])
for event in self._events_tuple:
if event.id == ArrangementID.New:
if not first:
yield make_arr()
events = []
first = not first
elif event.id == ArrangementsID.Current:
return make_arr() # last arrangement
for enum_ in (ArrangementID, TimeMarkerID, TrackID):
if event.id in enum_:
events.append(event)
break

๐Ÿ”ฅ Remove use of fixture factories in unittests

Fixtures like these make the function less readable, have no actual benefit over normal functions (as in, they don't need session scope etc.)

@pytest.fixture
def load_channel(get_model: ModelFixture):
def wrapper(preset: str, type: type[Channel] = Channel):
return get_model(f"channels/{preset}", type)
return wrapper
@pytest.fixture
def load_instrument(load_channel: ModelFixture):
def wrapper(preset: str):
return load_channel(preset, Instrument)
return wrapper
@pytest.fixture
def load_layer(load_channel: Any):
def wrapper(preset: str):
return load_channel(preset, Layer)
return wrapper
@pytest.fixture
def load_sampler(load_channel: Any):
def wrapper(preset: str):
return load_channel(preset, Sampler)
return wrapper

Additional type-hinting code is required as well to pass them as arguments to a test function:

ChannelFixture = Callable[[str], Channel]
InstrumentFixture = Callable[[str], Instrument]
LayerFixture = Callable[[str], Layer]
SamplerFixture = Callable[[str], Sampler]

Readthedocs 404s

It's not a huge deal since the docs are in the repo anyway, but it looks like the mkdocs build failed at some point. Works fine testing locally, but lots of 404s on the actual RTFD site.

screen_shot_2022-07-15_at_20 06 35

๐Ÿ“Œ Implement structs with the `construct` library

construct is a Python library especially made for parsing binary structs. Using it would make parsing structures of any kind immensely simpler, it would take away all the voodoo I do with metaclass of StructBase.

While construct itself is nothing new, I need to experiment with it to retrofit it inside the existing event system. Maybe I could use a construct.Struct to replace all BytesIOEx instances in DataEventBase?

โœจ `Note` key names

Algorithm: Note index // 12 for octave and %% 12 for the key names.

PyFLP/pyflp/pattern.py

Lines 148 to 150 in 2ff2d3c

# TODO Return note names instead of integers
key = StructProp[int]()
"""0-131 for C0-B10. Can hold stamped chords and scales also."""

And obviously the docstring needs to be fixed โš’

๐Ÿž `InsertEQBand` doesn't get required kwargs

Somewhere, here I guess the problem starts:

PyFLP/pyflp/mixer.py

Lines 268 to 277 in f2bd7a0

items: _InsertEQBandKW = {}
for param in instance._kw["params"]:
id = param["id"]
if id == self._ids.freq:
items["freq"] = param
elif id == self._ids.gain:
items["gain"] = param
elif id == self._ids.reso:
items["reso"] = param
return InsertEQBand(kw=items)

The InsertEQ gets the mixer parameters correctly (I checked):

return InsertEQ(self._kw["params"])

๐Ÿ”Ž `Content` declicking mode

image

PyFLP/pyflp/channel.py

Lines 753 to 760 in e5cf566

class Content(MultiEventModel, ModelReprMixin):
"""Used by :class:`Sampler`."""
# declick_mode: enum
keep_on_disk = FlagProp(_SamplerFlags.KeepOnDisk, ChannelID.SamplerFlags)
load_regions = FlagProp(_SamplerFlags.LoadRegions, ChannelID.SamplerFlags)
load_slices = FlagProp(_SamplerFlags.LoadSliceMarkers, ChannelID.SamplerFlags)
resample = FlagProp(_SamplerFlags.Resample, ChannelID.SamplerFlags)

๐Ÿž`Arrangement` doesn't pass playlist items to `Track` properly

This issue might be related to #48. But unlike IterProp properties, Track instances are passed the playlist items they contain from their parent Arrangement.

  1. Arrangement.tracks gathers items from the singular playlist event (there's only one such event per arrangement)

    PyFLP/pyflp/arrangement.py

    Lines 428 to 438 in f2bd7a0

    if ArrangementID.Playlist in self._events:
    pl_event = cast(PlaylistEvent, self._events[ArrangementID.Playlist][0])
    for events in self._collect_events(TrackID):
    items: list[_PlaylistItemStruct] = []
    if pl_event is not None:
    for item in pl_event.items:
    idx = item["track_index"]
    if max_idx - idx == count:
    items.append(cast(_PlaylistItemStruct, item))
    yield Track(*events, items=items)

  2. Track receives it:

    PyFLP/pyflp/arrangement.py

    Lines 295 to 302 in f2bd7a0

    class Track(MultiEventModel, Iterable[PlaylistItemBase], SupportsIndex):
    """Represents a track in an arrangement on which playlist items are arranged.
    ![](https://bit.ly/3de6R8y)
    """
    def __init__(self, *events: AnyEvent, **kw: Unpack[_TrackKW]):
    super().__init__(*events, **kw)

    PyFLP/pyflp/arrangement.py

    Lines 280 to 281 in f2bd7a0

    class _TrackKW(TypedDict):
    items: list[_PlaylistItemStruct]

  3. There goes Track.items:

    PyFLP/pyflp/arrangement.py

    Lines 352 to 353 in f2bd7a0

    items = KWProp[List[PlaylistItemBase]]()
    """Playlist items present on the track."""

๐Ÿž `Track.height` & `Track.locked_height` is not float

TrackStruct:

PyFLP/pyflp/arrangement.py

Lines 102 to 103 in f71c62b

"height": "f", # 17
"locked_height": "f", # 21

Track:

PyFLP/pyflp/arrangement.py

Lines 327 to 335 in f71c62b

height = StructProp[float](id=TrackID.Data)
"""Track height in FL's interface. Linear.
| Type | Value | Percentage |
|---------|-------|------------|
| Min | 0.0 | 0% |
| Max | 18.4 | 1840% |
| Default | 1.0 | 100% |
"""

locked_height = StructProp[float](id=TrackID.Data)

Why?

In a test FLP (FL 20.8.4), I found these values:

Tracked.locked_height

Enabled Disabled
56 4294967280 / -16

These values indicate that locked_height is somehow dependant on height. Although in FL's interface it just a check box

image

Track.height

Min Max Default
0 1097963930 1065353216

๐Ÿ”Ž New `Pattern` events; separate `TimeMarker` from arrangements?

PyFLP/pyflp/pattern.py

Lines 101 to 104 in ceff918

# ChannelIID, _161, _162, Looped, Length occur when pattern is looped.
# ChannelIID and _161 occur for every channel in order.
# ! Looping a pattern puts timemarkers in it. The same TimeMarkerID events are
# !used, which means I need to refactor it out from pyflp.arrangement.

PyFLP/pyflp/pattern.py

Lines 112 to 114 in ceff918

ChannelIID = (DWORD + 32, U32Event) # TODO (FL v20.1b1+)
_161 = (DWORD + 33, I32Event) # TODO -3 if channel is looped else 0 (FL v20.1b1+)
_162 = (DWORD + 34, U32Event) # TODO Appears when pattern is looped, default: 2

๐Ÿ“˜ Sphinx gives warnings

Warnings like these, come from magic I do in docs/conf.py:

Inline interpreted text or phrase reference start-string without end-string.

VST GUID non ASCII; MIDI channel 128

When I try to run the provided example,
I get the following error.
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd1 in position 1: ordinal not in range(128)

Therefore neither this package, nor FLPInspect or FLPInfo work.

Can you please have a look at this?

๐Ÿž `ChannelType` is not what it seems

PyFLP/pyflp/channel.py

Lines 311 to 322 in 816c78d

class ChannelType(enum.IntEnum): # cuz Type would be a super generic name
"""An internal marker used to indicate the type of a channel."""
Sampler = 0
"""Used exclusively for the inbuilt Sampler."""
Native = 2
"""Used by audio clips and other native FL Studio synths."""
Layer = 3 # 3.4.0+
Instrument = 4
Automation = 5 # 5.0+

Tests

This code was run with the provided test FLP:

import pyflp
from pyflp.channel import ChannelID, ChannelType

project = pyflp.parse("./tests/assets/FL 20.8.4.flp")

for channel in project.channels:
    print(f"{ChannelType(channel.events_asdict()[ChannelID.Type][0].value).name} - {channel.name}")

Output:

Native - BooBass
Sampler - Instrument track
Layer - Layer
Sampler - Sampler
Instrument - Colored
Automation - Automation Clip
Native - VST2
Instrument - Audio Clip
Native - MIDI Out
Native - Fruit Kick
Native - Plucked!
Instrument - 22in Kick

๐Ÿ™„ Not quite what I expected.

Observations

  • Empty Audio Clip instances are Instrument (Colored, Audio Clip and 22in Kick).

    image

  • 3rd party plugin Sylenth1 (named VST2) is surprisingly of type Native.

    image

Solutions

Channel type detection (Sampler, Instrument or Native) should not be done solely based upon ChannelID.Type event

PyFLP/pyflp/channel.py

Lines 1011 to 1039 in 816c78d

def __iter__(self): # pylint: disable=too-complex
ch_dict: dict[int, Channel] = {}
events: DefaultDict[int, list[AnyEvent]] = collections.defaultdict(list)
cur_ch_events = []
for event in self._events_tuple:
if event.id == ChannelID.New:
cur_ch_events = events[event.value]
if event.id not in RackID:
cur_ch_events.append(event)
for iid, ch_events in events.items():
ct = None
for event in ch_events:
if event.id == ChannelID.Type:
if event.value == ChannelType.Automation:
ct = Automation
elif event.value == ChannelType.Instrument:
ct = Instrument
elif event.value == ChannelType.Layer:
ct = Layer
elif event.value == ChannelType.Sampler:
ct = Sampler
else:
ct = Channel
if ct is not None:
cur_ch = ch_dict[iid] = ct(*ch_events, channels=ch_dict)
yield cur_ch

Feels like I am fixing IL's bugs now ๐Ÿ˜›

๐ŸžTypeError: argument of type `int` is not iterable when accessing `Insert.dock`

PyFLP/pyflp/mixer.py

Lines 472 to 484 in 227f862

def dock(self) -> InsertDock | None:
"""The position (left, middle or right) where insert is docked in mixer.
![](https://bit.ly/3eLum9D)
"""
events = self._events.get(InsertID.Flags)
if events is not None:
event = cast(InsertFlagsEvent, events[0])
if _InsertFlags.DockMiddle in event["flags"]:
return InsertDock.Middle
if _InsertFlags.DockRight in event["flags"]:
return InsertDock.Right
return InsertDock.Left

๐Ÿ”Ž Discover remaining `Envelope` properties

PyFLP/pyflp/channel.py

Lines 103 to 118 in 1571591

class _EnvelopeLFOStruct(StructBase): # 2.5.0+
PROPS = {
"flags": "i", # 4
"envelope.enabled": "i", # 8
"envelope.predelay": "i", # 12
"envelope.attack": "i", # 16
"envelope.hold": "i", # 20
"envelope.decay": "i", # 24
"envelope.sustain": "i", # 28
"envelope.release": "i", # 32
"_u20": 20, # 52
"lfo.shape": "i", # 56
"envelope.attack_tension": "i", # 60
"envelope.sustain_tension": "i", # 64
"envelope.release_tension": "i", # 68
}

20 bytes worth of data is still undiscovered

image

Undiscovered properties:

  • Envelope Amount
  • Envelope Tempo Sync
  • LFO Delay
  • LFO Attack
  • LFO Amount
  • LFO Speed

๐Ÿ”Ž `Note.key` representation.

PyFLP/pyflp/pattern.py

Lines 133 to 134 in ceff918

key = StructProp[int]() # TODO Separate property and chord/scale detection
"""0-131 for C0-B10. Can hold stamped chords and scales also."""

stamped_keys

The property name itself is ambiguous, it holds stamped scales and chords as a whole, not as 3 separate notes. This makes FL remember the grouping.

๐Ÿž `pan` var in `PatternNote` limited to -64 & 64 but some flp exceed these values

Describe the issue

the minimum and maximum values assigned to the pattern pan can be quickly exceeded by certain projects, which returns a value error.

PyFLP version

1.1.1

What code caused this issue?

import pyflp
import sys, os

os.chdir(sys.path[0])

parse = pyflp.Parser()

l = parse.parse("random.flp")

Screenshots, Additional info

image

Code of Conduct

  • I agree to follow this project's Code of Conduct

โœจ Interpreting encoded musical times

Many fields containing time and position information (as well as start / end offsets) have been discovered already. They are exposed to as-is, no conversions from the uint32 or whatever to a human readable representation consisting of beats, bars and divisions.

๐Ÿ˜Ž What I already know

Time / position fields are most of the time dependant on the PPQ of the project (and not the tempo!),

For example, at a PPQ of 96, a length of 96 means that the entity's musical length is a quarter or, a beat or, 1/4th of a bar.

Using the PPQ as a runtime dependency for calculation is already possible, but the part where problems begin are time signatures.

โฐ What's with time signatures?

The above calculations work only for a time signature of 4/4. If while measuring the length, a time signature occurs in the playlist or in a pattern, the formula probably changes and not accounting for this would mean all further calculations will go wrong.

๐Ÿ‘€ Where to look for?

The FLP format is very close to MIDI in terms of many things. I think, the MIDI format already has something of this kind and I would like to get insight from somebody who has experience in MIDI or how musical timings are represented in DAWs.


Achieving this would be a great feat ๐Ÿฅณ, its one of those features that would make PyFLP 10x more useful that its right now.

๐Ÿ’ก Make collections indexable by names

Currently, model collections like Patterns, ChannelRack, Arrangements, Mixer, Insert and so on implement __getitem__ to allow passing an index of the model.

Example:

project.arrangements[1]    # Arrangement(name="...", ...)

This works, but has little value practically speaking. Nobody remembers the index of the pattern or much less of a channel.
Models have a name property which can be used as a parameter for the __getitem__ method. Once that is done,

project.arrangements["arrangement_name"]    # returns an Arrangement with that name

Awesome Project

Awesome project! really like it but got some questions is it possible to get in touch with you somehow?
Through discord or sum?
Thanks

๐Ÿ”Ž Sampler `FX` properties

image

  • Remove DC offset
  • Normalize
  • Reverse
  • Reverse polarity
  • Fade stereo
  • Swap stereo
  • Sample start
  • Length
  • Fade in
  • Fade out
  • Crossfade
  • Trim

image

  • Boost
  • Clip
  • Cutoff
  • EQ
  • Pogo
  • Resonance
  • RM (Freq + Mix)
  • Reverb (Mix + Mode)
  • Stereo Delay

๐Ÿ”Ž `Playback` start offset

image

PyFLP/pyflp/channel.py

Lines 728 to 736 in e5cf566

class Playback(MultiEventModel, ModelReprMixin):
"""Used by :class:`Sampler`.
![](https://bit.ly/3xjSypY)
"""
ping_pong_loop = EventProp[bool](ChannelID.PingPongLoop)
# start_offset: int
use_loop_points = FlagProp(_SamplerFlags.UsesLoopPoints, ChannelID.SamplerFlags)

๐Ÿž `Arrangement` parsing logic is incorrect

  1. Pattern timemarkers get added to the first arrangement's events.

  2. Last arrangement isn't yielded by Arrangements.__iter__ at all.

    PyFLP/pyflp/arrangement.py

    Lines 444 to 463 in ceff918

    def __iter__(self) -> Iterator[Arrangement]:
    first = True
    events: list[AnyEvent] = []
    def make_arr():
    return Arrangement(*events, version=self._kw["version"])
    for event in self._events_tuple:
    if event.id == ArrangementID.New:
    if not first:
    yield make_arr()
    events = []
    first = not first
    elif event.id == ArrangementsID.Current:
    return make_arr() # last arrangement
    for enum_ in (ArrangementID, TimeMarkerID, TrackID):
    if event.id in enum_:
    events.append(event)
    break

โ“ Should model classes subclass protocols?

Many model classes (classes which derive from ModelBase), support dunder methods like __getitem__, __iter__, __index__ etc. to make the API more Pythonic / idiomatic and remove the need for certain properties and clumsily named functions which achieve the same thing.

To make this more apparent, I subclass my model classes with protocols from typing or typing_extensions whenever possible.

I have been wondering whether this really is necessary? It has no other benefits than to show support for a certain protocol at runtime and isinstance checks with a ModelBase class and a protocol work just fine without subclassing the protocol. ๐Ÿค”

React with ๐Ÿ‘ if you think I should keep the way it is or ๐Ÿ‘Ž if you don't

๐Ÿ’ก Slicing for `ModelBase` collections

TIL, slicing needs to be separately implemented even when the __getitem__ method currently supports SupportsIndex values.

In layman terms, this works:

obj[0], obj[1]  # โœ”

but this doesn't:

obj[0:1]    # โŒ

๐Ÿ“˜ Cache expanded image links in some way that they point back to the ones in `docs/img`

Currently docstrings contain bit.ly links pointing to images in docs/img folder. Sphinx docs frequently need to be cleaned and rebuilt and this step takes quite some time.

PyFLP/docs/conf.py

Lines 102 to 108 in f2bd7a0

def transform_image_links(app, what, name, obj, options, lines):
"""Convert Bit.ly markdown image links to local reStructuredText ones."""
for idx, line in enumerate(lines):
match = BITLY_LINK.fullmatch(line)
if match is not None:
url = urlopen(match[1]).geturl()
lines[idx] = f".. image:: {url.replace(GHUC_PREFIX, '', 1)}"

These links can be cached into a mapping of shortened links to relative local links, which the transform_image_links hook can directly parse.

๐Ÿ“˜ Add images to docstrings once I refresh my Bitly quota

PyFLP/pyflp/arrangement.py

Lines 335 to 336 in f2bd7a0

# TODO Add link to GIF from docs once Bitly quota is available again.
enabled = StructProp[bool](id=TrackID.Data)

PyFLP/pyflp/arrangement.py

Lines 355 to 357 in f2bd7a0

# TODO Add link to GIF from docs once Bitly quota is available again.
locked = StructProp[bool](id=TrackID.Data)
"""Whether the tracked is in a locked state."""

PyFLP/pyflp/channel.py

Lines 771 to 772 in f2bd7a0

# TODO Add link to GIF from docs once Bitly quota is available again.
enabled = EventProp[bool](ChannelID.IsEnabled)

๐Ÿ“˜ Convert VSCode compatible GFM tables into RST ones while building

Currently, all the tables in the docstrings are in the simple reStructured text format. They don't render in VSCode. If I use GFM tables instead (like I did earlier), then Sphinx doesn't understand them.

Since, VSCode will have no way to render a rst table, I plan to convert all the tables into GFM again.

I have explored MyST parser's Python API a bit, but there seems to be no way to just pass in a string and get it back converted.
Sphinx autodoc won't ever probably support this usecase, neither napoleon.

๐Ÿ“˜ Make docstrings IDE agnostic

Images and tables are a very integral part of PyFLP. Most properties use markdown image links. These perfectly work in VSCode but PyCharm, for example doesn't. Similarly for tables, VSCode supports the GFM syntax, but PyCharm supports rST tables.

I do dev in VSCode and plan to keep docstrings in markdown. If there's no IDE agnostic solution. If PyCharm has a extension which supports markdown in docstrings please let me know of it, so I can add that info to the docs.

EDIT: Looks like this might never be possible. So, PyFLP users - GO VSCODE!

implement: More native plugin data parsers

Its impossible and not the scope of this library to be able to parse all plugin data, these are a few which should be easier to implement.

Generators

  • 3xOSC
  • BooBass
  • Plucked

Effects

  • Fruity Balance
  • Fruity Blood Overdrive
  • Fruity Center (lol whatever it does)
  • Fruity Fast Dist
  • Fruity Free Filter
  • Fruity Mute 2
  • Fruity Notebook 2
  • Fruity Send
  • Fruity Soft Clipper
  • Fruity Stereo Enhancer
  • Soundgoodizer

Notes for implementers

Technically, all plugins with just knobs and sliders should be easy to implement, Most data is stored in 4 byte unsigned integers but 0-100% or whatever the extremes of the knobs/sliders are, they are pretty small values in terms of the available range.

Once you have implemented a parser for a plugin, please make sure that you add the relevant docstrings and docs. You can choose to add an image of the plugin in docs/img folder of the repo, but don't add your own bit.ly links to docstrings, that will be done by me.

If you have any suggestions, please let me know or if you have PRs mention this issue.

๐Ÿž `Track.color` returns `int` instead of `colour.Color`

color = StructProp[colour.Color](id=TrackID.Data)

Solution 1; Change StructBase to encode and decode colour.Color directly into StructEvent._props

PyFLP/pyflp/_base.py

Lines 521 to 530 in 4a063da

def __init__(self, stream: BytesIOEx):
self._props: dict[str, Any] = dict.fromkeys(type(self).PROPS)
self._stream = stream
self._stream_len = len(stream.getvalue())
for key, type_or_size in type(self).PROPS.items():
if isinstance(type_or_size, int):
self._props[key] = self._stream.read(type_or_size)
else:
self._props[key] = getattr(self._stream, f"read_{type_or_size}")()

PyFLP/pyflp/_base.py

Lines 544 to 557 in 4a063da

def __setitem__(self, key: str, value: Any):
if key not in type(self).PROPS:
raise KeyError(key)
self._stream.seek(type(self).OFFSETS[key])
type_or_size = type(self).PROPS[key]
if isinstance(type_or_size, int):
self._stream.write(value)
else:
getattr(self._stream, f"write_{type_or_size}")(value)
if len(self._stream.getvalue()) > self._stream_len and self.TRUNCATE:
raise PropertyCannotBeSet
self._props[key] = value

Solution 2: Add a ColorProp to pyflp._base, specifically for colours encoded in variable sized events

This is the better option imo, because it will not modify StructEvent._props, which will be used to store event values without any type of interpretation (mainly for debugging purposes). Using the first solution also breaks one of the highlights of PyFLP 2.0.0 - zero pre-parse validation.

๐Ÿž Parse errors regarding VST plugins

Describe the issue

About 16% of the files I tested v2.0 with raised some sort of error, though loadable through normal FL Studio. Most of these failed because of "KeyError: None" on line 243 of plugin.py, seemingly due to VST plugins.

PyFLP version

2.0.0a0

What code caused this issue?

def __bytes__(self) -> bytes:
        self._stream.seek(0)
        for event in self._events:
            try:
                key = getattr(_VSTPluginEventID(event.id), "key")
            except ValueError:
                key = event.id
       -------> event.value = self._props[key]
            self._stream.write(bytes(event))
        return super().__bytes__()

Screenshots, Additional info

Had to upload as a ZIP since GitHub doesn't support FLP files as attachments by default.

Projects causing VST plugin parse errors.zip

Code of Conduct

  • I agree to follow this project's Code of Conduct

โ— Improve testing

Currently, all testing is limited to just the property getters and a few validated property setters. A fair amount of code is untested - ignore coverage scores it gets confused by descriptors

๐Ÿž `IterProp` properties doesn't work with `ListEventBase`

IterProp is for properties having multiple events

tracking = IterProp(ChannelID.Tracking, Tracking)

PyFLP/pyflp/channel.py

Lines 952 to 953 in f2bd7a0

envelopes = IterProp(ChannelID.EnvelopeLFO, Envelope)
"""One each for Volume, Panning, Mod X, Mod Y and Pitch :class:`Envelope`."""

PyFLP/pyflp/channel.py

Lines 965 to 966 in f2bd7a0

lfos = IterProp(ChannelID.EnvelopeLFO, LFO)
"""One each for Volume, Panning, Mod X, Mod Y and Pitch :class:`LFO`."""

Each of these have a separate backing event. For instance, every Envelope or LFO has a separate ChannelID.EnvelopeLFO event.

The problem

controllers = IterProp(PatternID.Controllers, Controller)

notes = IterProp(PatternID.Notes, Note)

Each of these are a collection of items obtained from a single event. Their associated Structs derive from ListEventBase

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.