From 807a17e403007f33bef826498b699a91af468b34 Mon Sep 17 00:00:00 2001 From: demberto Date: Fri, 28 Oct 2022 00:21:28 +0530 Subject: [PATCH] feat: fruity wrapper flags via #95 --- CHANGELOG.md | 3 + docs/img/plugin/wrapper/settings-gui.png | Bin 0 -> 13966 bytes docs/img/plugin/wrapper/settings-midi.png | Bin 0 -> 19296 bytes docs/reference/plugins.rst | 57 ++- pyflp/arrangement.py | 5 +- pyflp/plugin.py | 415 ++++++++++++++++++---- tests/assets/plugins/fruity-wrapper.fst | Bin 0 -> 1519 bytes tests/test_plugin.py | 29 ++ tox.ini | 2 +- 9 files changed, 413 insertions(+), 98 deletions(-) create mode 100644 docs/img/plugin/wrapper/settings-gui.png create mode 100644 docs/img/plugin/wrapper/settings-midi.png create mode 100644 tests/assets/plugins/fruity-wrapper.fst diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a578fd..a1bed4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve enum performance by using `f-enum` library (`pyflp.parse` is 50% faster). - `Time.gate`, `Time.shift` and `Time.full_porta` [#89]. - *Experimental* Python 3.11 support is back. +- A shit ton of flags in `VSTPlugin` and refactoring [#95]. ### Changed @@ -31,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename `PlaylistItemBase` to `PLItemBase` and `PatternPlaylistItem` to `PatternPLItem`. - Rename `Polyphony` members `is_mono` to `mono` and `is_porta` to `porta`. - `NoModelsFound` also bases `LookupError` now. +- Compiled `VSTPluginEvent.STRUCT`. ### Fixed @@ -40,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PlaylistItemBase.start_offset` and `PlaylistItemBase.end_offset`. - Redundant exceptions `ExpectedValue`, `UnexpectedType`. +- Undiscovered `num_inputs`, `num_outputs` and `vst_number` from `VSTPlugin`. [#55]: https://github.com/demberto/PyFLP/issues/55 [#84]: https://github.com/demberto/PyFLP/issues/84 diff --git a/docs/img/plugin/wrapper/settings-gui.png b/docs/img/plugin/wrapper/settings-gui.png new file mode 100644 index 0000000000000000000000000000000000000000..3f027629cff503af64e588c5b265af38566637cb GIT binary patch literal 13966 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 z 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 Hu0mjf${is` literal 0 HcmV?d00001 diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 74e4536..7499c3f 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -4,23 +4,6 @@ .. module:: pyflp.plugin .. autoclass:: _PluginBase :members: -.. autoclass:: VSTPlugin - :members: - - .. tab-set:: - - .. tab-item:: Settings - - .. image:: /img/plugin/wrapper/settings.png - - .. tab-item:: Processing - - .. image:: /img/plugin/wrapper/processing.png - - .. tab-item:: Troubleshooting - - .. image:: /img/plugin/wrapper/troubleshooting.png - .. autoclass:: PluginIOInfo :members: @@ -50,6 +33,46 @@ Effects .. autoclass:: Soundgoodizer :members: +VST +--- + +.. autoclass:: VSTPlugin + :members: + + .. tab-set:: + + .. tab-item:: Settings + + .. image:: /img/plugin/wrapper/settings.png + + .. autoclass:: pyflp.plugin::VSTPlugin._AutomationOptions + :members: + .. autoclass:: pyflp.plugin::VSTPlugin._MIDIOptions + :members: + .. autoclass:: pyflp.plugin::VSTPlugin._UIOptions + :members: + + .. tab-item:: Processing + + .. image:: /img/plugin/wrapper/processing.png + + .. autoclass:: pyflp.plugin::VSTPlugin._ProcessingOptions + :members: + + .. tab-item:: Troubleshooting + + .. image:: /img/plugin/wrapper/troubleshooting.png + + .. autoclass:: pyflp.plugin::VSTPlugin._CompatibilityOptions + :members: + + +Enums +----- + +.. autoclass:: WrapperPage + :members: + Event IDs --------- diff --git a/pyflp/arrangement.py b/pyflp/arrangement.py index 28c20f1..93f0369 100644 --- a/pyflp/arrangement.py +++ b/pyflp/arrangement.py @@ -566,7 +566,10 @@ def current(self) -> Arrangement | None: raise ModelNotFound(index) from exc loop_pos = EventProp[int](ArrangementsID.LoopPos) - """*New in FL Studio v1.3.8*.""" + """Playlist loop start and end points, + + *New in FL Studio v1.3.8*. + """ @property def max_tracks(self) -> Literal[500, 199]: diff --git a/pyflp/plugin.py b/pyflp/plugin.py index c4626d9..2572afb 100644 --- a/pyflp/plugin.py +++ b/pyflp/plugin.py @@ -18,7 +18,7 @@ import enum import sys import warnings -from typing import Any, ClassVar, Dict, Generic, TypeVar +from typing import Any, ClassVar, Dict, Generic, TypeVar, cast if sys.version_info >= (3, 8): from typing import Literal, Protocol, get_args, runtime_checkable @@ -28,7 +28,7 @@ import construct as c import construct_typed as ct -from ._descriptors import FlagProp, RWProperty, StdEnum, StructProp +from ._descriptors import FlagProp, NamedPropMixin, RWProperty, StdEnum, StructProp from ._events import ( DATA, DWORD, @@ -160,10 +160,10 @@ class WrapperEvent(StructEventBase): @enum.unique class _VSTPluginEventID(ct.EnumBase): - def __new__(cls, id: int, key: str | None = None): + def __new__(cls, id: int, ascii: bool = False): obj = int.__new__(cls, id) obj._value_ = id - setattr(obj, "key", key) + setattr(obj, "ascii", ascii) return obj MIDI = 1 @@ -172,16 +172,60 @@ def __new__(cls, id: int, key: str | None = None): Inputs = 31 Outputs = 32 PluginInfo = 50 - FourCC = (51, "fourcc") # Not present for Waveshells & VST3 - GUID = (52, "guid") - State = (53, "state") - Name = (54, "name") - PluginPath = (55, "plugin_path") - Vendor = (56, "vendor") + FourCC = (51, True) # Not present for Waveshells & VST3 + GUID = 52 + State = 53 + Name = (54, True) + PluginPath = (55, True) + Vendor = (56, True) _57 = 57 # TODO, not present for Waveshells +class _VSTFlags(enum.IntFlag): + SendPBRange = 1 << 0 + FixedSizeBuffers = 1 << 1 + NotifyRender = 1 << 2 + ProcessInactive = 1 << 3 + DontSendRelVelo = 1 << 5 + DontNotifyChanges = 1 << 6 + SendLoopPos = 1 << 11 + AllowThreaded = 1 << 12 + KeepFocus = 1 << 15 + DontKeepCPUState = 1 << 16 + SendModX = 1 << 17 + LoadBridged = 1 << 18 + ExternalWindow = 1 << 21 + UpdateWhenHidden = 1 << 23 + DontResetOnTransport = 1 << 25 + DPIAwareBridged = 1 << 26 + AcceptFileDrop = 1 << 28 + AllowSmartDisable = 1 << 29 + ScaleEditor = 1 << 30 + DontUseTimeOffset = 1 << 31 + + +class _VSTFlags2(enum.IntFlag): + ProcessMaxSize = 1 << 0 + UseMaxFromHost = 1 << 1 + + class VSTPluginEvent(StructEventBase): + _MIDIStruct = c.Struct( + "input" / c.Optional(c.Int32sl), # 4 + "output" / c.Optional(c.Int32sl), # 8 + "pb_range" / c.Optional(c.Int32ul), # 12 + "_extra" / c.GreedyBytes, # upto 20 + ).compile() + + _FlagsStruct = c.Struct( + "_u1" / c.Optional(c.Bytes(9)), # 9 + "flags" / c.Optional(StdEnum[_VSTFlags](c.Int32ul)), # 13 + "flags2" / c.Optional(StdEnum[_VSTFlags2](c.Int32ul)), # 17 + "_u2" / c.Optional(c.Bytes(5)), # 22 + "fast_idle" / c.Optional(c.Flag), # 23 + "_extra" / c.GreedyBytes, + ).compile() + STRUCT = c.Struct( "type" / c.Int32ul, # * 8 or 10 for VSTs, but I am not forcing it "events" @@ -190,10 +234,21 @@ class VSTPluginEvent(StructEventBase): "id" / StdEnum[_VSTPluginEventID](c.Int32ul), # ! Using a c.Select or c.IfThenElse doesn't work here # Check https://github.com/construct/construct/issues/993 - "data" / c.Prefixed(c.Int64ul, c.GreedyBytes), + "data" + / c.Prefixed( + c.Int64ul, + c.Switch( + c.this["id"], + { + _VSTPluginEventID.MIDI: _MIDIStruct, + _VSTPluginEventID.Flags: _FlagsStruct, + }, + default=c.GreedyBytes, + ), + ), ), ), - ) + ).compile() def __init__(self, id: Any, data: bytearray): if data[0] not in (8, 10): @@ -206,34 +261,34 @@ def __init__(self, id: Any, data: bytearray): ) super().__init__(id, data) - def __getitem__(self, key: str) -> str | bytes: - for event in self._struct["events"]: - if event["id"].key == key: - if self._is_ascii_event(event["id"]): - return event["data"].decode("ascii") - return event["data"] + def __getitem__(self, key: Any): + if not isinstance(key, _VSTPluginEventID): + raise TypeError("Expected 'key' to be of type _VSTPluginEventID") + + for e in self._struct["events"]: + if e["id"] == key: + return e["data"].decode("ascii") if e["id"].ascii else e["data"] raise AttributeError(f"No event with key {key!r} found") - def __setitem__(self, key: str, value: str | bytes): - for event in self._struct["events"]: - if self._is_ascii_event(event["id"]) and isinstance(value, str): + def __setitem__(self, key: Any, value: Any): + if not isinstance(key, _VSTPluginEventID): + raise TypeError("Expected 'key' to be of type _VSTPluginEventID") + + for e in self._struct["events"]: + if e["id"].ascii and isinstance(value, str): try: value.encode("ascii") except UnicodeEncodeError as exc: raise ValueError("Strings must have only ASCII data") from exc - if event["id"].key == key: - event["size"] = len(value) - event["data"] = value + if e["id"].key == key: + e["size"] = len(value) + e["data"] = value # Errors if any, will be raised here itself, so its # better not to override __bytes__ for this part self._data = self.STRUCT.build(self._struct) - @staticmethod - def _is_ascii_event(id: _VSTPluginEventID): - return not getattr(id, "key").isdecimal() - @enum.unique class PluginID(EventEnum): @@ -327,9 +382,51 @@ def __set__(self, instance: EventModel, value: AnyPlugin): instance.events[PluginID.Wrapper] = value.events[PluginID.Wrapper] -class _PluginDataProp(StructProp[T]): - def __init__(self, prop: str | None = None): - super().__init__(PluginID.Data, prop=prop) +class _NativePluginProp(StructProp[T]): + def __init__(self, prop: str | None = None, **kwds: Any): + super().__init__(PluginID.Data, prop=prop, **kwds) + + +class _VSTPluginProp(RWProperty[T], NamedPropMixin): + def __init__(self, id: _VSTPluginEventID, prop: str | None = None): + self._id = id + NamedPropMixin.__init__(self, prop) + + def __get__(self, instance: EventModel, _=None) -> T: + value = cast(VSTPluginEvent, instance.events.first(PluginID.Data))[self._id] + return self._get(value) + + def _get(self, value: Any) -> T: + return cast(T, value if isinstance(value, (str, bytes)) else value[self._prop]) + + def __set__(self, instance: EventModel, value: T): + self._set(cast(VSTPluginEvent, instance.events.first(PluginID.Data)), value) + + def _set(self, event: VSTPluginEvent, value: T): + if self._prop is None: + event[self._id] = value + else: + event[self._id][self._prop] = value + + +class _VSTFlagProp(_VSTPluginProp[bool]): + def __init__(self, flag: enum.IntFlag, prop: str = "flags", inverted: bool = False): + super().__init__(_VSTPluginEventID.Flags, prop) + self._flag = flag + self._inverted = inverted + + def _get(self, value: Any) -> bool: + retbool = self._flag in value[self._prop] + return retbool if not self._inverted else not retbool + + def _set(self, event: VSTPluginEvent, value: bool): + if self._inverted: + value = not value + + if value: + event[self._id][self._prop] |= value + else: + event[self._id][self._prop] &= ~value class PluginIOInfo(EventModel): @@ -345,48 +442,208 @@ class VSTPlugin(_PluginBase[VSTPluginEvent], _IPlugin): """ INTERNAL_NAME = "Fruity Wrapper" - fourcc = _PluginDataProp[str]() + + class _AutomationOptions(EventModel): + """See :attr:`VSTPlugin.automation`.""" + + notify_changes = _VSTFlagProp(_VSTFlags.DontNotifyChanges, inverted=True) + """Record parameter changes as automation. + + :guilabel:`Notify about parameter changes`. Defaults to ``True``. + """ + + class _CompatibilityOptions(EventModel): + """See :attr:`VSTPlugin.compatibility`.""" + + buffers_maxsize = _VSTFlagProp(_VSTFlags2.UseMaxFromHost, prop="flags2") + """:guilabel:`Use maximum buffer size from host`. Defaults to ``False``.""" + + fast_idle = _VSTPluginProp[bool](_VSTPluginEventID.Flags) + """Increases idle rate - can make plugin GUI feel more responsive if its slow. + + May increase CPU usage. Defaults to ``False``. + """ + + fixed_buffers = _VSTFlagProp(_VSTFlags.FixedSizeBuffers) + """:guilabel:`Use fixed size buffers`. Defaults to ``False``. + + Makes FL Studio send fixed size buffers instead of variable ones when ``True``. + Can fix rendering errors caused by plugins. Increases latency by 2ms. + """ + + process_maximum = _VSTFlagProp(_VSTFlags2.ProcessMaxSize, prop="flags2") + """:guilabel:`Process maximum size buffers`. Defaults to ``False``.""" + + reset_on_transport = _VSTFlagProp(_VSTFlags.DontResetOnTransport, inverted=True) + """:guilabel:`Reset plugin when FL Studio resets`. Defaults to ``True``.""" + + send_loop = _VSTFlagProp(_VSTFlags.SendLoopPos) + """Lets the plugin know about :attr:`Arrangemnt.loop_pos`. + + :guilabel:`Send loop position`. Defaults to ``True``. + """ + + use_time_offset = _VSTFlagProp(_VSTFlags.DontUseTimeOffset, inverted=True) + """Adjust time information reported by plugin. + + Can fix timing issues caused by plugins in FL Studio <20.7 project. + :guilabel:`Use time offset`. Defaults to ``False``. + """ + + class _MIDIOptions(EventModel): + """See :attr:`VSTPlugin.midi`. + + ![](https://bit.ly/3NbGr4U) + """ + + input = _VSTPluginProp[int](_VSTPluginEventID.MIDI) + """MIDI Input Port. Min = 0, Max = 255. Not selected = -1 (default).""" + + output = _VSTPluginProp[int](_VSTPluginEventID.MIDI) + """MIDI Output Port. Min = 0, Max = 255. Not selected = -1 (default).""" + + pb_range = _VSTPluginProp[int](_VSTPluginEventID.MIDI) + """Pitch bend range MIDI RPN sent to the plugin (in semitones). + + Min = 1. Max = 48. Defaults to 12. + """ + + send_modx = _VSTFlagProp(_VSTFlags.SendModX) + """:guilabel:`Send MOD X as polyphonic aftertouch`. Defaults to ``False``.""" + + send_pb = _VSTFlagProp(_VSTFlags.SendPBRange) + """:guilabel:`Send pitch bend range (semitones)`. Defaults to ``False``. + + See also: + :attr:`pb_range` - Sent to plugin as a MIDI RPN if this is ``True``. + """ + + send_release = _VSTFlagProp(_VSTFlags.DontSendRelVelo, inverted=True) + """Whether release velocity should be sent in note off messages. + + :guilabel:`Send note release velocity`. Defaults to ``True``. + """ + + class _ProcessingOptions(EventModel): + """See :attr:`VSTPlugin.processing`.""" + + allow_sd = _VSTFlagProp(_VSTFlags.AllowSmartDisable) + """:guilabel:`Allow smart disable`. Defaults to ``True``. + + Disables the :attr:`VSTPlugin.smart_disable` feature if ``False``. + """ + + bridged = _VSTFlagProp(_VSTFlags.LoadBridged) + """Load a plugin in separate process. + + :guilabel:`Make bridged`. Defaults to ``False``. + """ + + external = _VSTFlagProp(_VSTFlags.ExternalWindow) + """Keep plugin editor in bridge process. + + :guilabel:`External window`. Defaults to ``False``. + """ + + keep_state = _VSTFlagProp(_VSTFlags.DontKeepCPUState, inverted=True) + """Don't touch unless you have issues like DC offsets, spikes and crashes. + + :guilabel:`Ensure processor state in callbacks`. Defaults to ``True``. + """ + + multithreaded = _VSTFlagProp(_VSTFlags.AllowThreaded) + """Allow plugin to be multi-threaded by FL Studio. + + Disables the :attr:`VSTPlugin.multithreaded` feature if ``False``. + + :guilabel:`Allow threaded processing`. Defaults to ``True``. + """ + + notify_render = _VSTFlagProp(_VSTFlags.NotifyRender) + """Lets the plugin know when rendering to audio file. + + This can be used by the plugin to switch to HQ processing or disable + output entirely if it is in demo mode (depends on the plugin logic). + + :guilabel:`Notify about rendering mode`. Defaults to ``True``. + """ + + process_inactive = _VSTFlagProp(_VSTFlags.ProcessInactive) + """Make FL Studio also process inputs / outputs marked as inactive by plugin. + + :guilabel:`Process inactive inputs and outputs`. Defaults to ``True``. + """ + + class _UIOptions(EventModel): + """See :attr:`VSTPlugin.ui`.. + + ![](https://bit.ly/3Nb3dtP) + """ + + accept_drop = _VSTFlagProp(_VSTFlags.AcceptFileDrop) + """Host is bypassed when a file is dropped on the plugin editor. + + :guilabel:`Accept dropped files`. Defaults to ``False``. + """ + + always_update = _VSTFlagProp(_VSTFlags.UpdateWhenHidden) + """Whether plugin UI should be updated when hidden; default to ``False``.""" + + dpi_aware = _VSTFlagProp(_VSTFlags.DPIAwareBridged) + """Enable if plugin editors look too big or small. + + :guilabel:`DPI aware when bridged`. Defaults to ``True``. + """ + + scale_editor = _VSTFlagProp(_VSTFlags.ScaleEditor) + """Scale dimensions of editor that appear cut-off on high-res screens. + + :guilabel:`Scale editor dimensions`. Defaults to ``False``. + """ + + def __init__(self, events: EventTree, **kw: Any): + super().__init__(events, **kw) + + # This doesn't break lazy evaluation in any way + self.automation = self._AutomationOptions(events) + self.compatibility = self._CompatibilityOptions(events) + self.midi = self._MIDIOptions(events) + self.processing = self._ProcessingOptions(events) + self.ui = self._UIOptions(events) + + fourcc = _VSTPluginProp[str](_VSTPluginEventID.FourCC) """A unique four character code identifying the plugin. A database can be found on Steinberg's developer portal. """ - guid = _PluginDataProp[bytes]() # See issue #8 - midi_in = _PluginDataProp[int]() - """MIDI Input Port. Min: 0, Max: 255.""" - - midi_out = _PluginDataProp[int]() - """MIDI Output Port. Min: 0, Max: 255.""" - - name = _PluginDataProp[str]() + guid = _VSTPluginProp[bytes](_VSTPluginEventID.GUID) # See issue #8 + name = _VSTPluginProp[str](_VSTPluginEventID.Name) """Factory name of the plugin.""" - num_inputs = _PluginDataProp[int]() - """Number of inputs the plugin supports.""" - - num_outputs = _PluginDataProp[int]() - """Number of outputs the plugin supports.""" + # num_inputs = _VSTPluginProp[int]() + # """Number of inputs the plugin supports.""" - pitch_bend = _PluginDataProp[int]() - """Pitch bend range sent to the plugin (in semitones).""" + # num_outputs = _VSTPluginProp[int]() + # """Number of outputs the plugin supports.""" - plugin_path = _PluginDataProp[str]() + plugin_path = _VSTPluginProp[str](_VSTPluginEventID.PluginPath) """The absolute path to the plugin binary.""" - state = _PluginDataProp[bytes]() + state = _VSTPluginProp[bytes](_VSTPluginEventID.State) """Plugin specific preset data blob.""" - vendor = _PluginDataProp[int]() + vendor = _VSTPluginProp[str](_VSTPluginEventID.Vendor) """Plugin developer (vendor) name.""" - vst_number = _PluginDataProp[int]() # TODO + # vst_number = _VSTPluginProp[int]() # TODO class BooBass(_PluginBase[BooBassEvent], _IPlugin, ModelReprMixin): """![](https://bit.ly/3Bk3aGK)""" INTERNAL_NAME = "BooBass" - bass = _PluginDataProp[int]() + bass = _NativePluginProp[int]() """Volume of the bass region. | Min | Max | Default | @@ -394,7 +651,7 @@ class BooBass(_PluginBase[BooBassEvent], _IPlugin, ModelReprMixin): | 0 | 65535 | 32767 | """ - high = _PluginDataProp[int]() + high = _NativePluginProp[int]() """Volume of the high region. | Min | Max | Default | @@ -402,7 +659,7 @@ class BooBass(_PluginBase[BooBassEvent], _IPlugin, ModelReprMixin): | 0 | 65535 | 32767 | """ - mid = _PluginDataProp[int]() + mid = _NativePluginProp[int]() """Volume of the mid region. | Min | Max | Default | @@ -415,7 +672,7 @@ class FruityBalance(_PluginBase[FruityBalanceEvent], _IPlugin, ModelReprMixin): """![](https://bit.ly/3RWItqU)""" INTERNAL_NAME = "Fruity Balance" - pan = _PluginDataProp[int]() + pan = _NativePluginProp[int]() """Linear. | Type | Value | Representation | @@ -425,7 +682,7 @@ class FruityBalance(_PluginBase[FruityBalanceEvent], _IPlugin, ModelReprMixin): | Default | 0 | Centred | """ - volume = _PluginDataProp[int]() + volume = _NativePluginProp[int]() """Logarithmic. | Type | Value | Representation | @@ -440,7 +697,7 @@ class FruityCenter(_PluginBase[FruityCenterEvent], _IPlugin, ModelReprMixin): """![](https://bit.ly/3TA9IIv)""" INTERNAL_NAME = "Fruity Center" - enabled = _PluginDataProp[bool]() + enabled = _NativePluginProp[bool]() """Removes DC offset if True; effectively behaving like a bypass button. Labelled as **Status** for some reason in the UI. @@ -451,8 +708,8 @@ class FruityFastDist(_PluginBase[FruityFastDistEvent], _IPlugin, ModelReprMixin) """![](https://bit.ly/3qT6Jil)""" INTERNAL_NAME = "Fruity Fast Dist" - kind = _PluginDataProp[Literal["A", "B"]]() - mix = _PluginDataProp[int]() + kind = _NativePluginProp[Literal["A", "B"]]() + mix = _NativePluginProp[int]() """Linear. Defaults to maximum value. | Type | Value | Mix (wet) | @@ -461,7 +718,7 @@ class FruityFastDist(_PluginBase[FruityFastDistEvent], _IPlugin, ModelReprMixin) | Max | 128 | 100% | """ - post = _PluginDataProp[int]() + post = _NativePluginProp[int]() """Linear. Defaults to maximum value. | Type | Value | Mix (wet) | @@ -470,7 +727,7 @@ class FruityFastDist(_PluginBase[FruityFastDistEvent], _IPlugin, ModelReprMixin) | Max | 128 | 100% | """ - pre = _PluginDataProp[int]() + pre = _NativePluginProp[int]() """Linear. | Type | Value | Percentage | @@ -480,7 +737,7 @@ class FruityFastDist(_PluginBase[FruityFastDistEvent], _IPlugin, ModelReprMixin) | Default | 128 | 67% | """ - threshold = _PluginDataProp[int]() + threshold = _NativePluginProp[int]() """Linear, Stepped. Defaults to maximum value. | Type | Value | Percentage | @@ -494,16 +751,16 @@ class FruityNotebook2(_PluginBase[FruityNotebook2Event], _IPlugin, ModelReprMixi """![](https://bit.ly/3RHa4g5)""" INTERNAL_NAME = "Fruity NoteBook 2" - active_page = _PluginDataProp[int]() + active_page = _NativePluginProp[int]() """Active page number of the notebook. Min: 0, Max: 100.""" - editable = _PluginDataProp[bool]() + editable = _NativePluginProp[bool]() """Whether the notebook is marked as editable or read-only. This attribute is just a visual marker used by FL Studio. """ - pages = _PluginDataProp[Dict[int, str]]() + pages = _NativePluginProp[Dict[int, str]]() """A dict of page numbers to their contents.""" @@ -511,7 +768,7 @@ class FruitySend(_PluginBase[FruitySendEvent], _IPlugin, ModelReprMixin): """![](https://bit.ly/3DqjvMu)""" INTERNAL_NAME = "Fruity Send" - dry = _PluginDataProp[int]() + dry = _NativePluginProp[int]() """Linear. Defaults to maximum value. | Type | Value | Mix (wet) | @@ -520,7 +777,7 @@ class FruitySend(_PluginBase[FruitySendEvent], _IPlugin, ModelReprMixin): | Max | 256 | 100% | """ - pan = _PluginDataProp[int]() + pan = _NativePluginProp[int]() """Linear. | Type | Value | Representation | @@ -530,10 +787,10 @@ class FruitySend(_PluginBase[FruitySendEvent], _IPlugin, ModelReprMixin): | Default | 0 | Centred | """ - send_to = _PluginDataProp[int]() + send_to = _NativePluginProp[int]() """Target insert index; depends on insert routing. Defaults to -1 (Master).""" - volume = _PluginDataProp[int]() + volume = _NativePluginProp[int]() """Logarithmic. | Type | Value | Representation | @@ -548,7 +805,7 @@ class FruitySoftClipper(_PluginBase[FruitySoftClipperEvent], _IPlugin, ModelRepr """![](https://bit.ly/3BCWfJX)""" INTERNAL_NAME = "Fruity Soft Clipper" - post = _PluginDataProp[int]() + post = _NativePluginProp[int]() """Linear. | Type | Value | Mix (wet) | @@ -558,7 +815,7 @@ class FruitySoftClipper(_PluginBase[FruitySoftClipperEvent], _IPlugin, ModelRepr | Default | 128 | 80% | """ - threshold = _PluginDataProp[int]() + threshold = _NativePluginProp[int]() """Logarithmic. | Type | Value | Representation | @@ -575,10 +832,10 @@ class FruityStereoEnhancer( """![](https://bit.ly/3DoHvji)""" INTERNAL_NAME = "Fruity Stereo Enhancer" - effect_position = _PluginDataProp[Literal["pre", "post"]]() + effect_position = _NativePluginProp[Literal["pre", "post"]]() """Defaults to ``post``.""" - pan = _PluginDataProp[int]() + pan = _NativePluginProp[int]() """Linear. | Type | Value | Representation | @@ -588,10 +845,10 @@ class FruityStereoEnhancer( | Default | 0 | Centred | """ - phase_inversion = _PluginDataProp[Literal["none", "left", "right"]]() + phase_inversion = _NativePluginProp[Literal["none", "left", "right"]]() """Default to ``None``.""" - phase_offset = _PluginDataProp[int]() + phase_offset = _NativePluginProp[int]() """Linear. | Type | Value | Representation | @@ -601,7 +858,7 @@ class FruityStereoEnhancer( | Default | 0 | No offset | """ - stereo_separation = _PluginDataProp[int]() + stereo_separation = _NativePluginProp[int]() """Linear. | Type | Value | Representation | @@ -611,7 +868,7 @@ class FruityStereoEnhancer( | Default | 0 | No effect | """ - volume = _PluginDataProp[int]() + volume = _NativePluginProp[int]() """Logarithmic. | Type | Value | Representation | @@ -626,7 +883,7 @@ class Soundgoodizer(_PluginBase[SoundgoodizerEvent], _IPlugin, ModelReprMixin): """![](https://bit.ly/3dip70y)""" INTERNAL_NAME = "Soundgoodizer" - amount = _PluginDataProp[int]() + amount = _NativePluginProp[int]() """Logarithmic. | Min | Max | Default | @@ -634,7 +891,7 @@ class Soundgoodizer(_PluginBase[SoundgoodizerEvent], _IPlugin, ModelReprMixin): | 0 | 1000 | 600 | """ - mode = _PluginDataProp[Literal["A", "B", "C", "D"]]() + mode = _NativePluginProp[Literal["A", "B", "C", "D"]]() """4 preset modes (A, B, C and D). Defaults to ``A``.""" diff --git a/tests/assets/plugins/fruity-wrapper.fst b/tests/assets/plugins/fruity-wrapper.fst new file mode 100644 index 0000000000000000000000000000000000000000..2699a8106ee097d7a83bd3ca3df73c8a9ae37066 GIT binary patch literal 1519 zcmb_cO=}ZT6g@9ZOVuv2sA54&7K*!&rk_Z`(n&|DgS4bg5wdV-q75XSFf%E1Aq?nB z1$E)ZiZ1;FqCY`g3W`{9SNASlSI?b!laA>sniKB3-}l{ja}(3rsty6jLg5xntLlFp z1m2Gn^0~R(RIV^RJA+r5lfXH3Q7r(cQy<1nxM-seAG^%kpk|}RPmL1rX^KaPl;*4C zO3N{ON8uT1f%6d%*FimN=ji#G!_hPUw; zr<~D2!jfHNsgH=U3kB+N&TyAco)-J8wNlwK8b!xxZ8n^Cb%s7^Qeef#St=8u2{0~K zR$S+fYd5oIy;1WjhSO|1+gd4?)+Q^RyxflGPtGy%lui~;)AszD>rK<->X!%;S|ACM z4~#6moie;Lx=7jcH>k=CLW{AveVy{5xoB*Wr!2!NV(5QwWTpP#cJF1D*NvqWx4vWh zwW96YP-=0vWqTfl0i4(Mjs<;;)>1EDnqf~!4*~>uD5JUw*a4OTl%cL7T>_Q?PR;_> zF?^3}4wF;Hx3jXqnM?Yf#2|k}H^?R?@IBnFUlRA_rTR^XlWHi>hmpY*mhYnI>kH%A zY(9)rc+H==7PWB160LNM@_Rd!B1Fg75q|Q{2;x`jzG7oC7Ce>XAX62Vz(FQz_{g+h zjAe<<-iRpQ24oJR3u2$hZv#31#(pua4RNGo_EKe!59n7fac}?Hi{$sGm&fv-I*LC? CSfT0w literal 0 HcmV?d00001 diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 83313f4..eab1512 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -90,3 +90,32 @@ def test_vst_plugin(plugin: PluginFixture[VSTPlugin]): djmfilter.plugin_path == r"C:\Program Files\Common Files\VST2\Xfer Records\DJMFilter_x64.dll" ) + + +def test_fruity_wrapper(plugin: PluginFixture[VSTPlugin]): + wrapper = plugin("fruity-wrapper.fst", VSTPlugin) + + # VSTPluginEvent properties + assert wrapper.automation.notify_changes + assert wrapper.compatibility.buffers_maxsize + assert wrapper.compatibility.fast_idle + assert not wrapper.compatibility.fixed_buffers + assert wrapper.compatibility.process_maximum + assert wrapper.compatibility.reset_on_transport + assert wrapper.compatibility.send_loop + assert not wrapper.compatibility.use_time_offset + assert wrapper.midi.input == 6 + assert wrapper.midi.output == 9 + assert wrapper.midi.pb_range == 36 + assert not wrapper.midi.send_modx + assert not wrapper.midi.send_pb + assert wrapper.midi.send_release + assert wrapper.processing.allow_sd + assert not wrapper.processing.bridged + assert wrapper.processing.keep_state + assert wrapper.processing.multithreaded + assert wrapper.processing.notify_render + assert wrapper.ui.accept_drop + assert not wrapper.ui.always_update + assert wrapper.ui.dpi_aware + assert not wrapper.ui.scale_editor diff --git a/tox.ini b/tox.ini index 0fa3895..849bea1 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ commands = [flake8] exclude = .tox,*.egg,build,data,venv,docs,main.py -extend-ignore = N818, D107, D101, D102, D105, D415, E203 +extend-ignore = N818, D107, D101, D102, D105, D106, D415, E203 per-file-ignores = _*.py: D205, D212 tests/*.py: D, E501