From 7a9bb8cd76a4844faf9b5601f5d4174d189ea44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9phine=20Wolf=20Oberholtzer?= Date: Thu, 27 Feb 2025 13:28:16 -0500 Subject: [PATCH 1/5] Consolidate clock modules --- supriya/clocks/__init__.py | 4 +- supriya/clocks/asynchronous.py | 4 +- supriya/clocks/{bases.py => core.py} | 209 ++++++++++++++++++++++++--- supriya/clocks/ephemera.py | 143 ------------------ supriya/clocks/eventqueue.py | 44 ------ supriya/clocks/offline.py | 3 +- supriya/clocks/threaded.py | 3 +- 7 files changed, 197 insertions(+), 213 deletions(-) rename supriya/clocks/{bases.py => core.py} (86%) delete mode 100644 supriya/clocks/ephemera.py delete mode 100644 supriya/clocks/eventqueue.py diff --git a/supriya/clocks/__init__.py b/supriya/clocks/__init__.py index af6bf6e58..5021b70e7 100644 --- a/supriya/clocks/__init__.py +++ b/supriya/clocks/__init__.py @@ -1,6 +1,6 @@ from .asynchronous import AsyncClock -from .bases import BaseClock -from .ephemera import ( +from .core import ( + BaseClock, CallbackEvent, ChangeEvent, ClockContext, diff --git a/supriya/clocks/asynchronous.py b/supriya/clocks/asynchronous.py index 76f9f3459..01ae3804a 100644 --- a/supriya/clocks/asynchronous.py +++ b/supriya/clocks/asynchronous.py @@ -4,9 +4,9 @@ import traceback from typing import Awaitable, Optional, Tuple -from .bases import BaseClock -from .ephemera import ( +from .core import ( Action, + BaseClock, CallbackEvent, ChangeEvent, ClockContext, diff --git a/supriya/clocks/bases.py b/supriya/clocks/core.py similarity index 86% rename from supriya/clocks/bases.py rename to supriya/clocks/core.py index 5ec1d79c5..ea4c11863 100644 --- a/supriya/clocks/bases.py +++ b/supriya/clocks/core.py @@ -1,34 +1,207 @@ import collections import dataclasses +import enum import fractions import itertools import logging import queue import time import traceback -from typing import Any, Callable, Deque, Dict, FrozenSet, Optional, Set, Tuple, Union +from functools import total_ordering +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + List, + Literal, + NamedTuple, + Optional, + Set, + Tuple, + Union, +) from .. import conversions -from .ephemera import ( - Action, - CallbackCommand, - CallbackEvent, - ChangeCommand, - ChangeEvent, - ClockContext, - ClockState, - Command, - Event, - EventType, - Moment, - Quantization, - TimeUnit, -) -from .eventqueue import EventQueue logger = logging.getLogger(__name__) +Quantization = Literal[ + "8M", + "4M", + "2M", + "1M", + "1/2", + "1/2T", + "1/4", + "1/4T", + "1/8", + "1/8T", + "1/16", + "1/16T", + "1/32", + "1/32T", + "1/64", + "1/64T", + "1/128", +] + + +class EventType(enum.IntEnum): + CHANGE = 0 + SCHEDULE = 1 + + +class TimeUnit(enum.IntEnum): + BEATS = 0 + SECONDS = 1 + MEASURES = 2 + + +class ClockState(NamedTuple): + beats_per_minute: float + initial_seconds: float + previous_measure: int + previous_offset: float + previous_seconds: float + previous_time_signature_change_offset: float + time_signature: Tuple[int, int] + + +@dataclasses.dataclass(frozen=True) +class Moment: + __slots__ = ( + "beats_per_minute", + "measure", + "measure_offset", + "offset", + "seconds", + "time_signature", + ) + beats_per_minute: float + measure: int + measure_offset: float + offset: float # the beat since zero + seconds: float # the seconds since zero + time_signature: Tuple[int, int] + + +@dataclasses.dataclass(frozen=True) +class Action: + event_id: int + event_type: int + + +@dataclasses.dataclass(frozen=True) +class Command(Action): + quantization: Optional[Quantization] + schedule_at: float + time_unit: Optional[TimeUnit] + + +@dataclasses.dataclass(frozen=True) +class CallbackCommand(Command): + args: Optional[Tuple] + kwargs: Optional[Dict] + procedure: Callable[["ClockContext"], Union[None, float, Tuple[float, TimeUnit]]] + + +@dataclasses.dataclass(frozen=True) +class ChangeCommand(Command): + beats_per_minute: Optional[float] + time_signature: Optional[Tuple[int, int]] + + +@total_ordering +@dataclasses.dataclass(frozen=True, eq=False) +class Event(Action): + seconds: float + measure: Optional[int] + offset: Optional[float] + + def __eq__(self, other: object) -> bool: + # Need to act like a tuple here + if not isinstance(other, Event): + return NotImplemented + return (self.seconds, self.event_type, self.event_id) == ( + other.seconds, + other.event_type, + other.event_id, + ) + + def __lt__(self, other: object) -> bool: + # Need to act like a tuple here + if not isinstance(other, Event): + return NotImplemented + return (self.seconds, self.event_type, self.event_id) < ( + other.seconds, + other.event_type, + other.event_id, + ) + + +@dataclasses.dataclass(frozen=True, eq=False) +class CallbackEvent(Event): + procedure: Callable[["ClockContext"], Union[None, float, Tuple[float, TimeUnit]]] + args: Optional[Tuple] + kwargs: Optional[Dict] + invocations: int + + def __hash__(self) -> int: + return hash((type(self), self.event_id)) + + +@dataclasses.dataclass(frozen=True, eq=False) +class ChangeEvent(Event): + beats_per_minute: Optional[float] + time_signature: Optional[Tuple[int, int]] + + def __hash__(self) -> int: + return hash((type(self), self.event_id)) + + +class ClockContext(NamedTuple): + current_moment: Moment + desired_moment: Moment + event: Union[CallbackEvent, ChangeEvent] + + +class _EventQueue(queue.PriorityQueue[Event]): + ### PRIVATE METHODS ### + + def _init(self, maxsize: Optional[int]) -> None: + self.queue: List[Event] = [] + self.flags: Dict[Event, bool] = {} + + def _put(self, event: Event) -> None: + self.flags[event] = True + super()._put(event) + + def _get(self) -> Event: + while self.queue: + if not self.flags.pop((event := super()._get()), None): + continue + return event + raise queue.Empty + + ### PUBLIC METHODS ### + + def clear(self) -> None: + with self.mutex: + self._init(None) + + def peek(self) -> Event: + with self.mutex: + self._put(event := self._get()) + return event + + def remove(self, event: Event) -> None: + with self.mutex: + self.flags.pop(event, None) + + class BaseClock: ### CLASS VARIABLES ### @@ -60,7 +233,7 @@ def __init__(self) -> None: self._name = None self._counter = itertools.count() self._command_deque: Deque[Command] = collections.deque() - self._event_queue = EventQueue() + self._event_queue = _EventQueue() self._is_running = False self._slop = 0.001 self._actions_by_id: Dict[int, Action] = {} diff --git a/supriya/clocks/ephemera.py b/supriya/clocks/ephemera.py deleted file mode 100644 index 7d3fe20ba..000000000 --- a/supriya/clocks/ephemera.py +++ /dev/null @@ -1,143 +0,0 @@ -import dataclasses -import enum -from functools import total_ordering -from typing import Callable, Dict, Literal, NamedTuple, Optional, Tuple, Union - -Quantization = Literal[ - "8M", - "4M", - "2M", - "1M", - "1/2", - "1/2T", - "1/4", - "1/4T", - "1/8", - "1/8T", - "1/16", - "1/16T", - "1/32", - "1/32T", - "1/64", - "1/64T", - "1/128", -] - - -class EventType(enum.IntEnum): - CHANGE = 0 - SCHEDULE = 1 - - -class TimeUnit(enum.IntEnum): - BEATS = 0 - SECONDS = 1 - MEASURES = 2 - - -class ClockState(NamedTuple): - beats_per_minute: float - initial_seconds: float - previous_measure: int - previous_offset: float - previous_seconds: float - previous_time_signature_change_offset: float - time_signature: Tuple[int, int] - - -@dataclasses.dataclass(frozen=True) -class Moment: - __slots__ = ( - "beats_per_minute", - "measure", - "measure_offset", - "offset", - "seconds", - "time_signature", - ) - beats_per_minute: float - measure: int - measure_offset: float - offset: float # the beat since zero - seconds: float # the seconds since zero - time_signature: Tuple[int, int] - - -@dataclasses.dataclass(frozen=True) -class Action: - event_id: int - event_type: int - - -@dataclasses.dataclass(frozen=True) -class Command(Action): - quantization: Optional[Quantization] - schedule_at: float - time_unit: Optional[TimeUnit] - - -@dataclasses.dataclass(frozen=True) -class CallbackCommand(Command): - args: Optional[Tuple] - kwargs: Optional[Dict] - procedure: Callable[["ClockContext"], Union[None, float, Tuple[float, TimeUnit]]] - - -@dataclasses.dataclass(frozen=True) -class ChangeCommand(Command): - beats_per_minute: Optional[float] - time_signature: Optional[Tuple[int, int]] - - -@total_ordering -@dataclasses.dataclass(frozen=True, eq=False) -class Event(Action): - seconds: float - measure: Optional[int] - offset: Optional[float] - - def __eq__(self, other: object) -> bool: - # Need to act like a tuple here - if not isinstance(other, Event): - return NotImplemented - return (self.seconds, self.event_type, self.event_id) == ( - other.seconds, - other.event_type, - other.event_id, - ) - - def __lt__(self, other: object) -> bool: - # Need to act like a tuple here - if not isinstance(other, Event): - return NotImplemented - return (self.seconds, self.event_type, self.event_id) < ( - other.seconds, - other.event_type, - other.event_id, - ) - - -@dataclasses.dataclass(frozen=True, eq=False) -class CallbackEvent(Event): - procedure: Callable[["ClockContext"], Union[None, float, Tuple[float, TimeUnit]]] - args: Optional[Tuple] - kwargs: Optional[Dict] - invocations: int - - def __hash__(self) -> int: - return hash((type(self), self.event_id)) - - -@dataclasses.dataclass(frozen=True, eq=False) -class ChangeEvent(Event): - beats_per_minute: Optional[float] - time_signature: Optional[Tuple[int, int]] - - def __hash__(self) -> int: - return hash((type(self), self.event_id)) - - -class ClockContext(NamedTuple): - current_moment: Moment - desired_moment: Moment - event: Union[CallbackEvent, ChangeEvent] diff --git a/supriya/clocks/eventqueue.py b/supriya/clocks/eventqueue.py deleted file mode 100644 index 30bd2ca1e..000000000 --- a/supriya/clocks/eventqueue.py +++ /dev/null @@ -1,44 +0,0 @@ -import queue -import sys -from typing import Dict, List, Optional - -from .ephemera import Event - -if sys.version_info >= (3, 9): - _EventQueueBase = queue.PriorityQueue[Event] -else: - _EventQueueBase = queue.PriorityQueue - - -class EventQueue(_EventQueueBase): - ### PRIVATE METHODS ### - - def _init(self, maxsize: Optional[int]) -> None: - self.queue: List[Event] = [] - self.flags: Dict[Event, bool] = {} - - def _put(self, event: Event) -> None: - self.flags[event] = True - super()._put(event) - - def _get(self) -> Event: - while self.queue: - if not self.flags.pop((event := super()._get()), None): - continue - return event - raise queue.Empty - - ### PUBLIC METHODS ### - - def clear(self) -> None: - with self.mutex: - self._init(None) - - def peek(self) -> Event: - with self.mutex: - self._put(event := self._get()) - return event - - def remove(self, event: Event) -> None: - with self.mutex: - self.flags.pop(event, None) diff --git a/supriya/clocks/offline.py b/supriya/clocks/offline.py index 39a9a1bbd..df49df006 100644 --- a/supriya/clocks/offline.py +++ b/supriya/clocks/offline.py @@ -3,8 +3,7 @@ from typing import Generator, Optional, Tuple from .asynchronous import AsyncClock -from .bases import BaseClock -from .ephemera import CallbackEvent, ClockContext, Moment, TimeUnit +from .core import BaseClock, CallbackEvent, ClockContext, Moment, TimeUnit logger = logging.getLogger(__name__) diff --git a/supriya/clocks/threaded.py b/supriya/clocks/threaded.py index 50c18f5a3..9ed26492c 100644 --- a/supriya/clocks/threaded.py +++ b/supriya/clocks/threaded.py @@ -4,8 +4,7 @@ import threading from typing import Optional, Tuple -from .bases import BaseClock -from .ephemera import Action, Command, Moment +from .core import Action, BaseClock, Command, Moment logger = logging.getLogger(__name__) From 017ef4e38d53882b5b93d3618aa468b7637ed90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9phine=20Wolf=20Oberholtzer?= Date: Thu, 27 Feb 2025 13:31:12 -0500 Subject: [PATCH 2/5] Add clock docstrings --- supriya/clocks/asynchronous.py | 6 ++++++ supriya/clocks/offline.py | 6 ++++++ supriya/clocks/threaded.py | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/supriya/clocks/asynchronous.py b/supriya/clocks/asynchronous.py index 01ae3804a..c895b0f63 100644 --- a/supriya/clocks/asynchronous.py +++ b/supriya/clocks/asynchronous.py @@ -19,6 +19,12 @@ class AsyncClock(BaseClock): + """ + An async clock. + """ + + ### INITIALIZER ### + def __init__(self) -> None: BaseClock.__init__(self) self._task: Optional[Awaitable[None]] = None diff --git a/supriya/clocks/offline.py b/supriya/clocks/offline.py index df49df006..78f55e841 100644 --- a/supriya/clocks/offline.py +++ b/supriya/clocks/offline.py @@ -9,6 +9,12 @@ class OfflineClock(BaseClock): + """ + An offline clock. + """ + + ### INITIALIZER ### + def __init__(self) -> None: super().__init__() self._generator: Optional[Generator[bool, None, None]] = None diff --git a/supriya/clocks/threaded.py b/supriya/clocks/threaded.py index 9ed26492c..dc27e8745 100644 --- a/supriya/clocks/threaded.py +++ b/supriya/clocks/threaded.py @@ -10,6 +10,10 @@ class Clock(BaseClock): + """ + A threaded clock. + """ + ### CLASS VARIABLES ### _default_clock: Optional["Clock"] = None From 3286d9c950c2a085fba7ad62e28d0b1991eb4f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9phine=20Wolf=20Oberholtzer?= Date: Thu, 27 Feb 2025 13:31:23 -0500 Subject: [PATCH 3/5] RM Clock.default() --- supriya/clocks/threaded.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/supriya/clocks/threaded.py b/supriya/clocks/threaded.py index dc27e8745..1eae72b06 100644 --- a/supriya/clocks/threaded.py +++ b/supriya/clocks/threaded.py @@ -14,10 +14,6 @@ class Clock(BaseClock): A threaded clock. """ - ### CLASS VARIABLES ### - - _default_clock: Optional["Clock"] = None - ### INITIALIZER ### def __init__(self) -> None: @@ -91,12 +87,6 @@ def cancel(self, event_id: int) -> Optional[Action]: self._event.set() return event - @classmethod - def default(cls) -> "Clock": - if cls._default_clock is None: - cls._default_clock = cls() - return cls._default_clock - def start( self, initial_time: Optional[float] = None, From a87a738b60f2f4a58a9ddd0d0a9bb96030608e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9phine=20Wolf=20Oberholtzer?= Date: Thu, 27 Feb 2025 13:37:27 -0500 Subject: [PATCH 4/5] Update Pattern.play() --- supriya/patterns/patterns.py | 9 ++++----- supriya/patterns/players.py | 6 +----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/supriya/patterns/patterns.py b/supriya/patterns/patterns.py index 00c33c01c..45e179971 100644 --- a/supriya/patterns/patterns.py +++ b/supriya/patterns/patterns.py @@ -27,7 +27,7 @@ from uqbar.objects import get_vars import supriya.patterns -from supriya.clocks import BaseClock, Clock, ClockContext, OfflineClock, Quantization +from supriya.clocks import BaseClock, ClockContext, OfflineClock, Quantization from supriya.contexts import Bus, Context, Node, Score from .events import CompositeEvent, Event, Priority @@ -285,7 +285,7 @@ def play( Optional[Coroutine], ] ] = None, - clock: Optional[BaseClock] = None, + clock: Optional[BaseClock], quantization: Optional[Quantization] = None, target_bus: Optional[Bus] = None, target_node: Optional[Node] = None, @@ -296,10 +296,9 @@ def play( from .players import PatternPlayer # Avoid circular import if isinstance(context, Score): - clock = OfflineClock() + if not isinstance(clock, OfflineClock): + raise ValueError(clock) at = at or 0.0 - elif clock is None: - clock = Clock.default() player = PatternPlayer( pattern=self, context=context, diff --git a/supriya/patterns/players.py b/supriya/patterns/players.py index 35853ef61..fe254ae1b 100644 --- a/supriya/patterns/players.py +++ b/supriya/patterns/players.py @@ -256,11 +256,7 @@ def play( ) if until: self._clock.schedule(self._stop_callback, event_type=2, schedule_at=until) - if ( - isinstance(self._clock, (Clock, OfflineClock)) - and not self._clock.is_running - ): - self._clock.start(initial_time=at) + self._clock.start(initial_time=at) def stop(self, quantization: Optional[Quantization] = None) -> None: with self._lock: From 003dd6b2f90e33d799a911079c2e3aabb9cf19cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9phine=20Wolf=20Oberholtzer?= Date: Thu, 27 Feb 2025 14:36:04 -0500 Subject: [PATCH 5/5] Streamline PatternPlayer.play() --- supriya/patterns/patterns.py | 2 +- supriya/patterns/players.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/supriya/patterns/patterns.py b/supriya/patterns/patterns.py index 45e179971..e292245c7 100644 --- a/supriya/patterns/patterns.py +++ b/supriya/patterns/patterns.py @@ -285,7 +285,7 @@ def play( Optional[Coroutine], ] ] = None, - clock: Optional[BaseClock], + clock: BaseClock, quantization: Optional[Quantization] = None, target_bus: Optional[Bus] = None, target_node: Optional[Node] = None, diff --git a/supriya/patterns/players.py b/supriya/patterns/players.py index fe254ae1b..8165c0acb 100644 --- a/supriya/patterns/players.py +++ b/supriya/patterns/players.py @@ -20,9 +20,7 @@ from ..clocks import ( BaseClock, CallbackEvent, - Clock, ClockContext, - OfflineClock, Quantization, ) from ..contexts import Bus, Context, ContextObject, Node @@ -256,7 +254,8 @@ def play( ) if until: self._clock.schedule(self._stop_callback, event_type=2, schedule_at=until) - self._clock.start(initial_time=at) + if not self._clock.is_running and hasattr(self._clock, "start"): + self._clock.start(initial_time=at) def stop(self, quantization: Optional[Quantization] = None) -> None: with self._lock: