From ebd1d05947c172a45fef180221a320a2d24431db Mon Sep 17 00:00:00 2001 From: Miguel Guthridge Date: Mon, 8 Apr 2024 23:06:36 +1000 Subject: [PATCH] Fix incorrect use of modules --- src/fl_classes/__fl_midi_msg.py | 650 +++++++++++++++++++++++++ src/fl_classes/__init__.py | 633 +----------------------- src/launchMapPages/__init__.py | 240 ++------- src/launchMapPages/__launchMapPages.py | 225 +++++++++ src/screen/__init__.py | 275 +++-------- src/screen/__screen.py | 230 +++++++++ src/utils/__init__.py | 581 +++------------------- src/utils/__utils.py | 544 +++++++++++++++++++++ 8 files changed, 1802 insertions(+), 1576 deletions(-) create mode 100644 src/fl_classes/__fl_midi_msg.py create mode 100644 src/launchMapPages/__launchMapPages.py create mode 100644 src/screen/__screen.py create mode 100644 src/utils/__utils.py diff --git a/src/fl_classes/__fl_midi_msg.py b/src/fl_classes/__fl_midi_msg.py new file mode 100644 index 0000000..8c07607 --- /dev/null +++ b/src/fl_classes/__fl_midi_msg.py @@ -0,0 +1,650 @@ +""" +fl_classes + +This module contains definitions for FL Studio's built-in types, which can be +used to assist with type hinting in your project. + +NOTE: This module is not included in FL Studio's runtime. + +```py +try: + from fl_classes import FlMidiMsg +except ImportError: + FlMidiMsg = 'FlMidiMsg' + +def OnMidiIn(event: FlMidiMsg) -> None: + ... +``` +""" +from typing import Optional, overload +from typing_extensions import TypeGuard + + +class FlMidiMsg: + """ + A shadow of FL Studio's `FlMidiMsg` object. Note that although creating + these is possible, it should be avoided during runtime as FL Studio's API + won't accept it as an argument for any of its functions. It can be used + within a testing environment, however. + + Note that two sub-types are also included, which allow for type narrowing + by separating standard MIDI events and Sysex MIDI events. These will work + for FL Studio's types as well. + + * `isMidiMsgStandard()` + + * `isMidiMsgSysex()` + + Basic type checking is performed when accessing properties of `FlMidiMsg` + objects, to ensure that incorrect properties aren't accessed (for example + accessing `data1` for a sysex event). These checks won't be performed + during runtime for your script, but can help to add more certainty to your + tests. + """ + + @overload + def __init__( + self, + status_sysex: int, + data1: int, + data2: int, + ) -> None: + ... + + @overload + def __init__( + self, + status_sysex: 'list[int] | bytes', + ) -> None: + ... + + def __init__( + self, + status_sysex: 'int | list[int] | bytes', + data1: Optional[int] = None, + data2: Optional[int] = None, + pmeFlags: int = 0b101110, + ) -> None: + """ + Create an `FlMidiMsg` object. + + Note that this object will be incompatible with FL Studio's API, and so + cannot be used as a parameter for any API functions during runtime. + + ### Args: + * `status_sysex` (`int | list[int] | bytes`): status byte or sysex data + + * `data1` (`Optional[int]`, optional): data1 byte if applicable. + Defaults to `None`. + + * `data2` (`Optional[int]`, optional): data2 byte if applicable. + Defaults to `None`. + + * `pmeFlags` (`int`, optional): PME flags of event. Defaults to + `PME_System | PME_System_Safe | PME_PreviewNote | PME_FromMIDI`. + + ### Example Usage + + ```py + # Create a note on event on middle C + msg = FlMidiMsg(0x90, 0x3C, 0x7F) + + # Create a CC#10 event + msg = FlMidiMsg(0xB0, 0x0A, 0x00) + + # Create a sysex event for a universal device enquiry + msg = FlMidiMsg([0xF0, 0x7E, 0x7F, 0x06, 0x01, 0xF7]) + ``` + """ + if isinstance(status_sysex, int): + if data1 is None: + raise TypeError( + "data1 value cannot be None for standard events") + if data2 is None: + raise TypeError( + "data2 value cannot be None for standard events") + self.__status = status_sysex + self.__sysex: Optional[bytes] = None + else: + if data1 is not None: + raise TypeError( + "data1 value must be None for sysex events") + if data2 is not None: + raise TypeError( + "data2 value must be None for sysex events") + self.__sysex = bytes(status_sysex) + self.__status = 0xF0 + self.__data1 = data1 + self.__data2 = data2 + + self.__timestamp = 0 + self.__handled = False + self.__port = 0 + self.__pitch_bend = 1 + self.__is_increment = False + self.__res = 0.0 + self.__in_ev = 0 + self.__out_ev = 0 + self.__midi_id = 0 + self.__midi_chan = 0 + self.__midi_chan_ex = 0 + self.__pme_flags = pmeFlags + + def __repr__(self) -> str: + if self.__sysex is not None: + return f"FlMidiMsg([{', '.join(f'0x{b:02X}' for b in self.sysex)})" + else: + return ( + f"FlMidiMsg(" + f"0x{self.status:02X}, " + f"0x{self.data1:02X}, " + f"0x{self.data2:02X})" + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, FlMidiMsg): + if (isMidiMsgStandard(self) and isMidiMsgStandard(other)): + return all([ + self.status == other.status + and self.data1 == other.data1 + and self.data2 == other.data2 + ]) + elif (isMidiMsgSysex(self) and isMidiMsgSysex(other)): + return self.sysex == other.sysex + elif isinstance(other, int): + if isMidiMsgStandard(self): + return eventToRawData(self) == other + elif isinstance(other, bytes): + if isMidiMsgSysex(self): + return eventToRawData(self) == other + return False + + @staticmethod + def __standard_check(value: Optional[int], prop: str) -> int: + """Check that it's a standard event, then return the value""" + if value is None: + raise ValueError( + f"Attempt to access {prop} on sysex event. " + f"Are you type narrowing your events correctly?" + ) + return value + + @staticmethod + def __range_check(value: int, prop: str) -> int: + """Check that the value is within the allowed range, then return it""" + if value < 0: + raise ValueError(f"Attempt to set {prop} to {value} (< 0)") + if value > 0x7F: + raise ValueError(f"Attempt to set {prop} to {value} (> 0x7F)") + return value + + @property + def handled(self) -> bool: + """Whether the event is considered to be handled by FL Studio. + + If this is set to `True`, the event will stop propagating after this + particular callback returns. + + You script should set it when an event is processed successfully. + """ + return self.__handled + + @handled.setter + def handled(self, handled: bool) -> None: + self.__handled = handled + + @property + def timestamp(self) -> int: + """The timestamp of the event + + ### HELP WANTED: + * This seems to only ever be zero. I can't determine what it is for. If + you know how it is used, create a pull request with details. + + This value is read-only. + """ + return self.__timestamp + + @property + def status(self) -> int: + """The status byte of the event + + This can be used to determine the type of MIDI event using the upper + nibble, and the channel of the event using the lower nibble. + + ```py + e_type = event.status & 0xF0 + channel = event.status & 0xF + ``` + + Note that for sysex messages, this property is `0xF0`. Other standard + event properties are inaccessible. + + ## Event types + * `0x8` Note off (`data1` is note number, `data2` is release value) + + * `0x9` Note on (`data1` is note number, `data2` is velocity) + + * `0xA` Note after-touch (`data1` is note number, `data2` is pressure + value) + + * `0xB` Control change (CC, `data1` is control number as per your + controller's documentation, `data2` is value) + + * `0xC` Program change (used to assign instrument selection, `data1` is + instrument number) + + * `0xD` Channel after-touch (`data1` is value, `data2` is unused) + + * `0xE` Pitch bend (`data1` and `data2` are value, as per the formula + `data1 + (data2 << 7)`, yielding a range of `0` - `16384`) + """ + return self.__status + + @status.setter + def status(self, status: int) -> None: + self.__status = self.__range_check(status, "status") + + @property + def data1(self) -> int: + """The first data byte of a MIDI message. + + This is used to determine the control number for CC events, the note + number for note events, and various other values. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data1, "data1") + + @data1.setter + def data1(self, data1: int) -> None: + self.__data1 = self.__range_check(data1, "data1") + + @property + def data2(self) -> int: + """The second data byte of a MIDI message. + + This is used to determine the value for CC events, the velocity for + note events, and various other values. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data2, "data2") + + @data2.setter + def data2(self, data2: int) -> None: + self.__data2 = self.__range_check(data2, "data2") + + @property + def port(self) -> int: + """The port of the message + + ### HELP WANTED: + * This value always appears to be zero. How should it be used? + + Note that this property is read-only. + """ + return self.__port + + @property + def note(self) -> int: + """The note number of a MIDI note on/off message. + + This is a shadow of the `data1` property. Modifications to this will + affect all `data1` derived properties. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data1, "note") + + @note.setter + def note(self, note: int) -> None: + self.__data1 = self.__range_check(note, "note") + + @property + def velocity(self) -> int: + """The velocity of a MIDI note on/off message. + + This is a shadow of the `data2` property. Modifications to this will + affect all `data2` derived properties + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data2, "velocity") + + @velocity.setter + def velocity(self, velocity: int) -> None: + self.__data2 = self.__range_check(velocity, "velocity") + + @property + def pressure(self) -> int: + """The pressure value for a channel after-touch event. + + This is a shadow of the `data1` property. Modifications to this will + affect all `data1` derived properties. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data1, "pressure") + + @pressure.setter + def pressure(self, pressure: int) -> None: + self.__data1 = self.__range_check(pressure, "pressure") + + @property + def progNum(self) -> int: + """The instrument number for a program change event. + + This is a shadow of the `data1` property. Modifications to this will + affect all `data1` derived properties. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data1, "progNum") + + @progNum.setter + def progNum(self, progNum: int) -> None: + self.__data1 = self.__range_check(progNum, "progNum") + + @property + def controlNum(self) -> int: + """The control number for a control change event. + + This is a shadow of the `data1` property. Modifications to this will + affect all `data1` derived properties. + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data1, "controlNum") + + @controlNum.setter + def controlNum(self, controlNum: int) -> None: + self.__data1 = self.__range_check(controlNum, "controlNum") + + @property + def controlVal(self) -> int: + """The value of a control change event. + + This is a shadow of the `data2` property. Modifications to this will + affect all `data2` derived properties + + Note that this property is inaccessible for sysex events. + """ + return self.__standard_check(self.__data2, "controlVal") + + @controlVal.setter + def controlVal(self, controlVal: int) -> None: + self.__data2 = self.__range_check(controlVal, "controlVal") + + @property + def pitchBend(self) -> int: + """MIDI pitch bend value + + ### HELP WANTED: + * This only ever seems to equal `1`. How should it be used? + + Note that this property is read-only. + """ + return self.__pitch_bend + + @property + def sysex(self) -> bytes: + """Data for a sysex event + + Contains the full event data from sysex events. + + This property is inaccessible for standard events. + """ + if self.__sysex is None: + raise ValueError( + "Attempt to access sysex data on standard event. " + "Are you type narrowing your events correctly?" + ) + return self.__sysex + + @sysex.setter + def sysex(self, sysex: bytes) -> None: + if len(sysex) == 0: + raise ValueError("New sysex data has length of zero") + if sysex[0] != 0xF0: + raise ValueError("New sysex data doesn't first value of 0xF0") + self.__sysex = sysex + + @property + def isIncrement(self) -> bool: + """Whether the event should be an increment event + + If the script sets this to `True`, FL Studio will consider it to be a + relative event, meaning that it will change values relative to that + value, rather than setting them absolutely. + + ### HELP WANTED: + * Notes on the particular cases where this happens. + """ + return self.__is_increment + + @isIncrement.setter + def isIncrement(self, isIncrement: bool) -> None: + self.__is_increment = isIncrement + + @property + def res(self) -> float: + """MIDI res + + ### HELP WANTED: + * How is this used? + """ + return self.__res + + @res.setter + def res(self, res: float) -> None: + self.__res = res + + @property + def inEv(self) -> int: + """MIDI inEv + + ### HELP WANTED: + * What is this? + """ + return self.__in_ev + + @inEv.setter + def inEv(self, inEv: int) -> None: + self.__in_ev = inEv + + @property + def outEv(self) -> int: + """MIDI outEv + + ### HELP WANTED: + * What is this? + """ + return self.__out_ev + + @outEv.setter + def outEv(self, outEv: int) -> None: + self.__out_ev = outEv + + @property + def midiId(self) -> int: + """MIDI ID + + ### HELP WANTED: + * What is this? + """ + return self.__midi_id + + @midiId.setter + def midiId(self, midiId: int) -> None: + self.__midi_id = midiId + + @property + def midiChan(self) -> int: + """MIDI chan + + ### HELP WANTED: + * What is this? + + * No, it's not a channel. It always seems to be zero, regardless of the + channel of the event. + """ + return self.__midi_chan + + @midiChan.setter + def midiChan(self, midiChan: int) -> None: + self.__midi_chan = midiChan + + @property + def midiChanEx(self) -> int: + """MIDI chanEx + + ### HELP WANTED: + * What is this? + """ + return self.__midi_chan_ex + + @midiChanEx.setter + def midiChanEx(self, midiChanEx: int) -> None: + self.__midi_chan_ex = midiChanEx + + @property + def pmeFlags(self) -> int: + """Flags used by FL Studio to indicate the permissions of the script in + the current environment. + + These can be used to ensure safety while running the script. If a + script ever attempts to execute unsafe behavior, a `TypeError` will be + raised. + + ```py + TypeError("Operation unsafe at current time") + ``` + + ## Flag analysis + + The flags can be analyzed by performing bitwise operations to determine + the current permissions of the script. + + * `0b000010` (`PME_System`) System operations allowed (play/pause, + etc). + + * `0b000100` (`PME_System_Safe`) Critical operations allowed (add + markers, etc). Things that can't be done when a modal dialog is + showing. + + * `0b001000` (`PME_PreviewNote`) Note events will trigger a preview. + + * `0x010000` (`PME_FromHost`) FL Studio is being hosted as a VSTi + (meaning it's probably a bad idea to do anything meaningful as it + could interfere with the behavior of other DAWs). In my testing, + using MIDI scripts in the FL Studio VST causes a crash anyway, so I + suppose it isn't that important either way. + + * `0x100000` This event was triggered by a MIDI event. + + ## Alternate to flag analysis + + It could be considered to be more Pythonic, as well as much simpler to + catch this exception rather than checking the flags. The following is + a simple decorator that will catch the exception. This does come with + the risk that any unsafe behavior that FL Studio misses will cause + a system lock-up in FL Studio. + + ```py + def catchUnsafeOperation(func): + ''' + Decorator to prevent exceptions due to unsafe operations + + ### Args: + * `func` (`Callable`): function to decorate + ''' + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except TypeError as e: + if e.args != ("Operation unsafe at current time",): + raise e + return wrapper + ``` + """ + return self.__pme_flags + + +class StandardMidiMsg(FlMidiMsg): + """ + An FlMidiMsg object which has been type narrowed to a StandardFlMidiMsg. + + Note that as FL Studio events are actually of a different type to these + shadow types, you should never use the `isinstance` function in order to + perform type-narrowing operations, as it will lead to very obscure bugs + when your type checks never work inside FL Studio, even if they work in + your tests. + + Instead, you can type narrow to a `StandardFlMidiMsg` object using the + `isMidiMsgStandard()` function. + """ + + def __init__(self, status: int, data1: int, data2: int) -> None: + super().__init__(status, data1, data2) + + +class SysexMidiMsg(FlMidiMsg): + """ + An FlMidiMsg object which has been type narrowed to a SysexFlMidiMsg. + + Note that as FL Studio events are actually of a different type to these + shadow types, you should never use the `isinstance` function in order to + perform type-narrowing operations, as it will lead to very obscure bugs + when your type checks never work inside FL Studio, even if they work in + your tests. + + Instead, you can type narrow to a `SysexFlMidiMsg` object using the + `isMidiMsgSysex()` function. + """ + + def __init__(self, sysex: list[int]) -> None: + super().__init__(sysex) + + +def isMidiMsgStandard(event: FlMidiMsg) -> 'TypeGuard[StandardMidiMsg]': + """ + Returns whether an event is a standard event + + ### Args: + * `event` (`FlMidiMsg`): event to check + + ### Returns: + * `TypeGuard[SysexFlMidiMsg]`: type guarded event + """ + return not isMidiMsgSysex(event) + + +def isMidiMsgSysex(event: FlMidiMsg) -> 'TypeGuard[SysexMidiMsg]': + """ + Returns whether an event is a sysex event + + ### Args: + * `event` (`FlMidiMsg`): event to check + + ### Returns: + * `TypeGuard[SysexFlMidiMsg]`: type guarded event + """ + return event.status == 0xF0 + + +def eventToRawData(event: FlMidiMsg) -> 'int | bytes': + """ + Convert event to raw data. + + For standard events data is presented as little-endian, meaning that the + status byte has the lowest component value in the integer. + + ### Returns: + * `int | bytes`: data + """ + if isMidiMsgStandard(event): + return (event.status) + (event.data1 << 8) + (event.data2 << 16) + else: + assert isMidiMsgSysex(event) + return event.sysex diff --git a/src/fl_classes/__init__.py b/src/fl_classes/__init__.py index 8c07607..44b5203 100644 --- a/src/fl_classes/__init__.py +++ b/src/fl_classes/__init__.py @@ -16,635 +16,6 @@ def OnMidiIn(event: FlMidiMsg) -> None: ... ``` """ -from typing import Optional, overload -from typing_extensions import TypeGuard +from .__fl_midi_msg import FlMidiMsg - -class FlMidiMsg: - """ - A shadow of FL Studio's `FlMidiMsg` object. Note that although creating - these is possible, it should be avoided during runtime as FL Studio's API - won't accept it as an argument for any of its functions. It can be used - within a testing environment, however. - - Note that two sub-types are also included, which allow for type narrowing - by separating standard MIDI events and Sysex MIDI events. These will work - for FL Studio's types as well. - - * `isMidiMsgStandard()` - - * `isMidiMsgSysex()` - - Basic type checking is performed when accessing properties of `FlMidiMsg` - objects, to ensure that incorrect properties aren't accessed (for example - accessing `data1` for a sysex event). These checks won't be performed - during runtime for your script, but can help to add more certainty to your - tests. - """ - - @overload - def __init__( - self, - status_sysex: int, - data1: int, - data2: int, - ) -> None: - ... - - @overload - def __init__( - self, - status_sysex: 'list[int] | bytes', - ) -> None: - ... - - def __init__( - self, - status_sysex: 'int | list[int] | bytes', - data1: Optional[int] = None, - data2: Optional[int] = None, - pmeFlags: int = 0b101110, - ) -> None: - """ - Create an `FlMidiMsg` object. - - Note that this object will be incompatible with FL Studio's API, and so - cannot be used as a parameter for any API functions during runtime. - - ### Args: - * `status_sysex` (`int | list[int] | bytes`): status byte or sysex data - - * `data1` (`Optional[int]`, optional): data1 byte if applicable. - Defaults to `None`. - - * `data2` (`Optional[int]`, optional): data2 byte if applicable. - Defaults to `None`. - - * `pmeFlags` (`int`, optional): PME flags of event. Defaults to - `PME_System | PME_System_Safe | PME_PreviewNote | PME_FromMIDI`. - - ### Example Usage - - ```py - # Create a note on event on middle C - msg = FlMidiMsg(0x90, 0x3C, 0x7F) - - # Create a CC#10 event - msg = FlMidiMsg(0xB0, 0x0A, 0x00) - - # Create a sysex event for a universal device enquiry - msg = FlMidiMsg([0xF0, 0x7E, 0x7F, 0x06, 0x01, 0xF7]) - ``` - """ - if isinstance(status_sysex, int): - if data1 is None: - raise TypeError( - "data1 value cannot be None for standard events") - if data2 is None: - raise TypeError( - "data2 value cannot be None for standard events") - self.__status = status_sysex - self.__sysex: Optional[bytes] = None - else: - if data1 is not None: - raise TypeError( - "data1 value must be None for sysex events") - if data2 is not None: - raise TypeError( - "data2 value must be None for sysex events") - self.__sysex = bytes(status_sysex) - self.__status = 0xF0 - self.__data1 = data1 - self.__data2 = data2 - - self.__timestamp = 0 - self.__handled = False - self.__port = 0 - self.__pitch_bend = 1 - self.__is_increment = False - self.__res = 0.0 - self.__in_ev = 0 - self.__out_ev = 0 - self.__midi_id = 0 - self.__midi_chan = 0 - self.__midi_chan_ex = 0 - self.__pme_flags = pmeFlags - - def __repr__(self) -> str: - if self.__sysex is not None: - return f"FlMidiMsg([{', '.join(f'0x{b:02X}' for b in self.sysex)})" - else: - return ( - f"FlMidiMsg(" - f"0x{self.status:02X}, " - f"0x{self.data1:02X}, " - f"0x{self.data2:02X})" - ) - - def __eq__(self, other: object) -> bool: - if isinstance(other, FlMidiMsg): - if (isMidiMsgStandard(self) and isMidiMsgStandard(other)): - return all([ - self.status == other.status - and self.data1 == other.data1 - and self.data2 == other.data2 - ]) - elif (isMidiMsgSysex(self) and isMidiMsgSysex(other)): - return self.sysex == other.sysex - elif isinstance(other, int): - if isMidiMsgStandard(self): - return eventToRawData(self) == other - elif isinstance(other, bytes): - if isMidiMsgSysex(self): - return eventToRawData(self) == other - return False - - @staticmethod - def __standard_check(value: Optional[int], prop: str) -> int: - """Check that it's a standard event, then return the value""" - if value is None: - raise ValueError( - f"Attempt to access {prop} on sysex event. " - f"Are you type narrowing your events correctly?" - ) - return value - - @staticmethod - def __range_check(value: int, prop: str) -> int: - """Check that the value is within the allowed range, then return it""" - if value < 0: - raise ValueError(f"Attempt to set {prop} to {value} (< 0)") - if value > 0x7F: - raise ValueError(f"Attempt to set {prop} to {value} (> 0x7F)") - return value - - @property - def handled(self) -> bool: - """Whether the event is considered to be handled by FL Studio. - - If this is set to `True`, the event will stop propagating after this - particular callback returns. - - You script should set it when an event is processed successfully. - """ - return self.__handled - - @handled.setter - def handled(self, handled: bool) -> None: - self.__handled = handled - - @property - def timestamp(self) -> int: - """The timestamp of the event - - ### HELP WANTED: - * This seems to only ever be zero. I can't determine what it is for. If - you know how it is used, create a pull request with details. - - This value is read-only. - """ - return self.__timestamp - - @property - def status(self) -> int: - """The status byte of the event - - This can be used to determine the type of MIDI event using the upper - nibble, and the channel of the event using the lower nibble. - - ```py - e_type = event.status & 0xF0 - channel = event.status & 0xF - ``` - - Note that for sysex messages, this property is `0xF0`. Other standard - event properties are inaccessible. - - ## Event types - * `0x8` Note off (`data1` is note number, `data2` is release value) - - * `0x9` Note on (`data1` is note number, `data2` is velocity) - - * `0xA` Note after-touch (`data1` is note number, `data2` is pressure - value) - - * `0xB` Control change (CC, `data1` is control number as per your - controller's documentation, `data2` is value) - - * `0xC` Program change (used to assign instrument selection, `data1` is - instrument number) - - * `0xD` Channel after-touch (`data1` is value, `data2` is unused) - - * `0xE` Pitch bend (`data1` and `data2` are value, as per the formula - `data1 + (data2 << 7)`, yielding a range of `0` - `16384`) - """ - return self.__status - - @status.setter - def status(self, status: int) -> None: - self.__status = self.__range_check(status, "status") - - @property - def data1(self) -> int: - """The first data byte of a MIDI message. - - This is used to determine the control number for CC events, the note - number for note events, and various other values. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data1, "data1") - - @data1.setter - def data1(self, data1: int) -> None: - self.__data1 = self.__range_check(data1, "data1") - - @property - def data2(self) -> int: - """The second data byte of a MIDI message. - - This is used to determine the value for CC events, the velocity for - note events, and various other values. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data2, "data2") - - @data2.setter - def data2(self, data2: int) -> None: - self.__data2 = self.__range_check(data2, "data2") - - @property - def port(self) -> int: - """The port of the message - - ### HELP WANTED: - * This value always appears to be zero. How should it be used? - - Note that this property is read-only. - """ - return self.__port - - @property - def note(self) -> int: - """The note number of a MIDI note on/off message. - - This is a shadow of the `data1` property. Modifications to this will - affect all `data1` derived properties. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data1, "note") - - @note.setter - def note(self, note: int) -> None: - self.__data1 = self.__range_check(note, "note") - - @property - def velocity(self) -> int: - """The velocity of a MIDI note on/off message. - - This is a shadow of the `data2` property. Modifications to this will - affect all `data2` derived properties - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data2, "velocity") - - @velocity.setter - def velocity(self, velocity: int) -> None: - self.__data2 = self.__range_check(velocity, "velocity") - - @property - def pressure(self) -> int: - """The pressure value for a channel after-touch event. - - This is a shadow of the `data1` property. Modifications to this will - affect all `data1` derived properties. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data1, "pressure") - - @pressure.setter - def pressure(self, pressure: int) -> None: - self.__data1 = self.__range_check(pressure, "pressure") - - @property - def progNum(self) -> int: - """The instrument number for a program change event. - - This is a shadow of the `data1` property. Modifications to this will - affect all `data1` derived properties. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data1, "progNum") - - @progNum.setter - def progNum(self, progNum: int) -> None: - self.__data1 = self.__range_check(progNum, "progNum") - - @property - def controlNum(self) -> int: - """The control number for a control change event. - - This is a shadow of the `data1` property. Modifications to this will - affect all `data1` derived properties. - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data1, "controlNum") - - @controlNum.setter - def controlNum(self, controlNum: int) -> None: - self.__data1 = self.__range_check(controlNum, "controlNum") - - @property - def controlVal(self) -> int: - """The value of a control change event. - - This is a shadow of the `data2` property. Modifications to this will - affect all `data2` derived properties - - Note that this property is inaccessible for sysex events. - """ - return self.__standard_check(self.__data2, "controlVal") - - @controlVal.setter - def controlVal(self, controlVal: int) -> None: - self.__data2 = self.__range_check(controlVal, "controlVal") - - @property - def pitchBend(self) -> int: - """MIDI pitch bend value - - ### HELP WANTED: - * This only ever seems to equal `1`. How should it be used? - - Note that this property is read-only. - """ - return self.__pitch_bend - - @property - def sysex(self) -> bytes: - """Data for a sysex event - - Contains the full event data from sysex events. - - This property is inaccessible for standard events. - """ - if self.__sysex is None: - raise ValueError( - "Attempt to access sysex data on standard event. " - "Are you type narrowing your events correctly?" - ) - return self.__sysex - - @sysex.setter - def sysex(self, sysex: bytes) -> None: - if len(sysex) == 0: - raise ValueError("New sysex data has length of zero") - if sysex[0] != 0xF0: - raise ValueError("New sysex data doesn't first value of 0xF0") - self.__sysex = sysex - - @property - def isIncrement(self) -> bool: - """Whether the event should be an increment event - - If the script sets this to `True`, FL Studio will consider it to be a - relative event, meaning that it will change values relative to that - value, rather than setting them absolutely. - - ### HELP WANTED: - * Notes on the particular cases where this happens. - """ - return self.__is_increment - - @isIncrement.setter - def isIncrement(self, isIncrement: bool) -> None: - self.__is_increment = isIncrement - - @property - def res(self) -> float: - """MIDI res - - ### HELP WANTED: - * How is this used? - """ - return self.__res - - @res.setter - def res(self, res: float) -> None: - self.__res = res - - @property - def inEv(self) -> int: - """MIDI inEv - - ### HELP WANTED: - * What is this? - """ - return self.__in_ev - - @inEv.setter - def inEv(self, inEv: int) -> None: - self.__in_ev = inEv - - @property - def outEv(self) -> int: - """MIDI outEv - - ### HELP WANTED: - * What is this? - """ - return self.__out_ev - - @outEv.setter - def outEv(self, outEv: int) -> None: - self.__out_ev = outEv - - @property - def midiId(self) -> int: - """MIDI ID - - ### HELP WANTED: - * What is this? - """ - return self.__midi_id - - @midiId.setter - def midiId(self, midiId: int) -> None: - self.__midi_id = midiId - - @property - def midiChan(self) -> int: - """MIDI chan - - ### HELP WANTED: - * What is this? - - * No, it's not a channel. It always seems to be zero, regardless of the - channel of the event. - """ - return self.__midi_chan - - @midiChan.setter - def midiChan(self, midiChan: int) -> None: - self.__midi_chan = midiChan - - @property - def midiChanEx(self) -> int: - """MIDI chanEx - - ### HELP WANTED: - * What is this? - """ - return self.__midi_chan_ex - - @midiChanEx.setter - def midiChanEx(self, midiChanEx: int) -> None: - self.__midi_chan_ex = midiChanEx - - @property - def pmeFlags(self) -> int: - """Flags used by FL Studio to indicate the permissions of the script in - the current environment. - - These can be used to ensure safety while running the script. If a - script ever attempts to execute unsafe behavior, a `TypeError` will be - raised. - - ```py - TypeError("Operation unsafe at current time") - ``` - - ## Flag analysis - - The flags can be analyzed by performing bitwise operations to determine - the current permissions of the script. - - * `0b000010` (`PME_System`) System operations allowed (play/pause, - etc). - - * `0b000100` (`PME_System_Safe`) Critical operations allowed (add - markers, etc). Things that can't be done when a modal dialog is - showing. - - * `0b001000` (`PME_PreviewNote`) Note events will trigger a preview. - - * `0x010000` (`PME_FromHost`) FL Studio is being hosted as a VSTi - (meaning it's probably a bad idea to do anything meaningful as it - could interfere with the behavior of other DAWs). In my testing, - using MIDI scripts in the FL Studio VST causes a crash anyway, so I - suppose it isn't that important either way. - - * `0x100000` This event was triggered by a MIDI event. - - ## Alternate to flag analysis - - It could be considered to be more Pythonic, as well as much simpler to - catch this exception rather than checking the flags. The following is - a simple decorator that will catch the exception. This does come with - the risk that any unsafe behavior that FL Studio misses will cause - a system lock-up in FL Studio. - - ```py - def catchUnsafeOperation(func): - ''' - Decorator to prevent exceptions due to unsafe operations - - ### Args: - * `func` (`Callable`): function to decorate - ''' - def wrapper(*args, **kwargs): - try: - func(*args, **kwargs) - except TypeError as e: - if e.args != ("Operation unsafe at current time",): - raise e - return wrapper - ``` - """ - return self.__pme_flags - - -class StandardMidiMsg(FlMidiMsg): - """ - An FlMidiMsg object which has been type narrowed to a StandardFlMidiMsg. - - Note that as FL Studio events are actually of a different type to these - shadow types, you should never use the `isinstance` function in order to - perform type-narrowing operations, as it will lead to very obscure bugs - when your type checks never work inside FL Studio, even if they work in - your tests. - - Instead, you can type narrow to a `StandardFlMidiMsg` object using the - `isMidiMsgStandard()` function. - """ - - def __init__(self, status: int, data1: int, data2: int) -> None: - super().__init__(status, data1, data2) - - -class SysexMidiMsg(FlMidiMsg): - """ - An FlMidiMsg object which has been type narrowed to a SysexFlMidiMsg. - - Note that as FL Studio events are actually of a different type to these - shadow types, you should never use the `isinstance` function in order to - perform type-narrowing operations, as it will lead to very obscure bugs - when your type checks never work inside FL Studio, even if they work in - your tests. - - Instead, you can type narrow to a `SysexFlMidiMsg` object using the - `isMidiMsgSysex()` function. - """ - - def __init__(self, sysex: list[int]) -> None: - super().__init__(sysex) - - -def isMidiMsgStandard(event: FlMidiMsg) -> 'TypeGuard[StandardMidiMsg]': - """ - Returns whether an event is a standard event - - ### Args: - * `event` (`FlMidiMsg`): event to check - - ### Returns: - * `TypeGuard[SysexFlMidiMsg]`: type guarded event - """ - return not isMidiMsgSysex(event) - - -def isMidiMsgSysex(event: FlMidiMsg) -> 'TypeGuard[SysexMidiMsg]': - """ - Returns whether an event is a sysex event - - ### Args: - * `event` (`FlMidiMsg`): event to check - - ### Returns: - * `TypeGuard[SysexFlMidiMsg]`: type guarded event - """ - return event.status == 0xF0 - - -def eventToRawData(event: FlMidiMsg) -> 'int | bytes': - """ - Convert event to raw data. - - For standard events data is presented as little-endian, meaning that the - status byte has the lowest component value in the integer. - - ### Returns: - * `int | bytes`: data - """ - if isMidiMsgStandard(event): - return (event.status) + (event.data1 << 8) + (event.data2 << 16) - else: - assert isMidiMsgSysex(event) - return event.sysex +__all__ = ['FlMidiMsg'] diff --git a/src/launchMapPages/__init__.py b/src/launchMapPages/__init__.py index 5069e10..b481e81 100644 --- a/src/launchMapPages/__init__.py +++ b/src/launchMapPages/__init__.py @@ -12,214 +12,32 @@ * More detailed explanations would be good, since it's not very well explained by the manual. """ -from fl_classes import FlMidiMsg - - -def init(deviceName: str, width: int, height: int) -> None: - """ - Initialise launchmap pages. - - ## Args - - * `deviceName` (`str`): ??? - - * `width` (`int`): ??? - - * `height` (`int`): ??? - - Included since API version 1. - """ - - -def createOverlayMap( - offColor: int, - onColor: int, - width: int, - height: int, -) -> None: - """ - Creates an overlay map. - - ## Args - - * `offColor` (`int`): ? - - * `onColor` (`int`): ? - - * `width` (`int`): ? - - * `height` (`int`): ? - - Included since API version 1. - """ - - -def length() -> int: - """ - Returns launchmap pages length. - - ## Returns - - * `int`: length. - - Included since API version 1. - """ - return 0 - - -def updateMap(index: int) -> None: - """ - Updates launchmap page at `index`. - - ## Args - - * `index` (`int`): index of page to update. - - Included since API version 1. - """ - - -def getMapItemColor(index: int, itemIndex: int) -> int: - """ - Returns item color of `itemIndex` in map `index`. - - ## Args - - * `index` (`int`): map index. - - * `itemIndex` (`int`): item index. - - ## Returns - - * `int`: color. - - Included since API version 1. - """ - return 0 - - -def getMapCount(index: int) -> int: - """ - Returns the number of items in page at `index`. - - ## Args - - * `index` (`int`): page index. - - ## Returns - - * `int`: number of items. - - Included since API version 1. - """ - return 0 - - -def getMapItemChannel(index: int, itemIndex: int) -> int: - """ - Returns the channel for item at `itemIndex` on page at `index`. - - ## Args - - * `index` (`int`): page index. - - * `itemIndex` (`int`): item index. - - ## Returns - - * `int`: channel number. - - Included since API version 1. - """ - return 0 - - -def getMapItemAftertouch(index: int, itemIndex: int) -> int: - """ - Returns the aftertouch for item at `itemIndex` on page at `index`. - - ## Args - - * `index` (`int`): page index. - - * `itemIndex` (`int`): item index. - - ## Returns - - * `int`: aftertouch value. - - Included since API version 1. - """ - return 0 - - -def processMapItem( - eventData: FlMidiMsg, - index: int, - itemIndex: int, - velocity: int, -) -> None: - """ - Process map item at `itemIndex` of page at `index` - - ## Args - - * eventData (`eventData`): event data. - - * index (`int`): page index. - - * itemIndex (`int`): item index. - - * velocity (`int`): velocity. - - Included since API version 1. - """ - - -def releaseMapItem(eventData: FlMidiMsg, index: int) -> None: - """ - Release map item at `itemIndex` of page at `index`. - - ## HELP WANTED - This doesn't seem quite right, there is no `itemIndex` argument. - - ## Args - - * `eventData` (`eventData`): event data. - - * `index` (`int`): page index. - - Included since API version 1. - """ - - -def checkMapForHiddenItem() -> None: - """ - Checks for launchpad hidden item??? - - ## HELP WANTED - What does this do? - - Included since API version 1. - """ - - -def setMapItemTarget(index: int, itemIndex: int, target: int) -> int: - """ - Set target for item at `itemIndex` of page at `index`. - - ## Args - - * `index` (`int`): page index. - - * `itemIndex` (`int`): item index. - - * `target` (`int`): ???? - - ## Returns - - * `int`: ???? - - Included since API version 1. - """ - return 0 +__all__ = [ + 'init', + 'createOverlayMap', + 'length', + 'updateMap', + 'getMapItemColor', + 'getMapCount', + 'getMapItemChannel', + 'getMapItemAftertouch', + 'processMapItem', + 'releaseMapItem', + 'checkMapForHiddenItem', + 'setMapItemTarget', +] + +from .__launchMapPages import ( + init, + createOverlayMap, + length, + updateMap, + getMapItemColor, + getMapCount, + getMapItemChannel, + getMapItemAftertouch, + processMapItem, + releaseMapItem, + checkMapForHiddenItem, + setMapItemTarget, +) diff --git a/src/launchMapPages/__launchMapPages.py b/src/launchMapPages/__launchMapPages.py new file mode 100644 index 0000000..5069e10 --- /dev/null +++ b/src/launchMapPages/__launchMapPages.py @@ -0,0 +1,225 @@ +""" +# Launchmap Pages + +FL Studio built-in module. + +Handles custom controller layouts for certain controllers. + +Refer to [reference](https://forum.image-line.com/viewtopic.php?f=1914&t=92193). + +## HELP WANTED + +* More detailed explanations would be good, since it's not very well + explained by the manual. +""" +from fl_classes import FlMidiMsg + + +def init(deviceName: str, width: int, height: int) -> None: + """ + Initialise launchmap pages. + + ## Args + + * `deviceName` (`str`): ??? + + * `width` (`int`): ??? + + * `height` (`int`): ??? + + Included since API version 1. + """ + + +def createOverlayMap( + offColor: int, + onColor: int, + width: int, + height: int, +) -> None: + """ + Creates an overlay map. + + ## Args + + * `offColor` (`int`): ? + + * `onColor` (`int`): ? + + * `width` (`int`): ? + + * `height` (`int`): ? + + Included since API version 1. + """ + + +def length() -> int: + """ + Returns launchmap pages length. + + ## Returns + + * `int`: length. + + Included since API version 1. + """ + return 0 + + +def updateMap(index: int) -> None: + """ + Updates launchmap page at `index`. + + ## Args + + * `index` (`int`): index of page to update. + + Included since API version 1. + """ + + +def getMapItemColor(index: int, itemIndex: int) -> int: + """ + Returns item color of `itemIndex` in map `index`. + + ## Args + + * `index` (`int`): map index. + + * `itemIndex` (`int`): item index. + + ## Returns + + * `int`: color. + + Included since API version 1. + """ + return 0 + + +def getMapCount(index: int) -> int: + """ + Returns the number of items in page at `index`. + + ## Args + + * `index` (`int`): page index. + + ## Returns + + * `int`: number of items. + + Included since API version 1. + """ + return 0 + + +def getMapItemChannel(index: int, itemIndex: int) -> int: + """ + Returns the channel for item at `itemIndex` on page at `index`. + + ## Args + + * `index` (`int`): page index. + + * `itemIndex` (`int`): item index. + + ## Returns + + * `int`: channel number. + + Included since API version 1. + """ + return 0 + + +def getMapItemAftertouch(index: int, itemIndex: int) -> int: + """ + Returns the aftertouch for item at `itemIndex` on page at `index`. + + ## Args + + * `index` (`int`): page index. + + * `itemIndex` (`int`): item index. + + ## Returns + + * `int`: aftertouch value. + + Included since API version 1. + """ + return 0 + + +def processMapItem( + eventData: FlMidiMsg, + index: int, + itemIndex: int, + velocity: int, +) -> None: + """ + Process map item at `itemIndex` of page at `index` + + ## Args + + * eventData (`eventData`): event data. + + * index (`int`): page index. + + * itemIndex (`int`): item index. + + * velocity (`int`): velocity. + + Included since API version 1. + """ + + +def releaseMapItem(eventData: FlMidiMsg, index: int) -> None: + """ + Release map item at `itemIndex` of page at `index`. + + ## HELP WANTED + This doesn't seem quite right, there is no `itemIndex` argument. + + ## Args + + * `eventData` (`eventData`): event data. + + * `index` (`int`): page index. + + Included since API version 1. + """ + + +def checkMapForHiddenItem() -> None: + """ + Checks for launchpad hidden item??? + + ## HELP WANTED + What does this do? + + Included since API version 1. + """ + + +def setMapItemTarget(index: int, itemIndex: int, target: int) -> int: + """ + Set target for item at `itemIndex` of page at `index`. + + ## Args + + * `index` (`int`): page index. + + * `itemIndex` (`int`): item index. + + * `target` (`int`): ???? + + ## Returns + + * `int`: ???? + + Included since API version 1. + """ + return 0 diff --git a/src/screen/__init__.py b/src/screen/__init__.py index e718db3..3a6179c 100644 --- a/src/screen/__init__.py +++ b/src/screen/__init__.py @@ -15,216 +15,65 @@ appreciate a pull request with improvements to the type safety and documentation. """ - - -def init( - display_width: int, - display_height: int, - text_row_height: int, - font_size: int, - value_a: int, - value_b: int, - /, -) -> None: - """ - Initialize the screen of the AKAI Fire - - This should be called before using any of the other functions in the - `screen` module. After calling it, the `screen.setup` function should be - called to allow the screen to be used. - - ## Args: - * `display_width` (`int`): width of the display in pixels - - * `display_height` (`int`): height of the display in pixels - - * `text_row_height` (`int`): height of a text row (in what units?) - - * `font_size` (`int`): font size to use (in what units?) - - * `value_a` (`int`): unknown - - * `value_b` (`int`): unknown - - Included since API Version 1 - """ - - -def deInit() -> None: - """ - De-initialize the screen of the AKAI Fire - - This should be called before the script closes. - - Included since API Version 1 - """ - - -def setup( - sysex_header: int, - screen_active_timeout: int, - screen_auto_timeout: int, - text_scroll_pause: int, - text_scroll_speed: int, - text_display_time: int, - /, -) -> None: - """ - Set up the AKAI Fire screen. - - This should be called after `screen.init` in order to perform more setup - - ## Args: - * `sysex_header` (`int`): header for sysex message for device - - * `screen_active_timeout` (`int`): unknown - - * `screen_auto_timeout` (`int`): unknown - - * `text_scroll_pause` (`int`): unknown - - * `text_scroll_speed` (`int`): unknown - - * `text_display_time` (`int`): unknown - - Included since API Version 1 - """ - - -def update() -> None: - """ - Notify the Fire that it should update its screen contents - - This should be called after performing updates to the device's screen. - - Included since API Version 1 - """ - - -def addMeter(*args) -> None: - ... - - -def addTextLine(text: str, line: int, /) -> None: - """ - Add text to a line on the screen? - - ## Args: - * `text` (`str`): text to add - - * `line` (`int`): line on the screen - - Included since API Version 1 - """ - - -def animateText(*args) -> None: - ... - - -def blank(*args) -> None: - ... - - -def displayBar(*args) -> None: - ... - - -def displayText(*args) -> None: - ... - - -def displayTimedText(*args) -> None: - ... - - -def drawRect(*args) -> None: - ... - - -def drawText(*args) -> None: - ... - - -def eraseRect(*args) -> None: - ... - - -def fillRect( - start_x: int, - start_y: int, - end_x: int, - end_y: int, - value: int, - /, -) -> None: - """ - Draw a filled rectangle to the given position on the screen. - - It is drawn from the start values up to (but not including) the end values. - - ## Args: - * `start_x` (`int`): starting horizontal position - - * `start_y` (`int`): starting vertical position - - * `end_x` (`int`): ending horizontal position - - * `end_y` (`int`): ending vertical position - - * `value` (`int`): unknown, maybe color? - - Included since API Version 1 - """ - - -def findTextLine(*args) -> None: - ... - - -def getScreenActiveCounter(*args) -> None: - ... - - -def hideMenu(*args) -> None: - ... - - -def isBlanked(*args) -> None: - ... - - -def isUnBlank(*args) -> None: - ... - - -def keepDisplayActive(*args) -> None: - ... - - -def menuItemClick(*args) -> None: - ... - - -def menuNext(*args) -> None: - ... - - -def menuPrev(*args) -> None: - ... - - -def menuShowing(*args) -> None: - ... - - -def removeTextLine(*args) -> None: - ... - - -def setScreenActiveCounter(*args) -> None: - ... - - -def unBlank(*args) -> None: - ... +__all__ = [ + 'init', + 'deInit', + 'setup', + 'update', + 'addMeter', + 'addTextLine', + 'animateText', + 'blank', + 'displayBar', + 'displayText', + 'displayTimedText', + 'drawRect', + 'drawText', + 'eraseRect', + 'fillRect', + 'findTextLine', + 'getScreenActiveCounter', + 'hideMenu', + 'isBlanked', + 'isUnBlank', + 'keepDisplayActive', + 'menuItemClick', + 'menuNext', + 'menuPrev', + 'menuShowing', + 'removeTextLine', + 'setScreenActiveCounter', + 'unBlank', +] + + +from .__screen import ( + init, + deInit, + setup, + update, + addMeter, + addTextLine, + animateText, + blank, + displayBar, + displayText, + displayTimedText, + drawRect, + drawText, + eraseRect, + fillRect, + findTextLine, + getScreenActiveCounter, + hideMenu, + isBlanked, + isUnBlank, + keepDisplayActive, + menuItemClick, + menuNext, + menuPrev, + menuShowing, + removeTextLine, + setScreenActiveCounter, + unBlank, +) diff --git a/src/screen/__screen.py b/src/screen/__screen.py new file mode 100644 index 0000000..e718db3 --- /dev/null +++ b/src/screen/__screen.py @@ -0,0 +1,230 @@ +""" +# Screen + +FL Studio built-in module. + +Helper functions for controlling the screen of the AKAI FL Studio Fire MIDI +controller. + +These likely aren't useful for most scripts, but if you're writing a script for +the Fire they might be handy. + +## HELP WANTED + +These functions are undocumented. If you know how they work, I'd massively +appreciate a pull request with improvements to the type safety and +documentation. +""" + + +def init( + display_width: int, + display_height: int, + text_row_height: int, + font_size: int, + value_a: int, + value_b: int, + /, +) -> None: + """ + Initialize the screen of the AKAI Fire + + This should be called before using any of the other functions in the + `screen` module. After calling it, the `screen.setup` function should be + called to allow the screen to be used. + + ## Args: + * `display_width` (`int`): width of the display in pixels + + * `display_height` (`int`): height of the display in pixels + + * `text_row_height` (`int`): height of a text row (in what units?) + + * `font_size` (`int`): font size to use (in what units?) + + * `value_a` (`int`): unknown + + * `value_b` (`int`): unknown + + Included since API Version 1 + """ + + +def deInit() -> None: + """ + De-initialize the screen of the AKAI Fire + + This should be called before the script closes. + + Included since API Version 1 + """ + + +def setup( + sysex_header: int, + screen_active_timeout: int, + screen_auto_timeout: int, + text_scroll_pause: int, + text_scroll_speed: int, + text_display_time: int, + /, +) -> None: + """ + Set up the AKAI Fire screen. + + This should be called after `screen.init` in order to perform more setup + + ## Args: + * `sysex_header` (`int`): header for sysex message for device + + * `screen_active_timeout` (`int`): unknown + + * `screen_auto_timeout` (`int`): unknown + + * `text_scroll_pause` (`int`): unknown + + * `text_scroll_speed` (`int`): unknown + + * `text_display_time` (`int`): unknown + + Included since API Version 1 + """ + + +def update() -> None: + """ + Notify the Fire that it should update its screen contents + + This should be called after performing updates to the device's screen. + + Included since API Version 1 + """ + + +def addMeter(*args) -> None: + ... + + +def addTextLine(text: str, line: int, /) -> None: + """ + Add text to a line on the screen? + + ## Args: + * `text` (`str`): text to add + + * `line` (`int`): line on the screen + + Included since API Version 1 + """ + + +def animateText(*args) -> None: + ... + + +def blank(*args) -> None: + ... + + +def displayBar(*args) -> None: + ... + + +def displayText(*args) -> None: + ... + + +def displayTimedText(*args) -> None: + ... + + +def drawRect(*args) -> None: + ... + + +def drawText(*args) -> None: + ... + + +def eraseRect(*args) -> None: + ... + + +def fillRect( + start_x: int, + start_y: int, + end_x: int, + end_y: int, + value: int, + /, +) -> None: + """ + Draw a filled rectangle to the given position on the screen. + + It is drawn from the start values up to (but not including) the end values. + + ## Args: + * `start_x` (`int`): starting horizontal position + + * `start_y` (`int`): starting vertical position + + * `end_x` (`int`): ending horizontal position + + * `end_y` (`int`): ending vertical position + + * `value` (`int`): unknown, maybe color? + + Included since API Version 1 + """ + + +def findTextLine(*args) -> None: + ... + + +def getScreenActiveCounter(*args) -> None: + ... + + +def hideMenu(*args) -> None: + ... + + +def isBlanked(*args) -> None: + ... + + +def isUnBlank(*args) -> None: + ... + + +def keepDisplayActive(*args) -> None: + ... + + +def menuItemClick(*args) -> None: + ... + + +def menuNext(*args) -> None: + ... + + +def menuPrev(*args) -> None: + ... + + +def menuShowing(*args) -> None: + ... + + +def removeTextLine(*args) -> None: + ... + + +def setScreenActiveCounter(*args) -> None: + ... + + +def unBlank(*args) -> None: + ... diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 9144ecd..f9fac75 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -21,524 +21,63 @@ file for your inspection and warnings have been added to the docstrings. Use any functions here with caution. """ - -# import warnings -# -# warnings.warn("The utils module contains exceptionally buggy code. Usage is " -# "not recommended", stacklevel=2) - -import math - - -class TRect: - """Represents a rectangle object - """ - - def __init__(self, left: int, top: int, right: int, bottom: int): - """Create a `TRect` object representing a rectangle - - ## Args: - * left (int): left position - - * top (int): top position - - * right (int): right position - - * bottom (int): bottom position - """ - self.Top = top - self.Left = left - self.Bottom = bottom - self.Right = right - - def Width(self) -> int: - """Returns width of a rectangle - - ## Returns: - * int: width - """ - return self.Right - self.Left - - def Height(self) -> int: - """Returns the height of a rectangle - - ## Returns: - * int: height - """ - return self.Bottom - self.Top - - -class TClipLauncherLastClip: - def __init__(self, trackNum, subNum, flags): - self.TrackNum = trackNum - self.SubNum = subNum - self.Flags = flags - - -def RectOverlapEqual(R1: TRect, R2: TRect) -> bool: - """Returns whether two rectangles are overlapping or touching - - ## Args: - * R1 (TRect): rectangle 1 - - * R2 (TRect): rectangle 2 - - ## Returns: - * bool: whether rectangles overlap or touch - """ - return (R1.Left <= R2.Right) & (R1.Right >= R2.Left) & (R1.Top <= R2.Bottom) & (R1.Bottom >= R2.Top) - - -def RectOverlap(R1: TRect, R2: TRect) -> bool: - """Returns whether two rectangles are overlapping - - ## Args: - * R1 (TRect): rectangle 1 - - * R2 (TRect): rectangle 2 - - ## Returns: - * bool: whether rectangles overlap - """ - return (R1.Left < R2.Right) & (R1.Right > R2.Left) & (R1.Top < R2.Bottom) & (R1.Bottom > R2.Top) - - -def Limited(Value: float, Min: float, Max: float) -> float: - """Limit a value to within the range `Min` - `Max` - - ## Args: - * Value (float): Current value - - * Min (float): Min value - - * Max (float): Max value - - ## Returns: - * float: limited value - """ - if Value <= Min: - res = Min - else: - res = Value - if res > Max: - res = Max - return res - - -def InterNoSwap(X, A, B) -> bool: - """Returns whether A <= X <= B, ie. whether X lies between A and B - - ## Args: - * X (number): x - - * A (number): a - - * B (number): b - - ## Returns: - * bool - """ - return (X >= A) & (X <= B) - - -def DivModU(A: int, B: int) -> 'tuple[int, int]': - """Return integer division and modulus - - ## Args: - * A (int): int 1 - - * B (int): int 2 - - ## Returns: - * int: integer division - - * int: modulus - """ - C = A % B - return (A // B), C - - -def SwapInt(A, B): - """Given A and B, return B and A - - It's probably easier to just manually write - ```py - A, B = B, A - ``` - in your code to begin with. - - ## Args: - * A (any): thing 1 - - * B (any): thing 2 - - ## Returns: - * tuple: B, A - """ - return B, A - - -def Zeros(value, nChars, c='0'): - """TODO - - ## Args: - * value ([type]): [description] - - * nChars ([type]): [description] - - * c (str, optional): [description]. Defaults to '0'. - - ## Returns: - * [type]: [description] - """ - if value < 0: - Result = str(-value) - Result = '-' + c * (nChars - len(Result)) + Result - else: - Result = str(value) - Result = c * (nChars - len(Result)) + Result - return Result - - -def Zeros_Strict(value, nChars, c='0'): - """TODO - - ## Args: - * value ([type]): [description] - - * nChars ([type]): [description] - - * c (str, optional): [description]. Defaults to '0'. - - ## Returns: - * [type]: [description] - - WARNING: - * Strict trimming looks incorrect - """ - if value < 0: - Result = str(-value) - Result = '-' + c * (nChars - len(Result) - 1) + Result - else: - Result = str(value) - Result = c * (nChars - len(Result)) + Result - if len(Result) > nChars: - Result = Result[len(Result) - nChars] - return Result - - -def Sign(value: 'float | int') -> int: - """Equivalent to `SignOf()` - - ## Args: - * value (`float | int`): number - - ## Returns: - * `int`: sign - """ - if value < 0: - return -1 - elif value == 0: - return 0 - else: - return 1 - - -SignBitPos_64 = 63 -SignBit_64 = 1 << SignBitPos_64 -SignBitPos_Nat = SignBitPos_64 - - -def SignOf(value: 'float | int') -> int: - """Return the sign of a numerical value - - ## Args: - * value (`float | int`): number - - ## Returns: - * `int`: sign: - * `0`: zero - - * `1`: positive - - * `-1`: negative - """ - if value == 0: - return 0 - elif value < 0: - return -1 - else: - return 1 - - -def KnobAccelToRes2(Value): - """TODO - - ## Args: - * Value ([type]): [description] - - ## Returns: - * [type]: [description] - """ - n = abs(Value) - if n > 1: - res = n ** 0.75 - else: - res = 1 - return res - - -def OffsetRect(R: TRect, dx: int, dy: int) -> None: - """Offset a rectangle by `dx` and `dy` - - ## Args: - * R (TRect): rectangle - - * dx (int): x offset - - * dy (int): y offset - - NOTE: Rectangle is adjusted in-place - """ - R.Left = R.Left + dx - R.Top = R.Top + dy - R.Right = R.Right + dx - R.Bottom = R.Bottom + dy - - -def RGBToHSV(R: float, G: float, B: float) -> 'tuple[float, float, float]': - """Convert an RGB color to a HSV color - - WARNING: Make sure to convert - - ## Args: - * R (float): red (0.0 - 1.0) - - * G (float): green (0.0 - 1.0) - - * B (float): blue (0.0 - 1.0) - - ## Returns: - * H: hue (degrees: 0.0-360) - - * S: saturation (0.0-1.0) - - * V: value/luminosity (0.0/1.0) - """ - Min = min(min(R, G), B) - V = max(max(R, G), B) - - Delta = V - Min - - if V == 0: - S = 0 - else: - S = Delta / V # type: ignore - - if S == 0.0: - H = 0.0 - else: - if R == V: - H = 60.0 * (G - B) / Delta - elif G == V: - H = 120.0 + 60.0 * (B - R) / Delta - elif B == V: - H = 240.0 + 60.0 * (R - G) / Delta - - if H < 0.0: # type: ignore - H = H + 360.0 # type: ignore - - return H, S, V # type: ignore - - -def RGBToHSVColor(Color: int) -> 'tuple[float, float, float]': - """Convert an RGB color to a HSV color - - ## Args: - * Color (int): color as integer (`0x--BBGGRR`) - - ## Returns: - * H: hue - - * S: saturation - - * V: value (brightness) - """ - r = ((Color & 0xFF0000) >> 16) / 255 - g = ((Color & 0x00FF00) >> 8) / 255 - b = ((Color & 0x0000FF) >> 0) / 255 - H, S, V = RGBToHSV(r, g, b) - return H, S, V - - -def HSVtoRGB(H: float, S: float, V: float) -> 'tuple[float, float, float]': - """Convert an HSV color to an RGB color - - WARNING: This function returns data in an unexpected format! Be sure to - convert as required before usage. - - ## Args: - * H (float): hue (degrees: 0.0-360) - - * S (float): saturation (0-1.0) - - * V (float): value/luminosity (0-1.0) - - ## Returns: - * float: red (0.0-1.0) - - * float: green (0.0-1.0) - - * float: blue (0.0-1.0) - """ - hTemp = 0 - if S == 0.0: - R = V - G = V - B = V - else: - if H == 360.0: - hTemp = 0.0 # type: ignore - else: - hTemp = H # type: ignore - - hTemp = hTemp / 60 # type: ignore - i = math.trunc(hTemp) - f = hTemp - i - - p = V * (1.0 - S) - q = V * (1.0 - (S * f)) - t = V * (1.0 - (S * (1.0 - f))) - - if i == 0: - R = V - G = t - B = p - elif i == 1: - R = q - G = V - B = p - elif i == 2: - R = p - G = V - B = t - elif i == 3: - R = p - G = q - B = V - elif i == 4: - R = t - G = p - B = V - elif i == 5: - R = V - G = p - B = q - return R, G, B # type: ignore - - -NoteNameT = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B') - - -def GetNoteName(NoteNum: int) -> str: - """Return the note name given a note number - - ## Args: - * NoteNum (int): note number - - ## Returns: - * str: note name - """ - NoteNum += 1200 - return NoteNameT[NoteNum % 12] + str((NoteNum // 12) - 100) - - -def ColorToRGB(Color: int) -> 'tuple[int, int, int]': - """Convert an integer color to an RGB tuple that uses range 0-255. - - ## Args: - * Color (int): color as integer - - ## Returns: - * int: red - - * int: green - - * int: blue - """ - return (Color >> 16) & 0xFF, (Color >> 8) & 0xFF, Color & 0xFF - - -def RGBToColor(R: int, G: int, B: int) -> int: - """convert an RGB set to an integer color. values must be 0-255 - - ## Args: - * R (int): red - - * G (int): green - - * B (int): blue - - ## Returns: - * int: color - """ - return (R << 16) | (G << 8) | B - - -def FadeColor(StartColor: int, EndColor: int, Value: float) -> int: - """Fade between two colors - - ## Args: - * StartColor (int): color integer - - * EndColor (int): color integer - - * Value (float): fade position (0-255) - - ## Returns: - * int: faded color - - WARNING: - * Blue value is incorrect, using green start value - """ - rStart, gStart, bStart = ColorToRGB(StartColor) - rEnd, gEnd, bEnd = ColorToRGB(EndColor) - ratio = Value / 255 - rEnd = round(rStart * (1 - ratio) + (rEnd * ratio)) - gEnd = round(gStart * (1 - ratio) + (gEnd * ratio)) - bEnd = round(gStart * (1 - ratio) + (bEnd * ratio)) - return RGBToColor(rEnd, gEnd, bEnd) - - -def LightenColor(Color: int, Value: float) -> int: - """Lighten a color by a certain amount - - ## Args: - * Color (int): color integer - - * Value (float): amount to lighten by (0-255) - - ## Returns: - * int: lightened color - """ - r, g, b = ColorToRGB(Color) - ratio = Value / 255 - return RGBToColor(round(r + (1.0 - r) * ratio), round(g + (1.0 - g) * ratio), round(b + (1.0 - b) * ratio)) - - -def VolTodB(Value: float) -> float: - """Convert volume as a decimal (0.0 - 1.0) to a decibel value - - ### WARNING: - * For zero volume, this returns 0 instead of -oo dB - - ## Args: - * Value (float): volume - - ## Returns: - * float: volume in decibels - """ - Value = (math.exp(Value * math.log(11)) - 1) * 0.1 - if Value == 0: - return 0 - return round(math.log10(Value) * 20, 1) +__all__ = [ + 'TRect', + 'TClipLauncherLastClip', + 'RectOverlapEqual', + 'RectOverlap', + 'Limited', + 'InterNoSwap', + 'DivModU', + 'SwapInt', + 'Zeros', + 'Zeros_Strict', + 'Sign', + 'SignBitPos_64', + 'SignBit_64', + 'SignBitPos_Nat', + 'SignOf', + 'KnobAccelToRes2', + 'OffsetRect', + 'RGBToHSV', + 'RGBToHSVColor', + 'HSVtoRGB', + 'NoteNameT', + 'GetNoteName', + 'ColorToRGB', + 'RGBToColor', + 'FadeColor', + 'LightenColor', + 'VolTodB', +] + + +from .__utils import ( + TRect, + TClipLauncherLastClip, + RectOverlapEqual, + RectOverlap, + Limited, + InterNoSwap, + DivModU, + SwapInt, + Zeros, + Zeros_Strict, + Sign, + SignBitPos_64, + SignBit_64, + SignBitPos_Nat, + SignOf, + KnobAccelToRes2, + OffsetRect, + RGBToHSV, + RGBToHSVColor, + HSVtoRGB, + NoteNameT, + GetNoteName, + ColorToRGB, + RGBToColor, + FadeColor, + LightenColor, + VolTodB, +) diff --git a/src/utils/__utils.py b/src/utils/__utils.py new file mode 100644 index 0000000..9144ecd --- /dev/null +++ b/src/utils/__utils.py @@ -0,0 +1,544 @@ +""" +# Util + +Module included in FL Studio Python lib folder. + +Contains useful functions and classes for use when working with FL Studio's +Python API. + +## Note + +This code is taken from FL Studio's Python lib folder and included in this +package in the hope that it will be useful for script developers. It is not the +creation of the repository authors, and no credit is claimed for the code +content. However, the documentation for the provided code is created by the +authors of this repository. + +## WARNING + +Many of the provided functions in the FL Studio installation have bugs +that may result in unexpected behavior. These bugs have been left as-is in this +file for your inspection and warnings have been added to the docstrings. Use +any functions here with caution. +""" + +# import warnings +# +# warnings.warn("The utils module contains exceptionally buggy code. Usage is " +# "not recommended", stacklevel=2) + +import math + + +class TRect: + """Represents a rectangle object + """ + + def __init__(self, left: int, top: int, right: int, bottom: int): + """Create a `TRect` object representing a rectangle + + ## Args: + * left (int): left position + + * top (int): top position + + * right (int): right position + + * bottom (int): bottom position + """ + self.Top = top + self.Left = left + self.Bottom = bottom + self.Right = right + + def Width(self) -> int: + """Returns width of a rectangle + + ## Returns: + * int: width + """ + return self.Right - self.Left + + def Height(self) -> int: + """Returns the height of a rectangle + + ## Returns: + * int: height + """ + return self.Bottom - self.Top + + +class TClipLauncherLastClip: + def __init__(self, trackNum, subNum, flags): + self.TrackNum = trackNum + self.SubNum = subNum + self.Flags = flags + + +def RectOverlapEqual(R1: TRect, R2: TRect) -> bool: + """Returns whether two rectangles are overlapping or touching + + ## Args: + * R1 (TRect): rectangle 1 + + * R2 (TRect): rectangle 2 + + ## Returns: + * bool: whether rectangles overlap or touch + """ + return (R1.Left <= R2.Right) & (R1.Right >= R2.Left) & (R1.Top <= R2.Bottom) & (R1.Bottom >= R2.Top) + + +def RectOverlap(R1: TRect, R2: TRect) -> bool: + """Returns whether two rectangles are overlapping + + ## Args: + * R1 (TRect): rectangle 1 + + * R2 (TRect): rectangle 2 + + ## Returns: + * bool: whether rectangles overlap + """ + return (R1.Left < R2.Right) & (R1.Right > R2.Left) & (R1.Top < R2.Bottom) & (R1.Bottom > R2.Top) + + +def Limited(Value: float, Min: float, Max: float) -> float: + """Limit a value to within the range `Min` - `Max` + + ## Args: + * Value (float): Current value + + * Min (float): Min value + + * Max (float): Max value + + ## Returns: + * float: limited value + """ + if Value <= Min: + res = Min + else: + res = Value + if res > Max: + res = Max + return res + + +def InterNoSwap(X, A, B) -> bool: + """Returns whether A <= X <= B, ie. whether X lies between A and B + + ## Args: + * X (number): x + + * A (number): a + + * B (number): b + + ## Returns: + * bool + """ + return (X >= A) & (X <= B) + + +def DivModU(A: int, B: int) -> 'tuple[int, int]': + """Return integer division and modulus + + ## Args: + * A (int): int 1 + + * B (int): int 2 + + ## Returns: + * int: integer division + + * int: modulus + """ + C = A % B + return (A // B), C + + +def SwapInt(A, B): + """Given A and B, return B and A + + It's probably easier to just manually write + ```py + A, B = B, A + ``` + in your code to begin with. + + ## Args: + * A (any): thing 1 + + * B (any): thing 2 + + ## Returns: + * tuple: B, A + """ + return B, A + + +def Zeros(value, nChars, c='0'): + """TODO + + ## Args: + * value ([type]): [description] + + * nChars ([type]): [description] + + * c (str, optional): [description]. Defaults to '0'. + + ## Returns: + * [type]: [description] + """ + if value < 0: + Result = str(-value) + Result = '-' + c * (nChars - len(Result)) + Result + else: + Result = str(value) + Result = c * (nChars - len(Result)) + Result + return Result + + +def Zeros_Strict(value, nChars, c='0'): + """TODO + + ## Args: + * value ([type]): [description] + + * nChars ([type]): [description] + + * c (str, optional): [description]. Defaults to '0'. + + ## Returns: + * [type]: [description] + + WARNING: + * Strict trimming looks incorrect + """ + if value < 0: + Result = str(-value) + Result = '-' + c * (nChars - len(Result) - 1) + Result + else: + Result = str(value) + Result = c * (nChars - len(Result)) + Result + if len(Result) > nChars: + Result = Result[len(Result) - nChars] + return Result + + +def Sign(value: 'float | int') -> int: + """Equivalent to `SignOf()` + + ## Args: + * value (`float | int`): number + + ## Returns: + * `int`: sign + """ + if value < 0: + return -1 + elif value == 0: + return 0 + else: + return 1 + + +SignBitPos_64 = 63 +SignBit_64 = 1 << SignBitPos_64 +SignBitPos_Nat = SignBitPos_64 + + +def SignOf(value: 'float | int') -> int: + """Return the sign of a numerical value + + ## Args: + * value (`float | int`): number + + ## Returns: + * `int`: sign: + * `0`: zero + + * `1`: positive + + * `-1`: negative + """ + if value == 0: + return 0 + elif value < 0: + return -1 + else: + return 1 + + +def KnobAccelToRes2(Value): + """TODO + + ## Args: + * Value ([type]): [description] + + ## Returns: + * [type]: [description] + """ + n = abs(Value) + if n > 1: + res = n ** 0.75 + else: + res = 1 + return res + + +def OffsetRect(R: TRect, dx: int, dy: int) -> None: + """Offset a rectangle by `dx` and `dy` + + ## Args: + * R (TRect): rectangle + + * dx (int): x offset + + * dy (int): y offset + + NOTE: Rectangle is adjusted in-place + """ + R.Left = R.Left + dx + R.Top = R.Top + dy + R.Right = R.Right + dx + R.Bottom = R.Bottom + dy + + +def RGBToHSV(R: float, G: float, B: float) -> 'tuple[float, float, float]': + """Convert an RGB color to a HSV color + + WARNING: Make sure to convert + + ## Args: + * R (float): red (0.0 - 1.0) + + * G (float): green (0.0 - 1.0) + + * B (float): blue (0.0 - 1.0) + + ## Returns: + * H: hue (degrees: 0.0-360) + + * S: saturation (0.0-1.0) + + * V: value/luminosity (0.0/1.0) + """ + Min = min(min(R, G), B) + V = max(max(R, G), B) + + Delta = V - Min + + if V == 0: + S = 0 + else: + S = Delta / V # type: ignore + + if S == 0.0: + H = 0.0 + else: + if R == V: + H = 60.0 * (G - B) / Delta + elif G == V: + H = 120.0 + 60.0 * (B - R) / Delta + elif B == V: + H = 240.0 + 60.0 * (R - G) / Delta + + if H < 0.0: # type: ignore + H = H + 360.0 # type: ignore + + return H, S, V # type: ignore + + +def RGBToHSVColor(Color: int) -> 'tuple[float, float, float]': + """Convert an RGB color to a HSV color + + ## Args: + * Color (int): color as integer (`0x--BBGGRR`) + + ## Returns: + * H: hue + + * S: saturation + + * V: value (brightness) + """ + r = ((Color & 0xFF0000) >> 16) / 255 + g = ((Color & 0x00FF00) >> 8) / 255 + b = ((Color & 0x0000FF) >> 0) / 255 + H, S, V = RGBToHSV(r, g, b) + return H, S, V + + +def HSVtoRGB(H: float, S: float, V: float) -> 'tuple[float, float, float]': + """Convert an HSV color to an RGB color + + WARNING: This function returns data in an unexpected format! Be sure to + convert as required before usage. + + ## Args: + * H (float): hue (degrees: 0.0-360) + + * S (float): saturation (0-1.0) + + * V (float): value/luminosity (0-1.0) + + ## Returns: + * float: red (0.0-1.0) + + * float: green (0.0-1.0) + + * float: blue (0.0-1.0) + """ + hTemp = 0 + if S == 0.0: + R = V + G = V + B = V + else: + if H == 360.0: + hTemp = 0.0 # type: ignore + else: + hTemp = H # type: ignore + + hTemp = hTemp / 60 # type: ignore + i = math.trunc(hTemp) + f = hTemp - i + + p = V * (1.0 - S) + q = V * (1.0 - (S * f)) + t = V * (1.0 - (S * (1.0 - f))) + + if i == 0: + R = V + G = t + B = p + elif i == 1: + R = q + G = V + B = p + elif i == 2: + R = p + G = V + B = t + elif i == 3: + R = p + G = q + B = V + elif i == 4: + R = t + G = p + B = V + elif i == 5: + R = V + G = p + B = q + return R, G, B # type: ignore + + +NoteNameT = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B') + + +def GetNoteName(NoteNum: int) -> str: + """Return the note name given a note number + + ## Args: + * NoteNum (int): note number + + ## Returns: + * str: note name + """ + NoteNum += 1200 + return NoteNameT[NoteNum % 12] + str((NoteNum // 12) - 100) + + +def ColorToRGB(Color: int) -> 'tuple[int, int, int]': + """Convert an integer color to an RGB tuple that uses range 0-255. + + ## Args: + * Color (int): color as integer + + ## Returns: + * int: red + + * int: green + + * int: blue + """ + return (Color >> 16) & 0xFF, (Color >> 8) & 0xFF, Color & 0xFF + + +def RGBToColor(R: int, G: int, B: int) -> int: + """convert an RGB set to an integer color. values must be 0-255 + + ## Args: + * R (int): red + + * G (int): green + + * B (int): blue + + ## Returns: + * int: color + """ + return (R << 16) | (G << 8) | B + + +def FadeColor(StartColor: int, EndColor: int, Value: float) -> int: + """Fade between two colors + + ## Args: + * StartColor (int): color integer + + * EndColor (int): color integer + + * Value (float): fade position (0-255) + + ## Returns: + * int: faded color + + WARNING: + * Blue value is incorrect, using green start value + """ + rStart, gStart, bStart = ColorToRGB(StartColor) + rEnd, gEnd, bEnd = ColorToRGB(EndColor) + ratio = Value / 255 + rEnd = round(rStart * (1 - ratio) + (rEnd * ratio)) + gEnd = round(gStart * (1 - ratio) + (gEnd * ratio)) + bEnd = round(gStart * (1 - ratio) + (bEnd * ratio)) + return RGBToColor(rEnd, gEnd, bEnd) + + +def LightenColor(Color: int, Value: float) -> int: + """Lighten a color by a certain amount + + ## Args: + * Color (int): color integer + + * Value (float): amount to lighten by (0-255) + + ## Returns: + * int: lightened color + """ + r, g, b = ColorToRGB(Color) + ratio = Value / 255 + return RGBToColor(round(r + (1.0 - r) * ratio), round(g + (1.0 - g) * ratio), round(b + (1.0 - b) * ratio)) + + +def VolTodB(Value: float) -> float: + """Convert volume as a decimal (0.0 - 1.0) to a decibel value + + ### WARNING: + * For zero volume, this returns 0 instead of -oo dB + + ## Args: + * Value (float): volume + + ## Returns: + * float: volume in decibels + """ + Value = (math.exp(Value * math.log(11)) - 1) * 0.1 + if Value == 0: + return 0 + return round(math.log10(Value) * 20, 1)