Skip to content

Commit

Permalink
feat: closes #86, closes #87, via #75
Browse files Browse the repository at this point in the history
  • Loading branch information
demberto committed Oct 25, 2022
1 parent 1c8cf26 commit 4f380ae
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
``FX.length``, ``FX.normalize``, ``FX.inverted``, ``FX.start`` [#55].
- Normalized linear values for certain properties, more user friendly to deal with.
The required encode / decode is done at event level itself.
- ``TimeStretching.time``, ``TimeStretching.pitch``, ``TimeStretching.multiplier`` [#87].

### Changed

Expand All @@ -28,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

[#55]: https://github.com/demberto/PyFLP/issues/55
[#84]: https://github.com/demberto/PyFLP/issues/84
[#87]: https://github.com/demberto/PyFLP/issues/87

## [2.0.0a4] - 2022-10-22

Expand Down
Binary file modified docs/img/channel/stretching.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 38 additions & 1 deletion pyflp/_descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import enum
import math
import sys
from typing import Any, List, TypeVar, Union, overload
import warnings
from typing import Any, List, NamedTuple, TypeVar, Union, overload

if sys.version_info >= (3, 8):
from typing import Protocol, final, runtime_checkable
Expand Down Expand Up @@ -234,6 +235,42 @@ def _set(self, ev_or_ins: Any, value: T):
ET = TypeVar("ET", bound=Union[ct.EnumBase, enum.IntFlag])


class MusicalTime(NamedTuple):
bars: int
"""1 bar == 16 beats == 768 (internal representation)"""

beats: int
"""1 beat == 240 ticks == 48 (internal representation)"""

ticks: int
"""5 ticks == 1 (internal representation)"""


class LinearMusical(ct.Adapter[int, int, MusicalTime, MusicalTime]):
def _encode(self, obj: MusicalTime, *_: Any) -> int:
if obj.ticks % 5:
warnings.warn("Ticks must be a multiple of 5", UserWarning)

return (obj.bars * 768) + (obj.beats * 48) + int(obj.ticks * 0.2)

def _decode(self, obj: int, *_: Any) -> MusicalTime:
bars, remainder = divmod(obj, 768)
beats, remainder = divmod(remainder, 48)
return MusicalTime(bars, beats, ticks=remainder * 5)


class Log2(ct.Adapter[int, int, float, float]):
def __init__(self, subcon: Any, factor: int):
super().__init__(subcon)
self.factor = factor

def _encode(self, obj: float, *_: Any) -> int:
return int(self.factor * math.log2(obj))

def _decode(self, obj: int, *_: Any) -> float:
return 2 ** (obj / self.factor)


# Thanks to @algmyr from Python Discord server for finding out the formulae used
# ! See https://github.com/construct/construct/issues/999
class LogNormal(ct.Adapter[List[int], List[int], float, float]):
Expand Down
34 changes: 24 additions & 10 deletions pyflp/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
EventProp,
FlagProp,
KWProp,
LinearMusical,
Log2,
LogNormal,
MusicalTime,
NestedProp,
StdEnum,
StructProp,
Expand Down Expand Up @@ -265,7 +268,9 @@ class ParametersEvent(StructEventBase):
"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+
"_u5" / c.Optional(c.Bytes(12)), # 108
"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
"_u6" / c.Optional(c.Bytes(21)), # 133
"fx.start" / LogNormal(c.Int16ul[2], (0, 61440)), # 137
Expand Down Expand Up @@ -354,7 +359,7 @@ class ChannelID(EventEnum):
# _MainResoCutOff = DWORD + 9
# DelayModXY = DWORD + 10
Reverb = (DWORD + 11, U32Event) #: 1.4.0+
StretchTime = (DWORD + 12, F32Event) #: 5.0+
_StretchTime = (DWORD + 12, F32Event) #: 5.0+
FineTune = (DWORD + 14, I32Event)
SamplerFlags = (DWORD + 15, U32Event)
LayerFlags = (DWORD + 16, U32Event)
Expand Down Expand Up @@ -1028,9 +1033,21 @@ class TimeStretching(EventModel, ModelReprMixin):
"""

mode = StructProp[StretchMode](ChannelID.Parameters, prop="stretching.mode")
# multiplier: Optional[int] = None
# pitch: Optional[int] = None
time = EventProp[float](ChannelID.StretchTime)
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)``."""


class Content(EventModel, ModelReprMixin):
Expand Down Expand Up @@ -1447,11 +1464,8 @@ def sample_path(self, value: pathlib.Path):
path = "" if str(value) == "." else str(value)
self.events.first(ChannelID.SamplePath).value = path

stretching = NestedProp(
TimeStretching,
ChannelID.StretchTime,
ChannelID.Parameters,
)
# TODO Find whether ChannelID._StretchTime was really used for attr ``time``.
stretching = NestedProp(TimeStretching, ChannelID.Parameters)
""":menuselection:`Sample settings (page) --> Time stretching`"""


Expand Down
9 changes: 9 additions & 0 deletions tests/test_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LFOShape,
ReverbType,
Sampler,
StretchMode,
)

from .conftest import ModelFixture
Expand Down Expand Up @@ -271,3 +272,11 @@ def test_sampler_playback(load_sampler: SamplerFixture):
assert playback.use_loop_points
assert playback.ping_pong_loop
assert playback.start_offset == 1072693248


def test_sampler_stretching(load_sampler: SamplerFixture):
stretching = load_sampler("sampler-stretching.fst").stretching
assert stretching.mode == StretchMode.E3Generic
assert stretching.multiplier == 0.25
assert stretching.pitch == 1200
assert stretching.time == (4, 0, 0)

0 comments on commit 4f380ae

Please sign in to comment.