Source code for pyflp.channel

# PyFLP - An FL Studio project file (.flp) parser
# Copyright (C) 2022 demberto
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.

"""Contains the types used by channels and the channel rack."""

from __future__ import annotations

import enum
import pathlib
from typing import Any, Iterator, Literal, Tuple, cast

import construct as c
import construct_typed as ct

from pyflp._adapters import LinearMusical, List2Tuple, Log2, LogNormal, StdEnum
from pyflp._descriptors import EventProp, FlagProp, NestedProp, StructProp
from pyflp._events import (
    DATA,
    DWORD,
    TEXT,
    WORD,
    BoolEvent,
    EventEnum,
    F32Event,
    I8Event,
    I32Event,
    StructEventBase,
    U8Event,
    U16Event,
    U16TupleEvent,
    U32Event,
)
from pyflp._models import EventModel, ItemModel, ModelCollection, ModelReprMixin, supports_slice
from pyflp.exceptions import ModelNotFound, NoModelsFound, PropertyCannotBeSet
from pyflp.plugin import BooBass, FruitKick, Plucked, PluginID, PluginProp, VSTPlugin
from pyflp.types import RGBA, MusicalTime

__all__ = [
    "ArpDirection",
    "Automation",
    "AutomationPoint",
    "Channel",
    "Instrument",
    "Layer",
    "ChannelRack",
    "ChannelNotFound",
    "DeclickMode",
    "LFOShape",
    "ReverbType",
    "FX",
    "Reverb",
    "Delay",
    "Envelope",
    "SamplerLFO",
    "Tracking",
    "Keyboard",
    "LevelAdjusts",
    "StretchMode",
    "Time",
    "TimeStretching",
    "Polyphony",
    "Playback",
    "ChannelType",
]

EnvelopeName = Literal["Panning", "Volume", "Mod X", "Mod Y", "Pitch"]
LFOName = EnvelopeName


[docs]class ChannelNotFound(ModelNotFound, KeyError): pass
class AutomationEvent(StructEventBase): @staticmethod def _get_position(stream: c.StreamType, index: int) -> float: cur = stream.tell() position = 0.0 for i in range(index + 1): stream.seek(21 + (i * 24)) position += c.Float64l.parse_stream(stream) stream.seek(cur) return position STRUCT = c.Struct( "_u1" / c.Bytes(4), # 4 # ? Always 1 "lfo.amount" / c.Int32sl, "_u2" / c.Bytes(1), # 9 "_u3" / c.Bytes(2), # 11 "_u4" / c.Bytes(2), # 13 # ? Always 0 "_u5" / c.Bytes(4), # 17 "points" / c.PrefixedArray( c.Int32ul, # 21 c.Struct( "_offset" / c.Float64l * "Change in X-axis w.r.t last point", "position" # TODO Implement a setter / c.IfThenElse( lambda ctx: ctx._index > 0, c.Computed(lambda ctx: AutomationEvent._get_position(ctx._io, ctx._index)), c.Computed(lambda ctx: ctx["_offset"]), ), "value" / c.Float64l, "tension" / c.Float32l, "_u1" / c.Bytes(4), # Linked to tension ), # 24 per struct ), "_u6" / c.GreedyBytes, # TODO Upto a whooping 112 bytes ) class DelayEvent(StructEventBase): STRUCT = c.Struct( "feedback" / c.Optional(c.Int32ul), "pan" / c.Optional(c.Int32sl), "pitch_shift" / c.Optional(c.Int32sl), "echoes" / c.Optional(c.Int32ul), "time" / c.Optional(c.Int32ul), ).compile() @enum.unique class _EnvLFOFlags(enum.IntFlag): EnvelopeTempoSync = 1 << 0 Unknown = 1 << 2 # Occurs for volume envlope only. Likely a bug in FL's serialiser LFOTempoSync = 1 << 1 LFOPhaseRetrig = 1 << 5
[docs]@enum.unique class LFOShape(ct.EnumBase): """Used by :attr:`LFO.shape`.""" Sine = 0 Triangle = 1 Pulse = 2
# FL Studio 2.5.0+ class EnvelopeLFOEvent(StructEventBase): STRUCT = c.Struct( "flags" / c.Optional(StdEnum[_EnvLFOFlags](c.Int32sl)), # 4 "envelope.enabled" / c.Optional(c.Int32sl), # 8 "envelope.predelay" / c.Optional(c.Int32sl), # 12 "envelope.attack" / c.Optional(c.Int32sl), # 16 "envelope.hold" / c.Optional(c.Int32sl), # 20 "envelope.decay" / c.Optional(c.Int32sl), # 24 "envelope.sustain" / c.Optional(c.Int32sl), # 28 "envelope.release" / c.Optional(c.Int32sl), # 32 "envelope.amount" / c.Optional(c.Int32sl), # 36 "lfo.predelay" / c.Optional(c.Int32ul), # 40 "lfo.attack" / c.Optional(c.Int32ul), # 44 "lfo.amount" / c.Optional(c.Int32sl), # 48 "lfo.speed" / c.Optional(c.Int32ul), # 52 "lfo.shape" / c.Optional(StdEnum[LFOShape](c.Int32sl)), # 56 "envelope.attack_tension" / c.Optional(c.Int32sl), # 60 "envelope.decay_tension" / c.Optional(c.Int32sl), # 64 "envelope.release_tension" / c.Optional(c.Int32sl), # 68 ).compile() class LevelAdjustsEvent(StructEventBase): STRUCT = c.Struct( "pan" / c.Optional(c.Int32sl), # 4 "volume" / c.Optional(c.Int32ul), # 8 "_u1" / c.Optional(c.Int32ul), # 12 "mod_x" / c.Optional(c.Int32sl), # 16 "mod_y" / c.Optional(c.Int32sl), # 20 ).compile() class FilterType(ct.EnumBase): FastLP = 0 LP = 1 BP = 2 HP = 3 BS = 4 LPx2 = 5 SVFLP = 6 SVFLPx2 = 7 class LevelsEvent(StructEventBase): STRUCT = c.Struct( "pan" / c.Optional(c.Int32sl), # 4 "volume" / c.Optional(c.Int32ul), # 8 "pitch_shift" / c.Optional(c.Int32sl), # 12 "filter.mod_x" / c.Optional(c.Int32ul), # 16 "filter.mod_y" / c.Optional(c.Int32ul), # 20 "filter.type" / c.Optional(StdEnum[FilterType](c.Int32ul)), # 24 ).compile()
[docs]@enum.unique class ArpDirection(ct.EnumBase): """Used by :attr:`Arp.direction`.""" Off = 0 Up = 1 Down = 2 UpDownBounce = 3 UpDownSticky = 4 Random = 5
[docs]@enum.unique class DeclickMode(ct.EnumBase): OutOnly = 0 TransientNoBleeding = 1 Transient = 2 Generic = 3 Smooth = 4 Crossfade = 5
@enum.unique class _DelayFlags(enum.IntFlag): PingPong = 1 << 1 FatMode = 1 << 2
[docs]@enum.unique class StretchMode(ct.EnumBase): Stretch = -1 Resample = 0 E3Generic = 1 E3Mono = 2 SliceStretch = 3 SliceMap = 4 Auto = 5 E2Generic = 6 E2Transient = 7 E2Mono = 8 E2Speech = 9
class ParametersEvent(StructEventBase): STRUCT = c.Struct( "_u1" / c.Optional(c.Bytes(9)), # 9 "fx.remove_dc" / c.Optional(c.Flag), # 10 "delay.flags" / c.Optional(StdEnum[_DelayFlags](c.Int8ul)), # 11 "keyboard.main_pitch" / c.Optional(c.Flag), # 12 "_u2" / c.Optional(c.Bytes(28)), # 40 "arp.direction" / c.Optional(StdEnum[ArpDirection](c.Int32ul)), # 44 "arp.range" / c.Optional(c.Int32ul), # 48 "arp.chord" / c.Optional(c.Int32ul), # 52 "arp.time" / c.Optional(c.Float32l), # 56 "arp.gate" / c.Optional(c.Float32l), # 60 "arp.slide" / c.Optional(c.Flag), # 61 "_u3" / c.Optional(c.Bytes(1)), # 62 "time.full_porta" / c.Optional(c.Flag), # 63 "keyboard.add_root" / c.Optional(c.Flag), # 64 "time.gate" / c.Optional(c.Int16ul), # 66 "_u4" / c.Optional(c.Bytes(2)), # 68 "keyboard.key_region" / c.Optional(List2Tuple(c.Int32ul[2])), # 76 "_u5" / c.Optional(c.Bytes(4)), # 80 "fx.normalize" / c.Optional(c.Flag), # 81 "fx.inverted" / c.Optional(c.Flag), # 82 "_u6" / c.Optional(c.Bytes(1)), # 83 "content.declick_mode" / c.Optional(StdEnum[DeclickMode](c.Int8ul)), # 84 "fx.crossfade" / c.Optional(c.Int32ul), # 88 "fx.trim" / c.Optional(c.Int32ul), # 92 "arp.repeat" / c.Optional(c.Int32ul), # 96; FL 4.5.2+ "stretching.time" / c.Optional(LinearMusical(c.Int32ul)), # 100 "stretching.pitch" / c.Optional(c.Int32sl), # 104 "stretching.multiplier" / c.Optional(Log2(c.Int32sl, 10000)), # 108 "stretching.mode" / c.Optional(StdEnum[StretchMode](c.Int32sl)), # 112 "_u7" / c.Optional(c.Bytes(21)), # 133 "fx.start" / c.Optional(LogNormal(c.Int16ul[2], (0, 61440))), # 137 "_u8" / c.Optional(c.Bytes(4)), # 141 "fx.length" / c.Optional(LogNormal(c.Int16ul[2], (0, 61440))), # 145 "_u9" / c.Optional(c.Bytes(3)), # 148 "playback.start_offset" / c.Optional(c.Int32ul), # 152 "_u10" / c.Optional(c.Bytes(5)), # 157 "fx.fix_trim" / c.Optional(c.Flag), # 158 (FL 20.8.4 max) "_extra" / c.GreedyBytes, # * 168 as of 20.9.1 ) @enum.unique class _PolyphonyFlags(enum.IntFlag): None_ = 0 Mono = 1 << 0 Porta = 1 << 1 class PolyphonyEvent(StructEventBase): STRUCT = c.Struct( "max" / c.Optional(c.Int32ul), # 4 "slide" / c.Optional(c.Int32ul), # 8 "flags" / c.Optional(StdEnum[_PolyphonyFlags](c.Byte)), # 9 ).compile() class TrackingEvent(StructEventBase): STRUCT = c.Struct( "middle_value" / c.Optional(c.Int32ul), # 4 "pan" / c.Optional(c.Int32sl), # 8 "mod_x" / c.Optional(c.Int32sl), # 12 "mod_y" / c.Optional(c.Int32sl), # 16 ).compile()
[docs]@enum.unique class ChannelID(EventEnum): IsEnabled = (0, BoolEvent) _VolByte = (2, U8Event) _PanByte = (3, U8Event) Zipped = (15, BoolEvent) # _19 = (19, BoolEvent) PingPongLoop = (20, BoolEvent) Type = (21, U8Event) RoutedTo = (22, I8Event) # FXProperties = 27 IsLocked = (32, BoolEvent) #: 12.3+ New = (WORD, U16Event) FreqTilt = (WORD + 5, U16Event) FXFlags = (WORD + 6, U16Event) Cutoff = (WORD + 7, U16Event) _VolWord = (WORD + 8, U16Event) _PanWord = (WORD + 9, U16Event) Preamp = (WORD + 10, U16Event) #: 1.2.12+ FadeOut = (WORD + 11, U16Event) #: 1.7.6+ FadeIn = (WORD + 12, U16Event) # _DotNote = WORD + 13 # _DotPitch = WORD + 14 # _DotMix = WORD + 15 Resonance = (WORD + 19, U16Event) # _LoopBar = WORD + 20 StereoDelay = (WORD + 21, U16Event) #: 1.3.56+ Pogo = (WORD + 22, U16Event) # _DotReso = WORD + 23 # _DotCutOff = WORD + 24 TimeShift = (WORD + 25, U16Event) # _Dot = WORD + 27 # _DotRel = WORD + 32 # _DotShift = WORD + 28 Children = (WORD + 30, U16Event) #: 3.4.0+ Swing = (WORD + 33, U16Event) # Echo = DWORD + 2 RingMod = (DWORD + 3, U16TupleEvent) CutGroup = (DWORD + 4, U16TupleEvent) RootNote = (DWORD + 7, U32Event) # _MainResoCutOff = DWORD + 9 DelayModXY = (DWORD + 10, U16TupleEvent) Reverb = (DWORD + 11, U32Event) #: 1.4.0+ _StretchTime = (DWORD + 12, F32Event) #: 5.0+ FineTune = (DWORD + 14, I32Event) SamplerFlags = (DWORD + 15, U32Event) LayerFlags = (DWORD + 16, U32Event) GroupNum = (DWORD + 17, I32Event) AUSampleRate = (DWORD + 25, U32Event) _Name = TEXT SamplePath = TEXT + 4 Delay = (DATA + 1, DelayEvent) Parameters = (DATA + 7, ParametersEvent) EnvelopeLFO = (DATA + 10, EnvelopeLFOEvent) Levels = (DATA + 11, LevelsEvent) # _Filter = DATA + 12 Polyphony = (DATA + 13, PolyphonyEvent) # _LegacyAutomation = DATA + 15 Tracking = (DATA + 20, TrackingEvent) LevelAdjusts = (DATA + 21, LevelAdjustsEvent) Automation = (DATA + 26, AutomationEvent)
[docs]@enum.unique class DisplayGroupID(EventEnum): Name = TEXT + 39 #: 3.4.0+
[docs]@enum.unique class RackID(EventEnum): Swing = (11, U8Event) _FitToSteps = (13, U8Event) WindowHeight = (DWORD + 5, U32Event)
[docs]@enum.unique class ReverbType(enum.IntEnum): """Used by :attr:`Reverb.type`.""" A = 0 B = 65536
# The type of a channel may decide how a certain event is interpreted. An # example of this is `ChannelID.Levels` event, which is used for storing # volume, pan and pich bend range of any channel other than automations. In # automations it is used for **Min** and **Max** knobs.
[docs]@enum.unique class ChannelType(ct.EnumBase): # cuz Type would be a super generic name """An internal marker used to indicate the type of a channel.""" Sampler = 0 """Used exclusively for the inbuilt Sampler.""" Native = 2 """Used by audio clips and other native FL Studio synths.""" Layer = 3 # 3.4.0+ Instrument = 4 Automation = 5 # 5.0+
class _FXFlags(enum.IntFlag): FadeStereo = 1 << 0 Reverse = 1 << 1 Clip = 1 << 2 SwapStereo = 1 << 8 class _LayerFlags(enum.IntFlag): Random = 1 << 0 Crossfade = 1 << 1 class _SamplerFlags(enum.IntFlag): Resample = 1 << 0 LoadRegions = 1 << 1 LoadSliceMarkers = 1 << 2 UsesLoopPoints = 1 << 3 KeepOnDisk = 1 << 8
[docs]class DisplayGroup(EventModel, ModelReprMixin): def __str__(self) -> str: if self.name is None: return "Unnamed display group" return f"Display group {self.name}" name = EventProp[str](DisplayGroupID.Name)
[docs]class Arp(EventModel, ModelReprMixin): """Used by :class:`Sampler`: and :class:`Instrument`. ![](https://bit.ly/3Lbk7Yi) """ chord = StructProp[int]() """Index of the selected arpeggio chord.""" direction = StructProp[ArpDirection]() gate = StructProp[float]() """Delay between two successive notes played.""" range = StructProp[int]() """Range (in octaves).""" repeat = StructProp[int]() """Number of times a note is repeated. *New in FL Studio v4.5.2*. """ slide = StructProp[bool]() """Whether arpeggio will slide between notes.""" time = StructProp[float]() """Delay between two successive notes played."""
[docs]class Delay(EventModel, ModelReprMixin): """Echo delay / fat mode section. Used by :class:`Sampler` and :class:`Instrument`. ![](https://bit.ly/3RyzbBD) """ echoes = StructProp[int](ChannelID.Delay) """Number of echoes generated for each note. Min = 1. Max = 10.""" fat_mode = FlagProp(_DelayFlags.FatMode, ChannelID.Parameters, prop="delay.flags") """*New in FL Studio v3.4.0*.""" feedback = StructProp[int](ChannelID.Delay) """Factor with which the volume of every next echo is multiplied. Defaults to minimum value. | Type | Value | Representation | |------|-------|----------------| | Min | 0 | 0% | | Max | 25600 | 200% | """ @property def mod_x(self) -> int: """Min = 0. Max = 256. Default = 128.""" return self.events.first(ChannelID.DelayModXY).value[0] @mod_x.setter def mod_x(self, value: int) -> None: event = self.events.first(ChannelID.DelayModXY) event.value = (value, event.value[1]) @property def mod_y(self) -> int: """Min = 0. Max = 256. Default = 128.""" return self.events.first(ChannelID.DelayModXY).value[1] @mod_y.setter def mod_y(self, value: int) -> None: event = self.events.first(ChannelID.DelayModXY) event.value = (event.value[0], value) pan = StructProp[int](ChannelID.Delay) """ | Type | Value | Representation | |---------|-------|----------------| | Min | -6400 | 100% left | | Max | 6400 | 100% right | | Default | 0 | Centred | """ ping_pong = FlagProp( _DelayFlags.PingPong, ChannelID.Parameters, prop="delay.flags", ) """*New in FL Studio v1.7.6*.""" pitch_shift = StructProp[int](ChannelID.Delay) """Pitch shift (in cents). | Min | Max | Default | |-------|-------|---------| | -1200 | 1200 | 0 | """ time = StructProp[int](ChannelID.Delay) """Tempo-synced delay time. PPQ dependant. | Type | Value | Representation | |---------|-----------|----------------| | Min | 0 | 0:00 | | Max | PPQ * 4 | 8:00 | | Default | PPQ * 3/2 | 3:00 | """
[docs]class Filter(EventModel, ModelReprMixin): """Used by :class:`Sampler`. ![](https://bit.ly/3zT5tAH) """ mod_x = StructProp[int](ChannelID.Levels, prop="filter.mod_x") """Filter cutoff. Min = 0. Max = 256. Defaults to maximum.""" mod_y = StructProp[int](ChannelID.Levels, prop="filter.mod_y") """Filter resonance. Min = 0. Max = 256. Defaults to minimum.""" type = StructProp[FilterType](ChannelID.Levels, prop="filter.type") """Defaults to :attr:`FilterType.FastLP`."""
[docs]class LevelAdjusts(EventModel, ModelReprMixin): """Used by :class:`Layer`, :class:`Instrument` and :class:`Sampler`. ![](https://bit.ly/3xkKeGn) *New in FL Studio v3.3.0*. """ mod_x = StructProp[int]() mod_y = StructProp[int]() pan = StructProp[int]() volume = StructProp[int]()
[docs]class Time(EventModel, ModelReprMixin): """Used by :class:`Sampler` and :class:`Instrument`. ![](https://bit.ly/3xjxUGG) """ swing = EventProp[int](ChannelID.Swing) """Percentage of the ``ChannelRack.swing`` that affects this channel. Linear. Min = 0. Max = 128. Defaults to maximum. """ gate = StructProp[int](ChannelID.Parameters, prop="time.gate") """Logarithmic. Defaults to disabled state. | Type | Value | Representation | |----------|-------|----------------| | Min | 450 | 0:03 | | Max | 1446 | 4:00 | | Disabled | 1447 | Off | """ shift = EventProp[int](ChannelID.TimeShift) """Fine time shift. Nonlinear. Defaults to minimum. | Type | Value | Representation | |------|-------|----------------| | Min | 0 | 0:00 | | Max | 1024 | 1:00 | """ full_porta = StructProp[bool](ChannelID.Parameters, prop="time.full_porta") """Whether :attr:`gate` is bypassed when :attr:`Polyphony.porta` is on."""
[docs]class Reverb(EventModel, ModelReprMixin): """Precalculated reverb used by :class:`Sampler`. *New in FL Studio v1.4.0*. """ @property def type(self) -> ReverbType | None: if ChannelID.Reverb in self.events.ids: event = self.events.first(ChannelID.Reverb) return ReverbType.B if event.value >= ReverbType.B else ReverbType.A @type.setter def type(self, value: ReverbType) -> None: if self.mix is None: raise PropertyCannotBeSet(ChannelID.Reverb) self.events.first(ChannelID.Reverb).value = value.value + self.mix @property def mix(self) -> int | None: """Mix % (wet). Defaults to minimum value. | Min | Max | |-----|-----| | 0 | 256 | """ if ChannelID.Reverb in self.events.ids: return self.events.first(ChannelID.Reverb).value - self.type @mix.setter def mix(self, value: int) -> None: if ChannelID.Reverb not in self.events.ids: raise PropertyCannotBeSet(ChannelID.Reverb) self.events.first(ChannelID.Reverb).value += value
[docs]class FX(EventModel, ModelReprMixin): """Pre-computed effects used by :class:`Sampler`. ![](https://bit.ly/3U3Ys8l) ![](https://bit.ly/3qvdBSN) See Also: :attr:`Sampler.fx`, :attr:`Reverb` """ boost = EventProp[int](ChannelID.Preamp) """Pre-amp gain. Defaults to minimum value. | Min | Max | |-----|-----| | 0 | 256 | *New in FL Studio v1.2.12*. """ clip = FlagProp(_FXFlags.Clip, ChannelID.FXFlags) """Whether output is clipped at 0dB for :attr:`boost`.""" crossfade = StructProp[int](ChannelID.Parameters, prop="fx.crossfade") """Linear. Defaults to minimum value | Type | Value | Representation | |------|-------|----------------| | Min | 0 | 0% | | Max | 256 | 100% | """ cutoff = EventProp[int](ChannelID.Cutoff) """Filter Mod X. Defaults to maximum value. Min = 16. Max = 1024.""" fade_in = EventProp[int](ChannelID.FadeIn) """Quick fade-in. Defaults to minimum value. Min = 0. Max = 1024.""" fade_out = EventProp[int](ChannelID.FadeOut) """Quick fade-out. Defaults to minimum value. Min = 0. Max = 1024. *New in FL Studio v1.7.6*. """ fade_stereo = FlagProp(_FXFlags.FadeStereo, ChannelID.FXFlags) fix_trim = StructProp[bool](ChannelID.Parameters, prop="fx.fix_trim") """:menuselection:`Trim --> Fix legacy precomputed length`. Has no effect on the value of :attr:`trim`. """ freq_tilt = EventProp[int](ChannelID.FreqTilt) """Shifts the frequency balance. Bipolar. | Min | Max | Default | |-----|-----|---------| | 0 | 256 | 128 | """ inverted = StructProp[bool](ChannelID.Parameters, prop="fx.inverted") """Named :guilabel:`Reverse polarity` in FL's interface.""" length = StructProp[float](ChannelID.Parameters, prop="fx.length") """Min = 0.0, Max = 1.0. Defaults to minimum value. Named :guilabel:`SMP START` in FL's interface. """ normalize = StructProp[bool](ChannelID.Parameters, prop="fx.normalize") """Maximizes volume without clipping by normalizing peaks to 0dB.""" pogo = EventProp[int](ChannelID.Pogo) """Pitch bend effect. Bipolar. | Min | Max | Default | |-----|-----|---------| | 0 | 512 | 256 | """ remove_dc = StructProp[bool](ChannelID.Parameters, prop="fx.remove_dc") """Whether DC offset (if present) is removed. *New in FL Studio v2.5.0*. """ resonance = EventProp[int](ChannelID.Resonance) """Filter Mod Y. Min = 0. Max = 640. Defaults to minimum value.""" reverb = NestedProp[Reverb](Reverb, ChannelID.Reverb) reverse = FlagProp(_FXFlags.Reverse, ChannelID.FXFlags) """Whether sample is reversed or not.""" ringmod = EventProp[Tuple[int, int]](ChannelID.RingMod) """Ring modulation returned as a tuple of ``(mix, frequency)``. Limits for both: | Min | Max | Default | |-----|-----|---------| | 0 | 256 | 128 | """ start = StructProp[float](ChannelID.Parameters, prop="fx.start") """Min = 0.0, Max = 1.0. Defaults to minimum value. Always set to 0.0 irrespective of the knob position unless a sample is loaded. """ stereo_delay = EventProp[int](ChannelID.StereoDelay) """Linear. Bipolar. | Min | Max | Default | |-----|------|---------| | 0 | 4096 | 2048 | *New in FL Studio v1.3.56*. """ swap_stereo = FlagProp(_FXFlags.SwapStereo, ChannelID.FXFlags) """Whether left and right channels are swapped or not.""" trim = StructProp[int](ChannelID.Parameters, prop="fx.trim") """Silence trimming threshold. Defaults to minimum. Linear. | Type | Value | Representation | |------|-------|----------------| | Min | 0 | 0% | | Max | 256 | 100% | """
[docs]class Envelope(EventModel, ModelReprMixin): """A PAHDSR envelope for various :class:`Sampler` paramters. ![](https://bit.ly/3d9WCCh) See Also: :attr:`Sampler.envelopes` *New in FL Studio v2.5.0*. """ enabled = StructProp[bool](prop="envelope.enabled") """Whether envelope section is enabled.""" predelay = StructProp[int](prop="envelope.predelay") """Linear. Defaults to minimum value. | Type | Value | Representation | |------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | """ amount = StructProp[int](prop="envelope.amount") """Linear. Bipolar. | Type | Value | Representation | |---------|-------|----------------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | 0 | 0% | """ attack = StructProp[int](prop="envelope.attack") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | | Default | 20000 | 31% | """ hold = StructProp[int](prop="envelope.hold") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | | Default | 20000 | 31% | """ decay = StructProp[int](prop="envelope.decay") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | | Default | 30000 | 46% | """ sustain = StructProp[int](prop="envelope.sustain") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 0 | 0% | | Max | 128 | 100% | | Default | 50 | 39% | """ release = StructProp[int](prop="envelope.release") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | | Default | 20000 | 31% | """ synced = FlagProp(_EnvLFOFlags.EnvelopeTempoSync) """Whether envelope is synced to tempo or not.""" attack_tension = StructProp[int](prop="envelope.attack_tension") """Linear. Bipolar. | Type | Value | Representation | |---------|-------|----------------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | 0 | 0% | *New in FL Studio v3.5.4*. """ decay_tension = StructProp[int](prop="envelope.decay_tension") """Linear. Bipolar. | Type | Value | Mix (wet) | |---------|-------|-----------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | 0 | 0% | *New in FL Studio v3.5.4*. """ release_tension = StructProp[int](prop="envelope.release_tension") """Linear. Bipolar. | Type | Value | Mix (wet) | |---------|-------|-----------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | -101 | -79% | *New in FL Studio v3.5.4*. """
[docs]class SamplerLFO(EventModel, ModelReprMixin): """A basic LFO for certain :class:`Sampler` parameters. ![](https://bit.ly/3RG5Jtw) See Also: :attr:`Sampler.lfos` *New in FL Studio v2.5.0*. """ amount = StructProp[int](prop="lfo.amount") """Linear. Bipolar. | Type | Value | Representation | |---------|-------|----------------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | 0 | 0% | """ attack = StructProp[int](prop="lfo.attack") """Linear. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | | Default | 20000 | 31% | """ predelay = StructProp[int](prop="lfo.predelay") """Linear. Defaults to minimum value. | Type | Value | Representation | |---------|-------|----------------| | Min | 100 | 0% | | Max | 65536 | 100% | """ speed = StructProp[int](prop="lfo.speed") """Logarithmic. Provides tempo synced options. | Type | Value | Representation | |---------|-------|----------------| | Min | 200 | 0% | | Max | 65536 | 100% | | Default | 32950 | 50% (16 steps) | """ synced = FlagProp(_EnvLFOFlags.LFOTempoSync) """Whether LFO is synced with tempo.""" retrig = FlagProp(_EnvLFOFlags.LFOPhaseRetrig) """Whether LFO phase is in global / retriggered mode.""" shape = StructProp[LFOShape](prop="lfo.shape") """Sine, triangle or pulse. Default: Sine."""
[docs]class Polyphony(EventModel, ModelReprMixin): """Used by :class:`Sampler` and :class:`Instrument`. ![](https://bit.ly/3DlvWcl) """ mono = FlagProp(_PolyphonyFlags.Mono) """Whether monophonic mode is enabled or not.""" porta = FlagProp(_PolyphonyFlags.Porta) """*New in FL Studio v3.3.0*.""" max = StructProp[int]() """Max number of voices.""" slide = StructProp[int]() """Portamento time. Nonlinear. | Type | Value | Representation | |---------|-------|-----------------| | Min | 0 | 0:00 | | Max | 1660 | 8:00 (8 steps) | | Default | 820 | 0:12 (1/2 step) | *New in FL Studio v3.3.0*. """
[docs]class Tracking(EventModel, ModelReprMixin): """Used by :class:`Sampler` and :class:`Instrument`. ![](https://bit.ly/3DmveM8) *New in FL Studio v3.3.0*. """ middle_value = StructProp[int]() """Note index. Min: C0 (0), Max: B10 (131).""" mod_x = StructProp[int]() """Bipolar. | Min | Max | Default | |------|-----|---------| | -256 | 256 | 0 | """ mod_y = StructProp[int]() """Bipolar. | Min | Max | Default | |------|-----|---------| | -256 | 256 | 0 | """ pan = StructProp[int]() """Linear. Bipolar. | Min | Max | Default | |------|-----|---------| | -256 | 256 | 0 | """
[docs]class Keyboard(EventModel, ModelReprMixin): """Used by :class:`Sampler` and :class:`Instrument`. ![](https://bit.ly/3qwIK8r) *New in FL Studio v1.3.56*. """ fine_tune = EventProp[int](ChannelID.FineTune) """-100 to +100 cents.""" # TODO Return this as a note name, like `Note.key` root_note = EventProp[int](ChannelID.RootNote, default=60) """Min - 0 (C0), Max - 131 (B10).""" main_pitch = StructProp[bool](ChannelID.Parameters, prop="keyboard.main_pitch") """Whether triggered note is affected by changes to :attr:`Project.main_pitch`.""" add_root = StructProp[bool](ChannelID.Parameters, prop="keyboard.add_root") """Whether to add root note (instead of pitch) to triggered note. Named as :guilabel:`Add to key`. Defaults to ``False``. *New in FL Studio v3.4.0*. """ key_region = StructProp[Tuple[int, int]](ChannelID.Parameters, prop="keyboard.key_region") """A `(start_note, end_note)` tuple representing the playable range."""
[docs]class Playback(EventModel, ModelReprMixin): """Used by :class:`Sampler`. ![](https://bit.ly/3xjSypY) """ ping_pong_loop = EventProp[bool](ChannelID.PingPongLoop) start_offset = StructProp[int](ChannelID.Parameters, prop="playback.start_offset") """Linear. Defaults to minimum value. | Type | Value | Representation | |------|------------|----------------| | Min | 0 | 0% | | Max | 1072693248 | 100% | """ use_loop_points = FlagProp(_SamplerFlags.UsesLoopPoints, ChannelID.SamplerFlags)
[docs]class TimeStretching(EventModel, ModelReprMixin): """Used by :class:`Sampler`. ![](https://bit.ly/3eIAjnG) *New in FL Studio v5.0*. """ mode = StructProp[StretchMode](ChannelID.Parameters, prop="stretching.mode") multiplier = StructProp[float](ChannelID.Parameters, prop="stretching.multiplier") """Logarithmic. Bipolar. | Type | Value | Representation | |---------|-------|----------------| | Min | 0.25 | 25% | | Max | 4.0 | 400% | | Default | 0 | 100% | """ pitch = StructProp[int](ChannelID.Parameters, prop="stretching.pitch") """Pitch shift (in cents). Min = -1200. Max = 1200. Defaults to 0.""" time = StructProp[MusicalTime](ChannelID.Parameters, prop="stretching.time") """Returns a tuple of ``(bars, beats, ticks)``."""
[docs]class Content(EventModel, ModelReprMixin): """Used by :class:`Sampler`. ![](https://bit.ly/3TCXFKI) """ declick_mode = StructProp[DeclickMode](ChannelID.Parameters, prop="content.declick_mode") """Defaults to ``DeclickMode.OutOnly``. *New in FL Studio v9.0.0*. """ keep_on_disk = FlagProp(_SamplerFlags.KeepOnDisk, ChannelID.SamplerFlags) """Whether a sample is streamed from disk or kept in RAM, defaults to ``False``. *New in FL Studio v2.5.0*. """ load_regions = FlagProp(_SamplerFlags.LoadRegions, ChannelID.SamplerFlags) """Load regions found in the sample, if any, defaults to ``True``.""" load_slices = FlagProp(_SamplerFlags.LoadSliceMarkers, ChannelID.SamplerFlags) """Defaults to ``False``.""" resample = FlagProp(_SamplerFlags.Resample, ChannelID.SamplerFlags) """Defaults to ``False``. *New in FL Studio v2.5.0*. """
[docs]class AutomationLFO(EventModel, ModelReprMixin): amount = StructProp[int](ChannelID.Automation, prop="lfo.amount") """Linear. Bipolar. | Type | Value | Representation | |---------|------------|----------------| | Min | -128 | -100% | | Max | 128 | 100% | | Default | 64 or 0 | 50% or 0% | """
[docs]class AutomationPoint(ItemModel[AutomationEvent], ModelReprMixin):
[docs] def __setitem__(self, prop: str, value: Any) -> None: self._item[prop] = value self._parent["points"][self._index] = self._item
position = StructProp[int](readonly=True) """PPQ dependant. Position on X-axis. This property cannot be set as of yet. """ tension = StructProp[float]() """A value in the range of 0 to 1.0.""" value = StructProp[float]() """Position on Y-axis in the range of 0 to 1.0."""
[docs]class Channel(EventModel): """Represents a channel in the channel rack.""" def __repr__(self) -> str: return f"{type(self).__name__} (name={self.display_name!r}, iid={self.iid})" color = EventProp[RGBA](PluginID.Color) """Defaults to #5C656A (granite gray). ![](https://bit.ly/3SllDsG) Values below 20 for any color component (R, G or B) are ignored by FL. """ # TODO controllers = KWProp[List[RemoteController]]() internal_name = EventProp[str](PluginID.InternalName) """Internal name of the channel. The value of this depends on the type of `plugin`: * Native (stock) plugin: Empty *afaik*. * VST instruments: "Fruity Wrapper". See Also: :attr:`name` """ enabled = EventProp[bool](ChannelID.IsEnabled) """![](https://bit.ly/3sbN8KU)""" @property def group(self) -> DisplayGroup: # TODO Setter """Display group / filter under which this channel is grouped.""" return self._kw["group"] icon = EventProp[int](PluginID.Icon) """Internal ID of the icon shown beside the ``display_name``. ![](https://bit.ly/3zjK2sf) """ iid = EventProp[int](ChannelID.New) keyboard = NestedProp(Keyboard, ChannelID.FineTune, ChannelID.RootNote, ChannelID.Parameters) """Located at the bottom of :menuselection:`Miscellaneous functions (page)`.""" locked = EventProp[bool](ChannelID.IsLocked) """Whether in a locked state or not; mute / solo acts differently when ``True``. ![](https://bit.ly/3BOBc7j) """ name = EventProp[str](PluginID.Name, ChannelID._Name) """The name associated with a channel. It's value depends on the type of plugin: * Native (stock): User-given name, None if not given one. * VST instrument: The name obtained from the VST or the user-given name. See Also: :attr:`internal_name` and :attr:`display_name`. """ @property def pan(self) -> int | None: """Linear. Bipolar. | Min | Max | Default | |-----|-------|---------| | 0 | 12800 | 6400 | """ if ChannelID.Levels in self.events.ids: return cast(LevelsEvent, self.events.first(ChannelID.Levels))["pan"] for id in (ChannelID._PanWord, ChannelID._PanByte): if id in self.events.ids: return self.events.first(id).value @pan.setter def pan(self, value: int) -> None: if self.pan is None: raise PropertyCannotBeSet if ChannelID.Levels in self.events.ids: cast(LevelsEvent, self.events.first(ChannelID.Levels))["pan"] = value return for id in (ChannelID._PanWord, ChannelID._PanByte): if id in self.events.ids: self.events.first(id).value = value @property def volume(self) -> int | None: """Nonlinear. | Min | Max | Default | |-----|-------|---------| | 0 | 12800 | 10000 | """ if ChannelID.Levels in self.events.ids: return cast(LevelsEvent, self.events.first(ChannelID.Levels))["volume"] for id in (ChannelID._VolWord, ChannelID._VolByte): if id in self.events.ids: return self.events.first(id).value @volume.setter def volume(self, value: int) -> None: if self.volume is None: raise PropertyCannotBeSet if ChannelID.Levels in self.events.ids: cast(LevelsEvent, self.events.first(ChannelID.Levels))["volume"] = value return for id in (ChannelID._VolWord, ChannelID._VolByte): if id in self.events.ids: self.events.first(id).value = value # If the channel is not zipped, underlying event is not stored. @property def zipped(self) -> bool: """Whether the channel is zipped / minimized. ![](https://bit.ly/3S2imib) """ if ChannelID.Zipped in self.events.ids: return self.events.first(ChannelID.Zipped).value return False @property def display_name(self) -> str | None: """The name of the channel that will be displayed in FL Studio.""" return self.name or self.internal_name # type: ignore
[docs]class Automation(Channel, ModelCollection[AutomationPoint]): """Represents an automation clip present in the channel rack. Iterate to get the :attr:`points` inside the clip. >>> repr([point for point in automation]) AutomationPoint(position=0.0, value=1.0, tension=0.5), ... ![](https://bit.ly/3RXQhIN) """
[docs] @supports_slice # type: ignore def __getitem__(self, i: int | slice) -> AutomationPoint: for idx, p in enumerate(self): if idx == i: return p raise ModelNotFound(i)
[docs] def __iter__(self) -> Iterator[AutomationPoint]: """Iterator over the automation points inside the automation clip.""" if ChannelID.Automation in self.events.ids: event = cast(AutomationEvent, self.events.first(ChannelID.Automation)) for i, point in enumerate(event["points"]): yield AutomationPoint(point, i, event)
lfo = NestedProp(AutomationLFO, ChannelID.Automation) # TODO Add image
[docs]class Layer(Channel, ModelCollection[Channel]): """Represents a layer channel present in the channel rack. ![](https://bit.ly/3S2MLgf) *New in FL Studio v3.4.0*. """
[docs] @supports_slice # type: ignore def __getitem__(self, i: int | str | slice) -> Channel: """Returns a child :class:`Channel` with an IID of :attr:`Channel.iid`. Args: i: IID or 0-based index of the child(ren). Raises: ChannelNotFound: Child(ren) with the specific index or IID couldn't be found. This exception derives from ``KeyError`` as well. """ for child in self: if i == child.iid: return child raise ChannelNotFound(i)
[docs] def __iter__(self) -> Iterator[Channel]: if ChannelID.Children in self.events.ids: for event in self.events.get(ChannelID.Children): yield self._kw["channels"][event.value]
[docs] def __len__(self) -> int: """Returns the number of channels whose parent this layer is.""" try: return self.events.count(ChannelID.Children) except KeyError: return 0
def __repr__(self) -> str: return f"{super().__repr__()[:-1]}, {len(self)} children)" crossfade = FlagProp(_LayerFlags.Crossfade, ChannelID.LayerFlags) """:menuselection:`Miscellaneous functions --> Layering`""" random = FlagProp(_LayerFlags.Random, ChannelID.LayerFlags) """:menuselection:`Miscellaneous functions --> Layering`"""
class _SamplerInstrument(Channel): arp = NestedProp(Arp, ChannelID.Parameters) """:menuselection:`Miscellaneous functions -> Arpeggiator`""" cut_group = EventProp[Tuple[int, int]](ChannelID.CutGroup) """Cut group in the form of (Cut self, cut by). :menuselection:`Miscellaneous functions --> Group` Hint: To cut itself when retriggered, set the same value for both. """ delay = NestedProp(Delay, ChannelID.Delay, ChannelID.DelayModXY, ChannelID.Parameters) """:menuselection:`Miscellaneous functions -> Echo delay / fat mode`""" insert = EventProp[int](ChannelID.RoutedTo) """The index of the :class:`Insert` the channel is routed to according to FL. "Current" insert = -1, Master = 0 and so on... till :attr:`Mixer.max_inserts`. """ level_adjusts = NestedProp(LevelAdjusts, ChannelID.LevelAdjusts) """:menuselection:`Miscellaneous functions -> Level adjustments`""" @property def pitch_shift(self) -> int | None: """-4800 to +4800 (cents). Raises: PropertyCannotBeSet: When a `ChannelID.Levels` event is not found. """ if ChannelID.Levels in self.events.ids: return cast(LevelsEvent, self.events.first(ChannelID.Levels))["pitch_shift"] @pitch_shift.setter def pitch_shift(self, value: int) -> None: try: event = self.events.first(ChannelID.Levels) except KeyError as exc: raise PropertyCannotBeSet(ChannelID.Levels) from exc else: cast(LevelsEvent, event)["pitch_shift"] = value polyphony = NestedProp(Polyphony, ChannelID.Polyphony) """:menuselection:`Miscellaneous functions -> Polyphony`""" time = NestedProp(Time, ChannelID.Swing, ChannelID.TimeShift, ChannelID.Parameters) """:menuselection:`Miscellaneous functions -> Time`""" @property def tracking(self) -> dict[str, Tracking] | None: """A :class:`Tracking` each for Volume & Keyboard. :menuselection:`Miscellaneous functions -> Tracking` """ if ChannelID.Tracking in self.events.ids: tracking = [Tracking(e) for e in self.events.separate(ChannelID.Tracking)] return dict(zip(("volume", "keyboard"), tracking))
[docs]class Instrument(_SamplerInstrument): """Represents a native or a 3rd party plugin loaded in a channel.""" plugin = PluginProp(VSTPlugin, BooBass, FruitKick, Plucked) """The plugin loaded into the channel."""
# TODO New in FL Studio v1.4.0 & v1.5.23: Sampler spectrum views
[docs]class Sampler(_SamplerInstrument): """Represents the native Sampler, either as a clip or a channel. ![](https://bit.ly/3DlHPiI) """ def __repr__(self) -> str: return f"{super().__repr__()[:-1]}, sample_path={self.sample_path!r})" au_sample_rate = EventProp[int](ChannelID.AUSampleRate) """AU-format sample specific.""" content = NestedProp(Content, ChannelID.SamplerFlags, ChannelID.Parameters) """:menuselection:`Sample settings --> Content`""" # FL's interface doesn't have an envelope for panning, but still stores # the default values in event data. @property def envelopes(self) -> dict[EnvelopeName, Envelope] | None: """An :class:`Envelope` each for Volume, Panning, Mod X, Mod Y and Pitch. :menuselection:`Envelope / instruement settings` """ if ChannelID.EnvelopeLFO in self.events.ids: envs = [Envelope(e) for e in self.events.separate(ChannelID.EnvelopeLFO)] return dict(zip(EnvelopeName.__args__, envs)) # type: ignore filter = NestedProp(Filter, ChannelID.Levels) fx = NestedProp( FX, ChannelID.Cutoff, ChannelID.FadeIn, ChannelID.FadeOut, ChannelID.FreqTilt, ChannelID.Parameters, ChannelID.Pogo, ChannelID.Preamp, ChannelID.Resonance, ChannelID.Reverb, ChannelID.RingMod, ChannelID.StereoDelay, ChannelID.FXFlags, ) """:menuselection:`Sample settings (page) --> Precomputed effects`""" @property def lfos(self) -> dict[LFOName, SamplerLFO] | None: """An :class:`LFO` each for Volume, Panning, Mod X, Mod Y and Pitch. :menuselection:`Envelope / instruement settings (page)` """ if ChannelID.EnvelopeLFO in self.events.ids: lfos = [SamplerLFO(e) for e in self.events.separate(ChannelID.EnvelopeLFO)] return dict(zip(LFOName.__args__, lfos)) # type: ignore playback = NestedProp( Playback, ChannelID.SamplerFlags, ChannelID.PingPongLoop, ChannelID.Parameters ) """:menuselection:`Sample settings (page) --> Playback`""" @property def sample_path(self) -> pathlib.Path | None: """Absolute path of a sample file on the disk. :menuselection:`Sample settings (page) --> File` Contains the string ``%FLStudioFactoryData%`` for stock samples. """ if ChannelID.SamplePath in self.events.ids: return pathlib.Path(self.events.first(ChannelID.SamplePath).value) @sample_path.setter def sample_path(self, value: pathlib.Path) -> None: if self.sample_path is None: raise PropertyCannotBeSet(ChannelID.SamplePath) path = "" if str(value) == "." else str(value) self.events.first(ChannelID.SamplePath).value = path # TODO Find whether ChannelID._StretchTime was really used for attr ``time``. stretching = NestedProp(TimeStretching, ChannelID.Parameters) """:menuselection:`Sample settings (page) --> Time stretching`"""
[docs]class ChannelRack(EventModel, ModelCollection[Channel]): """Represents the channel rack, contains all :class:`Channel` instances. ![](https://bit.ly/3RXR50h) """ def __repr__(self) -> str: return f"ChannelRack - {len(self)} channels"
[docs] @supports_slice # type: ignore def __getitem__(self, i: str | int | slice) -> Channel: """Gets a channel from the rack based on its IID or name. Args: i: Compared with :attr:`Channel.iid` if an int or slice or with the :attr:`Channel.display_name`. Raises: ChannelNotFound: A channel with the specified IID or name isn't found. """ for ch in self: if (isinstance(i, int) and i == ch.iid) or (i == ch.display_name): return ch raise ChannelNotFound(i)
[docs] def __iter__(self) -> Iterator[Channel]: """Yields all the channels found in the project.""" ch_dict: dict[int, Channel] = {} groups = [DisplayGroup(et) for et in self.events.separate(DisplayGroupID.Name)] for et in self.events.divide(ChannelID.New, *ChannelID, *PluginID): iid = et.first(ChannelID.New).value typ = et.first(ChannelID.Type).value groupnum = et.first(ChannelID.GroupNum).value ct = Channel # prevent type error and logic failure below if typ == ChannelType.Automation: ct = Automation elif typ == ChannelType.Layer: ct = Layer elif typ == ChannelType.Sampler: ct = Sampler elif typ in (ChannelType.Instrument, ChannelType.Native): ct = Instrument # Audio clips are stored as Instrument until a sample is loaded in them if all(id in et for id in (ChannelID.SamplePath, PluginID.InternalName)): if not et.first(PluginID.InternalName).value and ct == Instrument: ct = Sampler if iid is not None: cur_ch = ch_dict[iid] = ct(et, channels=ch_dict, group=groups[groupnum]) yield cur_ch
[docs] def __len__(self) -> int: """Returns the number of channels found in the project. Raises: NoModelsFound: No channels could be found in the project. """ if ChannelID.New not in self.events.ids: raise NoModelsFound return self.events.count(ChannelID.New)
@property def automations(self) -> Iterator[Automation]: """Yields automation clips in the project.""" yield from (ch for ch in self if isinstance(ch, Automation)) # TODO Find out what this meant fit_to_steps = EventProp[int](RackID._FitToSteps) @property def groups(self) -> Iterator[DisplayGroup]: for ed in self.events.separate(DisplayGroupID.Name): yield DisplayGroup(ed) height = EventProp[int](RackID.WindowHeight) """Window height of the channel rack in the interface (in pixels).""" @property def instruments(self) -> Iterator[Instrument]: """Yields native and 3rd-party synth channels in the project.""" yield from (ch for ch in self if isinstance(ch, Instrument)) @property def layers(self) -> Iterator[Layer]: """Yields ``Layer`` channels in the project.""" yield from (ch for ch in self if isinstance(ch, Layer)) @property def samplers(self) -> Iterator[Sampler]: """Yields samplers and audio clips in the project.""" yield from (ch for ch in self if isinstance(ch, Sampler)) swing = EventProp[int](RackID.Swing) """Global channel swing mix. Linear. Defaults to minimum value. | Type | Value | Mix (wet) | |------|-------|-----------| | Min | 0 | 0% | | Max | 128 | 100% | """