Skip to content

Commit

Permalink
feat: Pattern.timemarkers
Browse files Browse the repository at this point in the history
  • Loading branch information
demberto committed Dec 10, 2022
1 parent a8caf71 commit 19ab339
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 103 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [2.0.0a7] - Unreleased

### Added

- `Pattern` timemarkers [#27].

### Changed

- Renamed `PlaylistEvent.track_index` to `PlaylistEvent.track_rvidx`.
Expand All @@ -22,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `Patterns.__getitem__` didn't work with pattern names as documented.

[#27]: https://github.com/demberto/PyFLP/issues/27

## [2.0.0a6] - 2022-11-19

Expand Down
22 changes: 0 additions & 22 deletions docs/reference/arrangements.rst
Expand Up @@ -23,25 +23,6 @@ Playlist
:members:
:show-inheritance: PLItemBase

TimeMarker
----------

.. autoclass:: TimeMarker
:members:

.. grid::

.. grid-item::

.. autoclass:: TimeMarkerType
:members:

.. grid-item::
:child-align: center
:columns: auto

.. image:: /img/arrangement/timemarker/action.png

Track
-----

Expand Down Expand Up @@ -102,9 +83,6 @@ Event IDs
.. autoclass:: ArrangementID
:members:
:member-order: bysource
.. autoclass:: TimeMarkerID
:members:
:member-order: bysource
.. autoclass:: TrackID
:members:
:member-order: bysource
26 changes: 26 additions & 0 deletions docs/reference/timemarkers.rst
@@ -0,0 +1,26 @@
Timemarkers
===========

.. module:: pyflp.timemarker
.. autoclass:: TimeMarker
:members:

.. grid::

.. grid-item::

.. autoclass:: TimeMarkerType
:members:

.. grid-item::
:child-align: center
:columns: auto

.. image:: /img/arrangement/timemarker/action.png

Event IDs
---------

.. autoclass:: TimeMarkerID
:members:
:member-order: bysource
65 changes: 3 additions & 62 deletions pyflp/arrangement.py
Expand Up @@ -11,7 +11,7 @@
# GNU General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.

"""Contains the types used by timemarkers, tracks and arrangements."""
"""Contains the types used by tracks and arrangements."""

from __future__ import annotations

Expand Down Expand Up @@ -61,12 +61,11 @@
from .channel import Channel, ChannelRack
from .exceptions import ModelNotFound, NoModelsFound
from .pattern import Pattern, Patterns
from .timemarker import TimeMarker, TimeMarkerID

__all__ = [
"Arrangements",
"Arrangement",
"TimeMarker",
"TimeMarkerType",
"Track",
"TrackMotion",
"TrackPress",
Expand Down Expand Up @@ -167,28 +166,12 @@ class ArrangementID(EventEnum):
Playlist = (DATA + 25, PlaylistEvent)


@enum.unique
class TimeMarkerID(EventEnum):
Numerator = (33, U8Event)
Denominator = (34, U8Event)
Position = (DWORD + 20, U32Event)
Name = TEXT + 13


@enum.unique
class TrackID(EventEnum):
Name = TEXT + 47
Data = (DATA + 30, TrackEvent)


class TimeMarkerType(enum.IntEnum):
Marker = 0
"""Normal text marker."""

Signature = 134217728
"""Used for time signature markers."""


class PLItemBase(ItemModel[PlaylistEvent], ModelReprMixin):
group = StructProp[int]()
"""Returns 0 for no group, else a group number for clips in the same group."""
Expand Down Expand Up @@ -244,49 +227,7 @@ def pattern(self) -> Pattern:
@pattern.setter
def pattern(self, pattern: Pattern):
self._kw["pattern"] = pattern
self["item_index"] = pattern.index + self["pattern_base"]


class TimeMarker(EventModel):
"""A marker in the timeline of an :class:`Arrangement`.
![](https://bit.ly/3gltKbt)
"""

def __repr__(self):
if self.type == TimeMarkerType.Marker:
if self.name:
return f"Marker {self.name!r} @ {self.position!r}"
return f"Unnamed marker @ {self.position!r}"

time_sig = f"{self.numerator}/{self.denominator}"
if self.name:
return f"Signature {self.name!r} ({time_sig}) @ {self.position!r}"
return f"Unnamed {time_sig} signature @ {self.position!r}"

denominator: EventProp[int] = EventProp[int](TimeMarkerID.Denominator)
name = EventProp[str](TimeMarkerID.Name)
numerator = EventProp[int](TimeMarkerID.Numerator)

@property
def position(self) -> int | None:
if TimeMarkerID.Position in self.events.ids:
event = self.events.first(TimeMarkerID.Position)
if event.value < TimeMarkerType.Signature:
return event.value
return event.value - TimeMarkerType.Signature

@property
def type(self) -> TimeMarkerType | None:
"""The action with which a time marker is associated.
[![](https://bit.ly/3RDM1yn)]()
"""
if TimeMarkerID.Position in self.events.ids:
event = self.events.first(TimeMarkerID.Position)
if event.value >= TimeMarkerType.Signature:
return TimeMarkerType.Signature
return TimeMarkerType.Marker
self["item_index"] = pattern.iid + self["pattern_base"]


class _TrackColorProp(StructProp[colour.Color]):
Expand Down
16 changes: 9 additions & 7 deletions pyflp/pattern.py
Expand Up @@ -16,7 +16,6 @@
from __future__ import annotations

import enum
import warnings
from collections import defaultdict
from typing import DefaultDict, Iterator, cast

Expand All @@ -41,6 +40,7 @@
)
from ._models import EventModel, ItemModel
from .exceptions import ModelNotFound, NoModelsFound
from .timemarker import TimeMarker, TimeMarkerID

__all__ = ["Note", "Controller", "Pattern", "Patterns"]

Expand Down Expand Up @@ -87,9 +87,6 @@ class PatternsID(EventEnum):

# ChannelIID, _161, _162, Looped, Length occur when pattern is looped.
# ChannelIID and _161 occur for every channel in order.
# ! Looping a pattern puts timemarkers in it. The same TimeMarkerID events are
# ! used, which means I need to refactor it out from pyflp.arrangement.
# TODO Patterns share TimeMarker events with Arrangements
class PatternID(EventEnum):
Looped = (26, BoolEvent)
New = (WORD + 1, U16Event) # Marks the beginning of a new pattern, twice.
Expand Down Expand Up @@ -223,8 +220,6 @@ class Controller(ItemModel[ControllerEvent]):
value = StructProp[float]()


# As of the latest version of FL, note and controller events are stored before
# all channel events (if they exist). The rest is stored later on as it occurs.
class Pattern(EventModel):
"""Represents a pattern which can contain notes, controllers and time markers."""

Expand Down Expand Up @@ -299,6 +294,11 @@ def notes(self) -> Iterator[Note]:
event = cast(NotesEvent, self.events.first(PatternID.Notes))
yield from (Note(item, i, event) for i, item in enumerate(event))

@property
def timemarkers(self) -> Iterator[TimeMarker]:
"""Yields timemarkers inside this pattern."""
yield from (TimeMarker(et) for et in self.events.group(*TimeMarkerID))


class Patterns(EventModel):
def __repr__(self):
Expand All @@ -320,6 +320,8 @@ def __getitem__(self, i: int | str) -> Pattern:
return pattern
raise ModelNotFound(i)

# Doesn't use EventTree delegates since PatternID.New occurs twice.
# Once for note and controller events and again for the rest of them.
def __iter__(self) -> Iterator[Pattern]:
"""An iterator over the patterns found in the project."""
cur_pat_id = 0
Expand All @@ -329,7 +331,7 @@ def __iter__(self) -> Iterator[Pattern]:
if ie.e.id == PatternID.New:
cur_pat_id = ie.e.value

if ie.e.id in PatternID:
if ie.e.id in (*PatternID, *TimeMarkerID):
tmp_dict[cur_pat_id].append(ie)

for events in tmp_dict.values():
Expand Down
35 changes: 23 additions & 12 deletions pyflp/project.py
Expand Up @@ -53,24 +53,22 @@
U32Event,
)
from ._models import EventModel, FLVersion
from .arrangement import (
ArrangementID,
Arrangements,
ArrangementsID,
TimeMarkerID,
TrackID,
)
from .arrangement import ArrangementID, Arrangements, ArrangementsID, TrackID
from .channel import ChannelID, ChannelRack, DisplayGroupID, RackID
from .exceptions import PropertyCannotBeSet
from .mixer import InsertID, Mixer, MixerID, SlotID
from .pattern import PatternID, Patterns, PatternsID
from .plugin import PluginID
from .timemarker import TimeMarkerID

__all__ = ["PanLaw", "Project", "FileFormat", "VALID_PPQS"]

_DELPHI_EPOCH: Final = datetime.datetime(1899, 12, 30)
MIN_TEMPO: Final = 10.000
VALID_PPQS: Final = (24, 48, 72, 96, 120, 144, 168, 192, 384, 768, 960)
"""Minimum tempo (in BPM) FL Studio supports."""

__all__ = ["PanLaw", "Project", "FileFormat", "VALID_PPQS"]
VALID_PPQS: Final = (24, 48, 72, 96, 120, 144, 168, 192, 384, 768, 960)
"""PPQs / timebase supported by FL Studio as of its latest version."""


class TimestampEvent(StructEventBase):
Expand Down Expand Up @@ -366,9 +364,22 @@ def select(e: AnyEvent):
@property
def patterns(self) -> Patterns:
"""Returns a collection of patterns and other related properties."""
return Patterns(
self.events.subtree(lambda e: e.id in (*PatternID, *PatternsID))
)
arrnew_occured = False

def select(e: AnyEvent):
nonlocal arrnew_occured

if e.id == ArrangementID.New:
arrnew_occured = True

# * Prevents accidentally passing on Arrangement's timemarkers
elif e.id in TimeMarkerID and not arrnew_occured:
return True

elif e.id in (*PatternID, *PatternsID):
return True

return Patterns(self.events.subtree(select))

pan_law = EventProp[PanLaw](ProjectID.PanLaw)
"""Whether a circular or a triangular pan law is used for the project.
Expand Down
67 changes: 67 additions & 0 deletions pyflp/timemarker.py
@@ -0,0 +1,67 @@
"""Contains the types required for pattern and playlist timemarkers."""

import enum

from ._descriptors import EventProp
from ._events import DWORD, TEXT, EventEnum, U8Event, U32Event
from ._models import EventModel

__all__ = ["TimeMarkerID", "TimeMarkerType", "TimeMarker"]


@enum.unique
class TimeMarkerID(EventEnum):
Numerator = (33, U8Event)
Denominator = (34, U8Event)
Position = (DWORD + 20, U32Event)
Name = TEXT + 13


class TimeMarkerType(enum.IntEnum):
Marker = 0
"""Normal text marker."""

Signature = 134217728
"""Used for time signature markers."""


class TimeMarker(EventModel):
"""A marker in the timeline of an :class:`Arrangement`.
![](https://bit.ly/3gltKbt)
"""

def __repr__(self):
if self.type == TimeMarkerType.Marker:
if self.name:
return f"Marker {self.name!r} @ {self.position!r}"
return f"Unnamed marker @ {self.position!r}"

time_sig = f"{self.numerator}/{self.denominator}"
if self.name:
return f"Signature {self.name!r} ({time_sig}) @ {self.position!r}"
return f"Unnamed {time_sig} signature @ {self.position!r}"

denominator: EventProp[int] = EventProp[int](TimeMarkerID.Denominator)
name = EventProp[str](TimeMarkerID.Name)
numerator = EventProp[int](TimeMarkerID.Numerator)

@property
def position(self) -> int | None:
if TimeMarkerID.Position in self.events.ids:
event = self.events.first(TimeMarkerID.Position)
if event.value < TimeMarkerType.Signature:
return event.value
return event.value - TimeMarkerType.Signature

@property
def type(self) -> TimeMarkerType | None:
"""The action with which a time marker is associated.
[![](https://bit.ly/3RDM1yn)]()
"""
if TimeMarkerID.Position in self.events.ids:
event = self.events.first(TimeMarkerID.Position)
if event.value >= TimeMarkerType.Signature:
return TimeMarkerType.Signature
return TimeMarkerType.Marker
4 changes: 4 additions & 0 deletions tests/test_pattern.py
Expand Up @@ -27,6 +27,10 @@ def test_pattern_names(patterns: Patterns):
)


def test_pattern_timemarkers(patterns: Patterns):
assert len(tuple(patterns["Timemarkers"].timemarkers)) == 5


def test_empty_pattern():
assert not len(get_notes("empty.fsc"))

Expand Down

0 comments on commit 19ab339

Please sign in to comment.