Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/img/plugin/generators/fruity-slicer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions pyflp/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
)
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.plugin import BooBass, FruitKick, FruitySlicer, Plucked, PluginID, PluginProp, VSTPlugin
from pyflp.types import RGBA, MusicalTime

__all__ = [
Expand Down Expand Up @@ -1442,7 +1442,7 @@ def tracking(self) -> dict[str, Tracking] | None:
class Instrument(_SamplerInstrument):
"""Represents a native or a 3rd party plugin loaded in a channel."""

plugin = PluginProp(VSTPlugin, BooBass, FruitKick, Plucked)
plugin = PluginProp(VSTPlugin, BooBass, FruitKick, FruitySlicer, Plucked)
"""The plugin loaded into the channel."""


Expand Down
122 changes: 122 additions & 0 deletions pyflp/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"FruityFastDist",
"FruityNotebook2",
"FruitySend",
"FruitySlicer",
"FruitySoftClipper",
"FruityStereoEnhancer",
"Plucked",
Expand Down Expand Up @@ -155,6 +156,49 @@ class FruitySendEvent(StructEventBase):
).compile()


class FruitySlicerEvent(StructEventBase):
STRUCT = c.Struct(
"_u1" / c.Bytes(8),
"bpm" / c.Float32l,
"pitch_shift" / c.Int32sl,
"time_stretch" / c.Int32sl,
"stretching_method"
/ c.Enum(
c.Int32ul,
fill_gaps=0,
alt_fill_gaps=1,
pro_default=2,
pro_transient=3,
transient=4,
tonal=5,
monophonic=6,
speech=7,
),
"fade_in" / c.Int32ul,
"fade_out" / c.Int32ul,
"file_path" / c.PascalString(c.Int8ul, "utf-8"),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a VarInt prefixed string. If 2 bytes are used to encode a path having >127 characters then its definitely VarInt prefixed.

"slices"
/ c.PrefixedArray(
c.Int32ul,
c.Struct(
"name" / c.PascalString(c.Int8ul, "utf-8"), # TODO: This is a special format
"sample_offset" / c.Int32ul,
"key" / c.Int32sl,
"_u1" / c.Float32l,
"reversed" / c.Flag,
),
),
"animate" / c.Flag,
"start_note" / c.Int32ul,
"play_to_end" / c.Flag,
"bitrate" / c.Int32ul,
"auto_dump" / c.Flag,
"declick" / c.Flag,
"auto_fit" / c.Flag,
"view_spectrum" / FourByteBool,
).compile()


class FruitySoftClipperEvent(StructEventBase):
STRUCT = c.Struct("threshold" / c.Int32ul, "post" / c.Int32ul).compile()

Expand Down Expand Up @@ -1007,6 +1051,84 @@ class FruitySend(_PluginBase[FruitySendEvent], _IPlugin, ModelReprMixin):
"""


class FruitySlicer(_PluginBase[FruitySlicerEvent], _IPlugin, ModelReprMixin):
"""![](https://bit.ly/46mngih)"""

INTERNAL_NAME = "Fruity Slicer"
bpm = _NativePluginProp[float]()
"""The BPM (beats per minute) of the sample."""

pitch_shift = _NativePluginProp[int]()
"""Pitch shift, in cents. Linear.

| Type | Value | Representation |
|---------|-------|----------------|
| Min | -1200 | -1200 cents |
| Max | 1200 | +1200 cents |
| Default | 0 | +0 cent |
"""

time_stretch = _NativePluginProp[int]()
"""Logarithmic.

| Type | Value | Representation |
|---------|--------|-------------------|
| Min | -20000 | 25% / 60-240 bpm |
| Max | 20000 | 400% / 60-15 bpm |
| Default | 0 | 100% / 0-0 bpm |
"""

stretching_method = _NativePluginProp[
Literal[
"fill_gaps",
"alt_fill_gaps",
"pro_default",
"pro_transient",
"transient",
"tonal",
"monophonic",
"speech",
]
]()
"""The stretching method to use on the sample when `time_stretch` is not 0."""

fade_in = _NativePluginProp[int]()
"""Slice fade in, in milliseconds."""

fade_out = _NativePluginProp[int]()
"""Slice fade out, in milliseconds."""

file_path = _NativePluginProp[str]()
"""The file path of the sample."""

slices = _NativePluginProp[list[dict]]()
"""A list of slices."""

animate = _NativePluginProp[bool]()
"""Whether to highlight the slices as they are played."""

start_note = _NativePluginProp[int]()
"""The MIDI note for slicing to start on. Default 60."""

play_to_end = _NativePluginProp[bool]()
"""Whether to play slices to the end of the sample."""

bitrate = _NativePluginProp[int]()
"""The bitrate of the sample."""

auto_dump = _NativePluginProp[bool]()
"""Whether to automatically dump slices to the piano roll."""

declick = _NativePluginProp[bool]()
"""Whether to prevent clicking on slices."""

auto_fit = _NativePluginProp[bool]()
"""Whether to automatically fit the beat to the project tempo on load."""

view_spectrum = _NativePluginProp[bool]()
"""Whether to view the slices as a spectrum instead of a waveform."""


class FruitySoftClipper(_PluginBase[FruitySoftClipperEvent], _IPlugin, ModelReprMixin):
"""![](https://bit.ly/3BCWfJX)"""

Expand Down
Binary file added tests/assets/plugins/fruity-slicer.fst
Binary file not shown.
27 changes: 27 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FruityCenter,
FruityFastDist,
FruitySend,
FruitySlicer,
FruitySoftClipper,
FruityStereoEnhancer,
Plucked,
Expand Down Expand Up @@ -81,6 +82,32 @@ def test_fruity_send():
assert fruity_send.volume == 256


def test_fruity_slicer():
fruity_slicer = get_plugin("fruity-slicer.fst", FruitySlicer)
assert fruity_slicer.bpm == 60
assert fruity_slicer.pitch_shift == 100
assert fruity_slicer.time_stretch == 4300
assert fruity_slicer.stretching_method == "transient"
assert fruity_slicer.fade_in == 56
assert fruity_slicer.fade_out == 5
assert fruity_slicer.file_path == r"Z:\home\user\Music\audio.wav"

test_slice = fruity_slicer.slices[0]
assert test_slice.name == "1 - Beat 1"
assert test_slice.sample_offset == 0
assert test_slice.key == -1
assert test_slice.reversed is False

assert fruity_slicer.animate is False
assert fruity_slicer.start_note == 0
assert fruity_slicer.play_to_end is False
assert fruity_slicer.bitrate == 44100
assert fruity_slicer.auto_dump is False
assert fruity_slicer.declick is True
assert fruity_slicer.auto_fit is False
assert fruity_slicer.view_spectrum is False


def test_fruity_soft_clipper():
fruity_soft_clipper = get_plugin("fruity-soft-clipper.fst", FruitySoftClipper)
assert fruity_soft_clipper.threshold == 100
Expand Down