From ca3029aab6c29e245749e1be4a79424a40bb8ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 14 Jun 2024 21:32:15 +0200 Subject: [PATCH 01/15] implement blob --- sardine_core/run.py | 21 ++++++++++++++++++--- sardine_core/scheduler/async_runner.py | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/sardine_core/run.py b/sardine_core/run.py index e484bf25..bd3bb0d6 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -236,9 +236,24 @@ def decorator(func: Union[Callable, AsyncRunner], /) -> AsyncRunner: again(runner) bowl.scheduler.start_runner(runner) return runner - elif quant is not None: - deadline = get_quant_deadline(bowl.clock, quant) - runner.push_deferred(deadline, func, *args, **kwargs) + elif quant is not None or quant is not 'now': + if isinstance(quant, (float, int)): + deadline = get_quant_deadline(bowl.clock, quant) + runner.push_deferred(deadline, func, *args, **kwargs) + elif isinstance(quant, str): + match quant: + case 'beat': + time = bowl.clock.shifted_time + deadline = time + bowl.clock.get_beat_time(1, time=time) + runner.push_deferred(deadline, func, *args, **kwargs) + case 'bar': + deadline = get_quant_deadline(bowl.clock, 0) + runner.push_deferred(deadline, func, *args, **kwargs) + else: + raise ValueError( + f"Invalid snap argument {snap!r}; must be 'now', 'beat', " + f"'bar', None, or a numeric offset measured in beats" + ) else: runner.push(func, *args, **kwargs) diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index 0add1fde..22b805e8 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -217,7 +217,7 @@ def __init__(self, name: str): self.interval_shift = 0.0 self.quant= None self._iter = 0 - self._default_period = 0.25 + self._default_period = 1 self.background_job = False self._swimming = False From e0d3c513990133e4a7e7c2e84b174585641eb868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 14 Jun 2024 22:07:16 +0200 Subject: [PATCH 02/15] Fix condition in swim decorator --- sardine_core/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sardine_core/run.py b/sardine_core/run.py index bd3bb0d6..6fd8a8b8 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -236,7 +236,7 @@ def decorator(func: Union[Callable, AsyncRunner], /) -> AsyncRunner: again(runner) bowl.scheduler.start_runner(runner) return runner - elif quant is not None or quant is not 'now': + elif quant is not None quant != 'now': if isinstance(quant, (float, int)): deadline = get_quant_deadline(bowl.clock, quant) runner.push_deferred(deadline, func, *args, **kwargs) From b8840ab979f6eb02d6af044372d4284aa12c129b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 14 Jun 2024 22:09:44 +0200 Subject: [PATCH 03/15] fix again --- sardine_core/run.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sardine_core/run.py b/sardine_core/run.py index 6fd8a8b8..47b2426f 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -236,19 +236,17 @@ def decorator(func: Union[Callable, AsyncRunner], /) -> AsyncRunner: again(runner) bowl.scheduler.start_runner(runner) return runner - elif quant is not None quant != 'now': + elif quant is not None and quant != 'now': if isinstance(quant, (float, int)): deadline = get_quant_deadline(bowl.clock, quant) runner.push_deferred(deadline, func, *args, **kwargs) elif isinstance(quant, str): - match quant: - case 'beat': - time = bowl.clock.shifted_time - deadline = time + bowl.clock.get_beat_time(1, time=time) - runner.push_deferred(deadline, func, *args, **kwargs) - case 'bar': - deadline = get_quant_deadline(bowl.clock, 0) - runner.push_deferred(deadline, func, *args, **kwargs) + if quant == 'beat': + time = bowl.clock.shifted_time + deadline = time + bowl.clock.get_beat_time(1, time=time) + elif quant == 'bar': + deadline = get_quant_deadline(bowl.clock, 0) + runner.push_deferred(deadline, func, *args, **kwargs) else: raise ValueError( f"Invalid snap argument {snap!r}; must be 'now', 'beat', " From 53ec10825766d7f91dbf064af8fb7fac494ed8d1 Mon Sep 17 00:00:00 2001 From: thegamecracks <61257169+thegamecracks@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:48:23 -0400 Subject: [PATCH 04/15] feat: share new quant implementation with players --- sardine_core/handlers/player.py | 19 +++++++++---------- sardine_core/run.py | 31 ++++++++++--------------------- sardine_core/utils/__init__.py | 20 +++++++++++++++++--- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/sardine_core/handlers/player.py b/sardine_core/handlers/player.py index 39670b02..73f2ec14 100644 --- a/sardine_core/handlers/player.py +++ b/sardine_core/handlers/player.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Optional, ParamSpec, TypeVar, Self from ..handlers.sender import Number, NumericElement, Sender -from ..utils import alias_param, get_quant_deadline, lerp +from ..utils import Quant, alias_param, get_deadline_from_quant, lerp from ..scheduler import AsyncRunner from dataclasses import dataclass from ..base import BaseHandler @@ -43,7 +43,7 @@ class PatternInformation: iterator_limit: NumericElement divisor: NumericElement rate: NumericElement - quant: Number + quant: Quant timespan: Optional[float] until: Optional[int] @@ -104,7 +104,7 @@ def _play_factory( iterator_limit: Optional[Number] = None, divisor: NumericElement = 1, rate: NumericElement = 1, - quant: Number = 0, + quant: Quant = 0, **kwargs: P.kwargs, ) -> PatternInformation: """Entry point of a pattern into the Player""" @@ -224,15 +224,14 @@ def push(self, pattern: Optional[PatternInformation]): # the new pattern can be synchronized self.runner.interval_shift = 0.0 + func = for_(pattern.until)(self.func) if pattern.until else self.func + deadline = get_deadline_from_quant(self.env.clock, pattern.quant) period = self.get_new_period(pattern) - deadline = get_quant_deadline(self.env.clock, pattern.quant) - self.runner.push_deferred( - deadline, - for_(pattern.until)(self.func) if pattern.until else self.func, - pattern=pattern, - p=period, - ) + if deadline is None: + self.runner.push(func, pattern=pattern, p=period) + else: + self.runner.push_deferred(deadline, func, pattern=pattern, p=period) self.env.scheduler.start_runner(self.runner) self.runner.reload() diff --git a/sardine_core/run.py b/sardine_core/run.py index 47b2426f..925d9c89 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -4,7 +4,7 @@ from itertools import product from pathlib import Path from string import ascii_lowercase, ascii_uppercase -from typing import Any, Callable, Optional, ParamSpec, TypeVar, Union, Literal, overload +from typing import Any, Callable, Optional, ParamSpec, TypeVar, Union, overload from . import * from .io.UserConfig import read_user_configuration, read_extension_configuration @@ -12,7 +12,7 @@ from .sequences import ListParser, ziffers_factory from .sequences.tidal_parser import * from .superdirt import SuperDirtProcess -from .utils import greeter_printer, get_quant_deadline, join, sardine_intro +from .utils import Quant, greeter_printer, get_deadline_from_quant, join, sardine_intro from ziffers import z ParamSpec = ParamSpec("PS") @@ -175,12 +175,13 @@ def swim( ) -> Callable[[Union[Callable, AsyncRunner]], AsyncRunner]: ... +# FIXME: quant docstring is outdated # pylint: disable=keyword-arg-before-vararg # signature is valid def swim( func: Optional[Union[Callable, AsyncRunner]] = None, /, *args, - quant: Optional[Union[float, int]] = 0, + quant: Quant = 0, until: Optional[int] = None, background_job: bool = False, **kwargs, @@ -194,7 +195,7 @@ def swim( The function to be scheduled. If this is an AsyncRunner, the current state is simply updated with new arguments. *args: Positional arguments to be passed to `func.` - quant (Optional[Union[float, int]]): + quant (Quant): If set to a numeric value, the new function will be deferred until the next bar + `quant` beats arrives. If None, the function is immediately pushed and will @@ -236,24 +237,12 @@ def decorator(func: Union[Callable, AsyncRunner], /) -> AsyncRunner: again(runner) bowl.scheduler.start_runner(runner) return runner - elif quant is not None and quant != 'now': - if isinstance(quant, (float, int)): - deadline = get_quant_deadline(bowl.clock, quant) - runner.push_deferred(deadline, func, *args, **kwargs) - elif isinstance(quant, str): - if quant == 'beat': - time = bowl.clock.shifted_time - deadline = time + bowl.clock.get_beat_time(1, time=time) - elif quant == 'bar': - deadline = get_quant_deadline(bowl.clock, 0) - runner.push_deferred(deadline, func, *args, **kwargs) - else: - raise ValueError( - f"Invalid snap argument {snap!r}; must be 'now', 'beat', " - f"'bar', None, or a numeric offset measured in beats" - ) - else: + + deadline = get_deadline_from_quant(bowl.clock, quant) + if deadline is None: runner.push(func, *args, **kwargs) + else: + runner.push_deferred(deadline, func, *args, **kwargs) # Intentionally avoid interval correction so # the user doesn't accidentally nudge the runner diff --git a/sardine_core/utils/__init__.py b/sardine_core/utils/__init__.py index 82167e1f..7d77e665 100644 --- a/sardine_core/utils/__init__.py +++ b/sardine_core/utils/__init__.py @@ -1,6 +1,6 @@ import functools import inspect -from typing import TYPE_CHECKING, Callable, ParamSpec, TypeVar, Union +from typing import TYPE_CHECKING, Callable, Literal, Optional, ParamSpec, TypeVar, Union from .Messages import * @@ -11,6 +11,7 @@ T = TypeVar("T") Number = Union[float, int] +Quant = Optional[Union[Number, Literal["now", "beat", "bar"]]] MISSING = object() @@ -38,10 +39,23 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return deco -def get_quant_deadline(clock: "BaseClock", offset_beats: Union[float, int]): +def get_deadline_from_quant(clock: "BaseClock", quant: Quant) -> Optional[float]: + if quant == "now" or quant is None: + return None + elif quant == "beat": + time = clock.shifted_time + return time + clock.get_beat_time(1, time=time) + elif quant == "bar": + return get_deadline_from_quant(clock, 0) + elif not isinstance(quant, (float, int)): + raise ValueError( + f"Invalid quant argument {quant!r}; must be 'now', 'beat', " + f"'bar', None, or a numeric offset measured in beats" + ) + time = clock.shifted_time next_bar = clock.get_bar_time(1, time=time) - offset = clock.get_beat_time(offset_beats, sync=False) + offset = clock.get_beat_time(quant, sync=False) return time + next_bar + offset From d73a3069b99e44b2f7afc1375447b91bde325464 Mon Sep 17 00:00:00 2001 From: thegamecracks <61257169+thegamecracks@users.noreply.github.com> Date: Fri, 14 Jun 2024 19:03:50 -0400 Subject: [PATCH 05/15] fix: jump-start runner's first iteration when deferred Before, runners would sleep an entire period before running their first iteration, which always caused desync between two runners with different periods even when their snap deadlines are synchronized. BTW, I think jump_start is a poor, obscure name for this fix, but I can't think of anything else. Please fix me! --- sardine_core/scheduler/async_runner.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index 712c6510..dda9d4ed 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -200,6 +200,7 @@ class AsyncRunner: _task: Optional[asyncio.Task] _reload_event: asyncio.Event _has_reverted: bool + _jump_start: bool _deferred_state_index: int @@ -225,6 +226,7 @@ def __init__(self, name: str): self._task = None self._reload_event = asyncio.Event() self._has_reverted = False + self._jump_start = False self._deferred_state_index = 0 @@ -659,14 +661,18 @@ async def _run_once(self): ) return self._skip_iteration() elif state is None: - # Nothing to do until the next deferred state arrives + # Nothing to do until the next deferred state arrives. + # + # However, we will need to jump-start the next iteration + # so it runs exactly on the deadline instead of unnecessarily + # sleeping a full period. deadline = self.deferred_states[0].deadline interrupted = await self._sleep_until(deadline) - return self._skip_iteration() + return self._jump_start_iteration() # NOTE: deadline will always be defined at this point if not self.background_job: - interrupted = await self._sleep_until(deadline) + interrupted = await self._sleep_unless_jump_started(deadline) if interrupted: return self._skip_iteration() @@ -762,6 +768,13 @@ async def _sleep_until(self, deadline: Union[float, int]) -> bool: ) ) + async def _sleep_unless_jump_started(self, deadline: Union[float, int]) -> bool: + if self._jump_start: + self._jump_start = False + return False + + return await self._sleep_until(deadline) + def _revert_state(self): if self.states: self.states.pop() @@ -769,3 +782,7 @@ def _revert_state(self): def _skip_iteration(self) -> None: self.swim() + + def _jump_start_iteration(self) -> None: + self._jump_start = True + self._skip_iteration() From 1f0f23291600cc07b3024fc393d3a524b1ce7505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 10:19:48 +0200 Subject: [PATCH 06/15] Change default evaluation/execution mode to 'bar' --- sardine_core/handlers/player.py | 2 +- sardine_core/run.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sardine_core/handlers/player.py b/sardine_core/handlers/player.py index 73f2ec14..c3852f30 100644 --- a/sardine_core/handlers/player.py +++ b/sardine_core/handlers/player.py @@ -104,7 +104,7 @@ def _play_factory( iterator_limit: Optional[Number] = None, divisor: NumericElement = 1, rate: NumericElement = 1, - quant: Quant = 0, + quant: Quant = 'bar', **kwargs: P.kwargs, ) -> PatternInformation: """Entry point of a pattern into the Player""" diff --git a/sardine_core/run.py b/sardine_core/run.py index 925d9c89..5e2b9b13 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -160,7 +160,7 @@ def swim( /, # NOTE: AsyncRunner doesn't support generic args/kwargs *args: ParamSpec.args, - quant: Optional[Union[float, int]] = 0, + quant: Quant = 'bar', until: Optional[int] = None, **kwargs: ParamSpec.kwargs, ) -> AsyncRunner: ... @@ -169,7 +169,7 @@ def swim( @overload def swim( *args, - quant: Optional[Union[float, int]] = 0, + quant: Quant = 'bar', until: Optional[int] = None, **kwargs, ) -> Callable[[Union[Callable, AsyncRunner]], AsyncRunner]: ... @@ -181,7 +181,7 @@ def swim( func: Optional[Union[Callable, AsyncRunner]] = None, /, *args, - quant: Quant = 0, + quant: Quant = 'bar', until: Optional[int] = None, background_job: bool = False, **kwargs, From 0fc8c8fde6d75fd52fea4f67474ad625a1b8fedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 17:06:14 +0200 Subject: [PATCH 07/15] Replacer Player iterator by internalized iterator (AsyncRunner) This commit replaces the 'iterator' system from the Player class by the internal iterator already implemented in the AsyncRunner class. These iterators possess a 'step' and 'limit' argument to control iteration. It also fixes the 'sync' keywords accessible used by Players. The 'sync' parameter expects an AsyncRunner, and will lock player A iterator to player B iterator. This can be quite useful in some scenarios. --- sardine_core/handlers/player.py | 48 ++++++++++---------------- sardine_core/scheduler/async_runner.py | 19 ++++++++-- sardine_core/utils/__init__.py | 1 + 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/sardine_core/handlers/player.py b/sardine_core/handlers/player.py index c3852f30..e8484f21 100644 --- a/sardine_core/handlers/player.py +++ b/sardine_core/handlers/player.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Optional, ParamSpec, TypeVar, Self from ..handlers.sender import Number, NumericElement, Sender -from ..utils import Quant, alias_param, get_deadline_from_quant, lerp +from ..utils import Quant, alias_param, get_deadline_from_quant, lerp, Span from ..scheduler import AsyncRunner from dataclasses import dataclass from ..base import BaseHandler @@ -35,12 +35,10 @@ class PatternInformation: args: tuple[Any] kwargs: dict[str, Any] period: NumericElement - sync: Optional[ - Any - ] # NOTE: Actually Optional[Player] but I don't know how to type it + sync: Optional[AsyncRunner] iterator: Optional[Number] - iterator_span: NumericElement - iterator_limit: NumericElement + iterator_step: NumericElement + iterator_limit: Span divisor: NumericElement rate: NumericElement quant: Quant @@ -61,9 +59,6 @@ def __init__(self, name: str): super().__init__() self._name = name self.runner = AsyncRunner(name=name) - self.iterator: Number = 0 - self._iteration_span: Number = 1 - self._iteration_limit: Optional[Number] = None self._period: int | float = 1.0 @property @@ -86,7 +81,7 @@ def fit_period_to_timespan(self, period: NumericElement, timespan: float): @staticmethod @alias_param(name="period", alias="p") - @alias_param(name="iterator_span", alias="i") + @alias_param(name="iterator_step", alias="i") @alias_param(name="iterator_limit", alias="l") @alias_param(name="divisor", alias="d") @alias_param(name="rate", alias="r") @@ -98,10 +93,10 @@ def _play_factory( timespan: Optional[float] = None, until: Optional[int] = None, period: NumericElement = 1, - sync: Optional[Self] = None, + sync: Optional[AsyncRunner] = None, iterator: Optional[Number] = None, - iterator_span: Optional[Number] = 1, - iterator_limit: Optional[Number] = None, + iterator_step: Optional[Number] = 1, + iterator_limit: Span = 'inf', divisor: NumericElement = 1, rate: NumericElement = 1, quant: Quant = 'bar', @@ -117,7 +112,7 @@ def _play_factory( period, sync, iterator, - iterator_span, + iterator_step, iterator_limit, divisor, rate, @@ -170,32 +165,25 @@ def func( p: NumericElement = 1, # pylint: disable=invalid-name,unused-argument ) -> None: """Central swimming function defined by the player""" - self._iterator_span = pattern.iterator_span + self._iterator_step = pattern.iterator_step self._iterator_limit = pattern.iterator_limit - if pattern.iterator is not None: - self.iterator = pattern.iterator - pattern.iterator = None + self.runner._iter_limit = pattern.iterator_limit + self.runner._iter_step = pattern.iterator_step + + if pattern.sync is None: + iterator = self.runner.iter + else: + iterator = pattern.sync.runner.iter dur = pattern.send_method( *pattern.args, **pattern.kwargs, - iterator=self.iterator, + iterator=iterator, divisor=pattern.divisor, rate=pattern.rate, ) - # Reset the iterator when it reaches a certain ceiling - if self._iterator_limit: - if self.iterator >= self._iterator_limit: - self.iterator = 0 - - # Moving the iterator up - self.iterator += self._iterator_span - - # If synced, we use the iterator from the other player - self.iterator = pattern.sync.iterator if pattern.sync else self.iterator - period = self.get_new_period(pattern) if not dur: self.again(pattern=pattern, p=period) diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index e163bb6d..35f584e4 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -12,7 +12,7 @@ from ..base import BaseClock from ..clock import Time -from ..utils import MISSING, maybe_coro +from ..utils import MISSING, maybe_coro, Span from .constants import MaybeCoroFunc from .errors import * @@ -131,6 +131,12 @@ class AsyncRunner: _iter: int """Number of times this asyncrunner has been executed""" + _iter_step: int + """The iteration step to use for the next iteration""" + + _iter_limit: Span + """The maximum number of iterations allowed before going back to 0""" + _default_period: int | float """Default recursion period""" @@ -218,6 +224,8 @@ def __init__(self, name: str): self.interval_shift = 0.0 self.snap = None self._iter = 0 + self._iter_step = 1 + self ._iter_limit = 'inf' self._default_period = 1 self.background_job = False @@ -684,7 +692,14 @@ async def _run_once(self): ) finally: self._last_expected_time = self._expected_time - self._iter += 1 + self._update_iter() + + def _update_iter(self): + """Updates the iteration number""" + self._iter += self._iter_step + if self._iter_limit != 'inf': + if self._iter >= self._iter_limit: + self._iter = 0 async def _call_func(self, func, args, kwargs): """Calls the given function and optionally applies time shift diff --git a/sardine_core/utils/__init__.py b/sardine_core/utils/__init__.py index 7d77e665..61779a6c 100644 --- a/sardine_core/utils/__init__.py +++ b/sardine_core/utils/__init__.py @@ -12,6 +12,7 @@ Number = Union[float, int] Quant = Optional[Union[Number, Literal["now", "beat", "bar"]]] +Span = Optional[Union[Number, Literal['inf']]] MISSING = object() From 7752bf7425d0fe9211d63dc5f61359bd441f6432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 17:18:39 +0200 Subject: [PATCH 08/15] Lint code --- sardine_core/handlers/player.py | 4 ++-- sardine_core/run.py | 6 +++--- sardine_core/scheduler/async_runner.py | 4 ++-- sardine_core/utils/__init__.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sardine_core/handlers/player.py b/sardine_core/handlers/player.py index e8484f21..5a46079e 100644 --- a/sardine_core/handlers/player.py +++ b/sardine_core/handlers/player.py @@ -96,10 +96,10 @@ def _play_factory( sync: Optional[AsyncRunner] = None, iterator: Optional[Number] = None, iterator_step: Optional[Number] = 1, - iterator_limit: Span = 'inf', + iterator_limit: Span = "inf", divisor: NumericElement = 1, rate: NumericElement = 1, - quant: Quant = 'bar', + quant: Quant = "bar", **kwargs: P.kwargs, ) -> PatternInformation: """Entry point of a pattern into the Player""" diff --git a/sardine_core/run.py b/sardine_core/run.py index 5e2b9b13..a763d021 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -160,7 +160,7 @@ def swim( /, # NOTE: AsyncRunner doesn't support generic args/kwargs *args: ParamSpec.args, - quant: Quant = 'bar', + quant: Quant = "bar", until: Optional[int] = None, **kwargs: ParamSpec.kwargs, ) -> AsyncRunner: ... @@ -169,7 +169,7 @@ def swim( @overload def swim( *args, - quant: Quant = 'bar', + quant: Quant = "bar", until: Optional[int] = None, **kwargs, ) -> Callable[[Union[Callable, AsyncRunner]], AsyncRunner]: ... @@ -181,7 +181,7 @@ def swim( func: Optional[Union[Callable, AsyncRunner]] = None, /, *args, - quant: Quant = 'bar', + quant: Quant = "bar", until: Optional[int] = None, background_job: bool = False, **kwargs, diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index 35f584e4..6d6d6ed7 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -225,7 +225,7 @@ def __init__(self, name: str): self.snap = None self._iter = 0 self._iter_step = 1 - self ._iter_limit = 'inf' + self._iter_limit = "inf" self._default_period = 1 self.background_job = False @@ -697,7 +697,7 @@ async def _run_once(self): def _update_iter(self): """Updates the iteration number""" self._iter += self._iter_step - if self._iter_limit != 'inf': + if self._iter_limit != "inf": if self._iter >= self._iter_limit: self._iter = 0 diff --git a/sardine_core/utils/__init__.py b/sardine_core/utils/__init__.py index 61779a6c..155d88d9 100644 --- a/sardine_core/utils/__init__.py +++ b/sardine_core/utils/__init__.py @@ -12,7 +12,7 @@ Number = Union[float, int] Quant = Optional[Union[Number, Literal["now", "beat", "bar"]]] -Span = Optional[Union[Number, Literal['inf']]] +Span = Optional[Union[Number, Literal["inf"]]] MISSING = object() From b56e5e8ded9ec6a2ff7bb924419ec9fb8afc267d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 17:30:38 +0200 Subject: [PATCH 09/15] Update feedback message colors + stop lying about exact execution time This commit updates the coloring of some feedback messages (they need to be yellow). It also removes the timing indication printed when an AsyncRunner is getting evaluated. It was obviously not the right time if I am to believe the model. --- sardine_core/scheduler/async_runner.py | 4 ++-- sardine_core/superdirt/process.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index 6d6d6ed7..ea45fe17 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -587,7 +587,7 @@ async def _runner(self): if not self.background_job: print( - f"[yellow][[red]{self.name}[/red] is swimming at {current_bar}/{current_beat}/{current_phase:.2f}][/yellow]" + f"[yellow][[red]{self.name}[/red] is swimming][/yellow]" ) try: @@ -603,7 +603,7 @@ async def _runner(self): self.swim() finally: print( - f"[yellow][Stopped [red]{self.name}[/red] at {current_bar}/{current_beat}/{current_phase:.2f}][/yellow]" + f"[yellow][Stopped [red]{self.name}[/red]][/yellow]" ) def _prepare(self): diff --git a/sardine_core/superdirt/process.py b/sardine_core/superdirt/process.py index e304009d..5b19f13c 100644 --- a/sardine_core/superdirt/process.py +++ b/sardine_core/superdirt/process.py @@ -95,9 +95,9 @@ def _analyze_and_warn(self, decoded_line: str): sample_name = decoded_line.split("'") print(f"[[red]/!\\\\[/red] - Sample {sample_name[1]} not found]") if "late 0." in decoded_line: - print(f"[[red]/!\\\\[/red] - Late messages. Increase SC latency]") + print(f"[yellow][[red]/!\\\\[/red] - Late messages. Increase SC latency][/yellow]") if "listening on port 57120" in decoded_line: - print(f"[[green]/!\\\\[/green] - Audio server ready!]") + print(f"[yellow][[green]/!\\\\[/green] - Audio server ready!][/yellow]") if self._synth_directory is not None: self.load_custom_synthdefs() if "ERROR: failed to open UDP socket: address in use" in decoded_line: From 551ba592600ba1d44e9ae27c035de55d451b4a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 19:13:38 +0200 Subject: [PATCH 10/15] Mock: make everything support Callable parameters in superdirt.py --- sardine_core/handlers/sender.py | 5 +- sardine_core/handlers/superdirt.py | 79 ++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/sardine_core/handlers/sender.py b/sardine_core/handlers/sender.py index be797331..cab831e9 100644 --- a/sardine_core/handlers/sender.py +++ b/sardine_core/handlers/sender.py @@ -1,7 +1,7 @@ import asyncio from math import floor from random import random -from typing import Callable, Generator, ParamSpec, TypeVar, Union, Optional +from typing import Callable, Generator, ParamSpec, TypeVar, Union, Optional, Any from ..base import BaseHandler from ..utils import maybe_coro from ..sequences import euclid @@ -32,6 +32,9 @@ def _maybe_index(val: RecursiveElement, i: int) -> RecursiveElement: length = len(val) return val[i % length] +def _resolve_if_callable(val: Callable[[], Any] | Any) -> Any: + """Evaluate a callable if it is one, otherwise return the value.""" + return val() if callable(val) else val def _maybe_length(val: RecursiveElement) -> int: if isinstance(val, list): diff --git a/sardine_core/handlers/superdirt.py b/sardine_core/handlers/superdirt.py index 361835a1..3ef78e87 100644 --- a/sardine_core/handlers/superdirt.py +++ b/sardine_core/handlers/superdirt.py @@ -1,13 +1,20 @@ import time from itertools import chain -from typing import Optional, List +from typing import Optional, List, Union, Callable, Any from osc4py3 import oscbuildparse from osc4py3.as_eventloop import osc_send, osc_udp_client from ..utils import alias_param from .osc_loop import OSCLoop -from .sender import Number, NumericElement, ParsableElement, Sender, StringElement +from .sender import ( + Number, + NumericElement, + ParsableElement, + Sender, + StringElement, + _resolve_if_callable +) __all__ = ("SuperDirtHandler",) @@ -140,11 +147,11 @@ def rename_keys(initial_dictionary: dict, aliases: dict) -> dict: @alias_param(name="rate", alias="r") def send( self, - sound: Optional[StringElement | List[StringElement]], - orbit: NumericElement = 0, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, + sound: Union[Optional[StringElement | List[StringElement]], Callable[[], Optional[StringElement | List[StringElement]]]], + orbit: Union[NumericElement, Callable[[], NumericElement]] = 0, + iterator: Union[Number, Callable[[], Number]] = 0, + divisor: Union[NumericElement, Callable[[], NumericElement]] = 1, + rate: Union[NumericElement, Callable[[], NumericElement]] = 1, **pattern: ParsableElement, ): if sound is None: @@ -155,19 +162,28 @@ def send( ): return + # Evaluate all potential callables + for key, value in pattern.items(): + pattern[key] = _resolve_if_callable(value) + # Replace some shortcut parameters by their real name pattern = self._parse_aliases(pattern) pattern = {**self._defaults, **pattern} - pattern["sound"] = sound - pattern["orbit"] = orbit + pattern["sound"] = _resolve_if_callable(sound) + pattern["orbit"] = _resolve_if_callable(orbit) pattern["cps"] = round(self.env.clock.phase, 1) pattern["cycle"] = ( self.env.clock.bar * self.env.clock.beats_per_bar ) + self.env.clock.beat deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["sound"] is None: continue serialized = list(chain(*sorted(message.items()))) @@ -178,15 +194,15 @@ def send( @alias_param(name="rate", alias="r") def send_ziffers( self, - sound: Optional[StringElement | List[StringElement]], - ziff: str, - orbit: NumericElement = 0, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - key: str = "C4", - scale: str = "IONIAN", - degrees: bool = False, + sound: Optional[StringElement | List[StringElement]] | Callable[[], Optional[StringElement | List[StringElement]]], + ziff: str | Callable[[], str], + orbit: NumericElement | Callable[[], NumericElement] = 0, + iterator: Number | Callable [[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + key: str | Callable[[], str] = "C4", + scale: str | Callable[[], str] = "IONIAN", + degrees: bool | Callable[[], bool] = False, **pattern: ParsableElement, ) -> int | float: # Replace some shortcut parameters by their real name @@ -198,12 +214,18 @@ def send_ziffers( ): return + # Evaluate all potential callables + for key, value in pattern.items(): + pattern[key] = _resolve_if_callable(value) + if not self._ziffers_parser: raise Exception("The ziffers package is not imported!") else: - ziffer = self._ziffers_parser(ziff, scale=scale, key=key, degrees=degrees)[ - iterator - ] + ziffer = self._ziffers_parser( + _resolve_if_callable(ziff), + scale=_resolve_if_callable(scale), + key=_resolve_if_callable(key), + degrees=_resolve_if_callable(degrees))[_resolve_if_callable(iterator)] try: freq = ziffer.freq except AttributeError: # if there is no note, it must be a silence @@ -225,16 +247,19 @@ def send_ziffers( return if sound != "rest": - pattern["freq"] = freq - pattern["sound"] = sound - pattern["orbit"] = orbit + pattern["freq"] = _resolve_if_callable(freq) + pattern["sound"] = _resolve_if_callable(sound) + pattern["orbit"] = _resolve_if_callable(orbit) pattern["cps"] = round(self.env.clock.phase, 4) pattern["cycle"] = ( self.env.clock.bar * self.env.clock.beats_per_bar ) + self.env.clock.beat - deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate)): if message["sound"] is None: continue serialized = list(chain(*sorted(message.items()))) From f1fcebf0d3c6c8edf86f739121d43fcf555ee72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 15 Jun 2024 21:31:57 +0200 Subject: [PATCH 11/15] Support Callables on every MIDI or OSC sender parameters --- sardine_core/handlers/midi.py | 232 +++++++++++++++++++++++----------- sardine_core/handlers/osc.py | 28 ++-- 2 files changed, 176 insertions(+), 84 deletions(-) diff --git a/sardine_core/handlers/midi.py b/sardine_core/handlers/midi.py index 1f8e9233..6724f6db 100644 --- a/sardine_core/handlers/midi.py +++ b/sardine_core/handlers/midi.py @@ -1,5 +1,9 @@ -from .sender import Number, NumericElement, Sender, ParsableElement -from typing import Optional, Union +from .sender import ( + Number, NumericElement, + Sender, ParsableElement, + _resolve_if_callable +) +from typing import Optional, Callable from ..utils import alias_param from ..logger import print import asyncio @@ -173,7 +177,7 @@ def _pitch_wheel(self, pitch: int, channel: int) -> None: self._midi.send(mido.Message("pitchweel", pitch=pitch, channel=channel)) async def send_off( - self, note: int, channel: int, velocity: int, delay: Union[int, float] + self, note: int, channel: int, velocity: int, delay: int | float ): await self.env.sleep_beats(delay) self._midi.send( @@ -243,12 +247,12 @@ async def send_midi_note( @alias_param(name="rate", alias="r") def send_control( self, - control: Optional[NumericElement] = 0, - channel: NumericElement = 0, - value: NumericElement = 60, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, + control: Optional[NumericElement] | Callable[[], NumericElement] = 0, + channel: NumericElement | Callable[[], NumericElement] = 0, + value: NumericElement | Callable[[], NumericElement] = 60, + iterator: Number | Callable[[], NumericElement] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, **rest_of_pattern: ParsableElement, ) -> None: """ @@ -264,10 +268,24 @@ def send_control( ): return - pattern = {"control": control, "channel": channel, "value": value} + pattern = { + "control": _resolve_if_callable(control), + "channel": _resolve_if_callable(channel), + "value": _resolve_if_callable(value) + } + + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if None in [message["control"], message["value"]]: continue for k, v in message.items(): @@ -282,10 +300,10 @@ def send_control( def send_program( self, channel: Optional[NumericElement], - program: NumericElement = 60, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, + program: NumericElement | Callable[[], NumericElement] = 60, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, **rest_of_pattern: ParsableElement, ) -> None: if channel is None: @@ -296,10 +314,23 @@ def send_program( ): return - pattern = {"channel": channel, "program": program} + pattern = { + "channel": _resolve_if_callable(channel), + "program": _resolve_if_callable(program) + } + + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["channel"] is None: continue for k, v in message.items(): @@ -313,12 +344,12 @@ def send_program( @alias_param(name="rate", alias="r") def send_sysex( self, - data: list[int], - value: NumericElement = 60, - optional_modulo: NumericElement = 127, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, + data: list[int] | Callable[[], list[int]], + value: NumericElement | Callable[[], NumericElement] = 60, + optional_modulo: NumericElement | Callable[[], NumericElement] = 127, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, **rest_of_pattern: ParsableElement, ) -> None: if data is None: @@ -329,9 +360,19 @@ def send_sysex( ): return - pattern = {"value": value} + pattern = { + "value": _resolve_if_callable(value) + } + + # NOTE: No need to resolve any more callables for such a simple message... + deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["value"] is None: continue for k, v in message.items(): @@ -350,15 +391,15 @@ def send_sysex( @alias_param(name="rate", alias="r") def send_ziffers( self, - ziff: str, - velocity: NumericElement = 100, - channel: NumericElement = 0, - duration: NumericElement = 1, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - scale: str = "IONIAN", - key: str = "C4", + ziff: str | Callable[[], str], + velocity: NumericElement | Callable[[], NumericElement] = 100, + channel: NumericElement | Callable[[], NumericElement] = 0, + duration: NumericElement | Callable[[], NumericElement] = 1, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + scale: str | Callable[[], str] = "IONIAN", + key: str | Callable[[], str] = "C4", **rest_of_pattern: ParsableElement, ) -> int | float: """ @@ -371,6 +412,7 @@ def send_ziffers( ): return + if not self._ziffers_parser: raise Exception("The ziffers package is not imported!") else: @@ -388,15 +430,25 @@ def send_ziffers( note = f"{{{' '.join([str(x) for x in note])}}}" pattern = { - "note": note, - "velocity": velocity, - "channel": channel, - "duration": duration, + "note": _resolve_if_callable(note), + "velocity": _resolve_if_callable(velocity), + "channel": _resolve_if_callable(channel), + "duration": _resolve_if_callable(duration), } + + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["note"] is None: continue for k in ("note", "velocity", "channel"): @@ -415,14 +467,14 @@ def send_ziffers( def send_instrument( self, note: Optional[NumericElement] = 60, - velocity: NumericElement = 100, - channel: NumericElement = 0, - duration: NumericElement = 1, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - map: dict = {}, - program_change: Optional[Number] = None, + velocity: NumericElement | Callable[[], NumericElement] = 100, + channel: NumericElement | Callable[[], NumericElement] = 0, + duration: NumericElement | Callable[[], NumericElement] = 1, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + map: dict | Callable[[], dict] = {}, + program_change: Optional[Number] | Callable[[], Number] = None, **rest_of_pattern: ParsableElement, ) -> None: """ @@ -446,15 +498,25 @@ def send_instrument( def note_pattern(): pattern = { - "note": note, - "velocity": velocity, - "channel": channel, - "duration": duration, - "program_change": (program_change if program_change else None), + "note": _resolve_if_callable(note), + "velocity": _resolve_if_callable(velocity), + "channel": _resolve_if_callable(channel), + "duration": _resolve_if_callable(duration), + "program_change": (_resolve_if_callable(program_change) if program_change else None), } + + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["program_change"] is not None: self._send_control( program=message["program_change"], channel=message["channel"] @@ -468,7 +530,12 @@ def note_pattern(): def send_controls(pattern: dict) -> None: deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if None in [message["control"], message["value"]]: continue for k, v in message.items(): @@ -486,11 +553,11 @@ def send_controls(pattern: dict) -> None: @alias_param(name="rate", alias="r") def send_controller( self, - channel: NumericElement = 0, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - map: dict = {}, + channel: NumericElement | Callable[[], NumericElement] = 0, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + map: dict | Callable[[], dict] = {}, **rest_of_pattern: ParsableElement, ) -> None: """ @@ -506,12 +573,17 @@ def send_controller( for key, value in map.items(): if key in rest_of_pattern.keys(): control = value - control["value"] = rest_of_pattern[key] + control["value"] = _resolve_if_callable(rest_of_pattern[key]) control_messages.append(control) def send_controls(pattern: dict) -> None: deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if None in [message["control"], message["value"]]: continue for k, v in message.items(): @@ -531,14 +603,14 @@ def send_controls(pattern: dict) -> None: @alias_param(name="program_change", alias="pgch") def send( self, - note: Optional[NumericElement] = 60, - velocity: NumericElement = 100, - channel: NumericElement = 0, - duration: NumericElement = 1, - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - program_change: Optional[Number] = None, + note: Optional[NumericElement] | Callable[[], Optional[NumericElement]] = 60, + velocity: NumericElement | Callable[[], NumericElement] = 100, + channel: NumericElement | Callable[[], NumericElement] = 0, + duration: NumericElement | Callable[[], NumericElement] = 1, + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], Number] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + program_change: Optional[Number] | Callable[[], Number] = None, **rest_of_pattern: ParsableElement, ) -> None: """ @@ -556,15 +628,25 @@ def send( return pattern = { - "note": note, - "velocity": velocity, - "channel": channel, - "duration": duration, - "program_change": program_change, + "note": _resolve_if_callable(note), + "velocity": _resolve_if_callable(velocity), + "channel": _resolve_if_callable(channel), + "duration": _resolve_if_callable(duration), + "program_change": _resolve_if_callable(program_change), } + + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["program_change"] is not None: self.send_program( program=message["program_change"], channel=message["channel"] diff --git a/sardine_core/handlers/osc.py b/sardine_core/handlers/osc.py index d1c65d5e..cac17ff6 100644 --- a/sardine_core/handlers/osc.py +++ b/sardine_core/handlers/osc.py @@ -1,6 +1,6 @@ import time from itertools import chain -from typing import Optional +from typing import Optional, Callable from osc4py3 import oscbuildparse from osc4py3.as_eventloop import * @@ -8,7 +8,7 @@ from ..utils import alias_param from .osc_loop import OSCLoop -from .sender import Number, NumericElement, Sender, StringElement +from .sender import Number, NumericElement, Sender, StringElement, _resolve_if_callable __all__ = ("OSCHandler",) @@ -96,11 +96,11 @@ def _make_bundle(self, messages: list) -> oscbuildparse.OSCBundle: @alias_param(name="sorted", alias="s") def send( self, - address: Optional[StringElement], - iterator: Number = 0, - divisor: NumericElement = 1, - rate: NumericElement = 1, - sort: bool = True, + address: Optional[StringElement] | Callable[[], StringElement], + iterator: Number | Callable[[], Number] = 0, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, + sort: bool | Callable[[], bool] = True, **pattern: NumericElement, ) -> None: if address is None: @@ -111,9 +111,19 @@ def send( ): return - pattern["address"] = address + # Evaluate all potential callables + for key, value in rest_of_pattern.items(): + pattern[key] = _resolve_if_callable(value) + + pattern["address"] = _resolve_if_callable(address) + deadline = self.env.clock.shifted_time - for message in self.pattern_reduce(pattern, iterator, divisor, rate): + for message in self.pattern_reduce( + pattern, + _resolve_if_callable(iterator), + _resolve_if_callable(divisor), + _resolve_if_callable(rate) + ): if message["address"] is None: continue address = message.pop("address") From 64246af164abce432fef5d5d2cf6cac847600f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 16 Jun 2024 00:47:41 +0200 Subject: [PATCH 12/15] Add edge case for handling 'n' parameter with SuperDirt sender The 'n' parameter can now be used to change the sample number. The old behavior of 'n' is still accessible through the 'midinote' or 'midi' parameters. --- sardine_core/handlers/superdirt.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sardine_core/handlers/superdirt.py b/sardine_core/handlers/superdirt.py index 3ef78e87..294fb173 100644 --- a/sardine_core/handlers/superdirt.py +++ b/sardine_core/handlers/superdirt.py @@ -106,11 +106,23 @@ def _send(self, address, message): self.__send(address=address, message=message) def _dirt_play(self, message: list): + # TODO: custom logic here? self._send_timed_message(address="/dirt/play", message=message) def _dirt_panic(self): self._dirt_play(message=["sound", "superpanic"]) + def _handle_sample_number(self, message: dict): + if ":" in message["sound"]: + orig_sp, orig_nb = message["sound"].split(":") + message["sound"] = orig_sp + ":" + str( + int(orig_nb) + int(message['n']) + ) + else: + message["sound"] = message['sound'] + ":" + str(message['n']) + del message["n"] + return message + def _parse_aliases(self, pattern: dict): """Parse aliases for certain keys in the pattern (lpf -> cutoff)""" @@ -126,7 +138,6 @@ def rename_keys(initial_dictionary: dict, aliases: dict) -> dict: "bpq": "resonance", "res": "resonance", "midi": "midinote", - "n": "midinote", "oct": "octave", "accel": "accelerate", "leg": "legato", @@ -186,6 +197,8 @@ def send( ): if message["sound"] is None: continue + if "n" in message and message["sound"] is not None: + message = self._handle_sample_number(message) serialized = list(chain(*sorted(message.items()))) self.call_timed(deadline, self._dirt_play, serialized) From a15f360e1c9c0b5b2fb0918dbc1dcdf71e2208d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 16 Jun 2024 00:51:34 +0200 Subject: [PATCH 13/15] Rewrite the 'solo' function --- sardine_core/handlers/midi.py | 45 +++++++++++++------------- sardine_core/handlers/osc.py | 2 +- sardine_core/handlers/sender.py | 2 ++ sardine_core/handlers/superdirt.py | 40 +++++++++++++---------- sardine_core/run.py | 13 +++++--- sardine_core/scheduler/async_runner.py | 8 ++--- sardine_core/superdirt/process.py | 4 ++- 7 files changed, 63 insertions(+), 51 deletions(-) diff --git a/sardine_core/handlers/midi.py b/sardine_core/handlers/midi.py index 6724f6db..a6516d3c 100644 --- a/sardine_core/handlers/midi.py +++ b/sardine_core/handlers/midi.py @@ -1,7 +1,9 @@ from .sender import ( - Number, NumericElement, - Sender, ParsableElement, - _resolve_if_callable + Number, + NumericElement, + Sender, + ParsableElement, + _resolve_if_callable, ) from typing import Optional, Callable from ..utils import alias_param @@ -271,7 +273,7 @@ def send_control( pattern = { "control": _resolve_if_callable(control), "channel": _resolve_if_callable(channel), - "value": _resolve_if_callable(value) + "value": _resolve_if_callable(value), } # Evaluate all potential callables @@ -281,10 +283,10 @@ def send_control( pattern = {**self._defaults, **pattern} deadline = self.env.clock.shifted_time for message in self.pattern_reduce( - pattern, + pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if None in [message["control"], message["value"]]: continue @@ -316,7 +318,7 @@ def send_program( pattern = { "channel": _resolve_if_callable(channel), - "program": _resolve_if_callable(program) + "program": _resolve_if_callable(program), } # Evaluate all potential callables @@ -329,7 +331,7 @@ def send_program( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["channel"] is None: continue @@ -360,9 +362,7 @@ def send_sysex( ): return - pattern = { - "value": _resolve_if_callable(value) - } + pattern = {"value": _resolve_if_callable(value)} # NOTE: No need to resolve any more callables for such a simple message... @@ -371,7 +371,7 @@ def send_sysex( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["value"] is None: continue @@ -394,10 +394,10 @@ def send_ziffers( ziff: str | Callable[[], str], velocity: NumericElement | Callable[[], NumericElement] = 100, channel: NumericElement | Callable[[], NumericElement] = 0, - duration: NumericElement | Callable[[], NumericElement] = 1, + duration: NumericElement | Callable[[], NumericElement] = 1, iterator: Number | Callable[[], Number] = 0, - divisor: NumericElement | Callable[[], NumericElement] = 1, - rate: NumericElement | Callable[[], NumericElement] = 1, + divisor: NumericElement | Callable[[], NumericElement] = 1, + rate: NumericElement | Callable[[], NumericElement] = 1, scale: str | Callable[[], str] = "IONIAN", key: str | Callable[[], str] = "C4", **rest_of_pattern: ParsableElement, @@ -412,7 +412,6 @@ def send_ziffers( ): return - if not self._ziffers_parser: raise Exception("The ziffers package is not imported!") else: @@ -447,7 +446,7 @@ def send_ziffers( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["note"] is None: continue @@ -502,7 +501,9 @@ def note_pattern(): "velocity": _resolve_if_callable(velocity), "channel": _resolve_if_callable(channel), "duration": _resolve_if_callable(duration), - "program_change": (_resolve_if_callable(program_change) if program_change else None), + "program_change": ( + _resolve_if_callable(program_change) if program_change else None + ), } # Evaluate all potential callables @@ -515,7 +516,7 @@ def note_pattern(): pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["program_change"] is not None: self._send_control( @@ -534,7 +535,7 @@ def send_controls(pattern: dict) -> None: pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if None in [message["control"], message["value"]]: continue @@ -582,7 +583,7 @@ def send_controls(pattern: dict) -> None: pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if None in [message["control"], message["value"]]: continue @@ -645,7 +646,7 @@ def send( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["program_change"] is not None: self.send_program( diff --git a/sardine_core/handlers/osc.py b/sardine_core/handlers/osc.py index cac17ff6..6ec333cd 100644 --- a/sardine_core/handlers/osc.py +++ b/sardine_core/handlers/osc.py @@ -122,7 +122,7 @@ def send( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["address"] is None: continue diff --git a/sardine_core/handlers/sender.py b/sardine_core/handlers/sender.py index cab831e9..0e228bfe 100644 --- a/sardine_core/handlers/sender.py +++ b/sardine_core/handlers/sender.py @@ -32,10 +32,12 @@ def _maybe_index(val: RecursiveElement, i: int) -> RecursiveElement: length = len(val) return val[i % length] + def _resolve_if_callable(val: Callable[[], Any] | Any) -> Any: """Evaluate a callable if it is one, otherwise return the value.""" return val() if callable(val) else val + def _maybe_length(val: RecursiveElement) -> int: if isinstance(val, list): return len(val) diff --git a/sardine_core/handlers/superdirt.py b/sardine_core/handlers/superdirt.py index 294fb173..3ad97f35 100644 --- a/sardine_core/handlers/superdirt.py +++ b/sardine_core/handlers/superdirt.py @@ -8,12 +8,12 @@ from ..utils import alias_param from .osc_loop import OSCLoop from .sender import ( - Number, - NumericElement, - ParsableElement, - Sender, - StringElement, - _resolve_if_callable + Number, + NumericElement, + ParsableElement, + Sender, + StringElement, + _resolve_if_callable, ) __all__ = ("SuperDirtHandler",) @@ -115,11 +115,9 @@ def _dirt_panic(self): def _handle_sample_number(self, message: dict): if ":" in message["sound"]: orig_sp, orig_nb = message["sound"].split(":") - message["sound"] = orig_sp + ":" + str( - int(orig_nb) + int(message['n']) - ) + message["sound"] = orig_sp + ":" + str(int(orig_nb) + int(message["n"])) else: - message["sound"] = message['sound'] + ":" + str(message['n']) + message["sound"] = message["sound"] + ":" + str(message["n"]) del message["n"] return message @@ -158,7 +156,10 @@ def rename_keys(initial_dictionary: dict, aliases: dict) -> dict: @alias_param(name="rate", alias="r") def send( self, - sound: Union[Optional[StringElement | List[StringElement]], Callable[[], Optional[StringElement | List[StringElement]]]], + sound: Union[ + Optional[StringElement | List[StringElement]], + Callable[[], Optional[StringElement | List[StringElement]]], + ], orbit: Union[NumericElement, Callable[[], NumericElement]] = 0, iterator: Union[Number, Callable[[], Number]] = 0, divisor: Union[NumericElement, Callable[[], NumericElement]] = 1, @@ -190,10 +191,10 @@ def send( deadline = self.env.clock.shifted_time for message in self.pattern_reduce( - pattern, + pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate) + _resolve_if_callable(rate), ): if message["sound"] is None: continue @@ -207,10 +208,13 @@ def send( @alias_param(name="rate", alias="r") def send_ziffers( self, - sound: Optional[StringElement | List[StringElement]] | Callable[[], Optional[StringElement | List[StringElement]]], + sound: ( + Optional[StringElement | List[StringElement]] + | Callable[[], Optional[StringElement | List[StringElement]]] + ), ziff: str | Callable[[], str], orbit: NumericElement | Callable[[], NumericElement] = 0, - iterator: Number | Callable [[], Number] = 0, + iterator: Number | Callable[[], Number] = 0, divisor: NumericElement | Callable[[], NumericElement] = 1, rate: NumericElement | Callable[[], NumericElement] = 1, key: str | Callable[[], str] = "C4", @@ -238,7 +242,8 @@ def send_ziffers( _resolve_if_callable(ziff), scale=_resolve_if_callable(scale), key=_resolve_if_callable(key), - degrees=_resolve_if_callable(degrees))[_resolve_if_callable(iterator)] + degrees=_resolve_if_callable(degrees), + )[_resolve_if_callable(iterator)] try: freq = ziffer.freq except AttributeError: # if there is no note, it must be a silence @@ -272,7 +277,8 @@ def send_ziffers( pattern, _resolve_if_callable(iterator), _resolve_if_callable(divisor), - _resolve_if_callable(rate)): + _resolve_if_callable(rate), + ): if message["sound"] is None: continue serialized = list(chain(*sorted(message.items()))) diff --git a/sardine_core/run.py b/sardine_core/run.py index a763d021..cec55ac6 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -368,10 +368,15 @@ def silence(*runners: AsyncRunner) -> None: def solo(*args): - """Soloing a single player out of all running players""" - for pat in bowl.scheduler.runners: - if pat.name not in args: - silence(pat) + """Soloing a single player out of all running players, excluding background job.""" + foreground_runner_names = { + runner.name for runner in bowl.scheduler.runners if not runner.background_job + } + args_names = {runner.name for runner in args} + names_to_silence = foreground_runner_names - args_names + for runner in bowl.scheduler.runners: + if runner.name in names_to_silence: + silence(runner) def panic(*runners: AsyncRunner) -> None: diff --git a/sardine_core/scheduler/async_runner.py b/sardine_core/scheduler/async_runner.py index ea45fe17..e5f86d45 100644 --- a/sardine_core/scheduler/async_runner.py +++ b/sardine_core/scheduler/async_runner.py @@ -586,9 +586,7 @@ async def _runner(self): raise exc if not self.background_job: - print( - f"[yellow][[red]{self.name}[/red] is swimming][/yellow]" - ) + print(f"[yellow][[red]{self.name}[/red] is swimming][/yellow]") try: while self._is_ready_for_iteration(): @@ -602,9 +600,7 @@ async def _runner(self): self._revert_state() self.swim() finally: - print( - f"[yellow][Stopped [red]{self.name}[/red]][/yellow]" - ) + print(f"[yellow][Stopped [red]{self.name}[/red]][/yellow]") def _prepare(self): self._last_expected_time = -math.inf diff --git a/sardine_core/superdirt/process.py b/sardine_core/superdirt/process.py index 5b19f13c..19b64a80 100644 --- a/sardine_core/superdirt/process.py +++ b/sardine_core/superdirt/process.py @@ -95,7 +95,9 @@ def _analyze_and_warn(self, decoded_line: str): sample_name = decoded_line.split("'") print(f"[[red]/!\\\\[/red] - Sample {sample_name[1]} not found]") if "late 0." in decoded_line: - print(f"[yellow][[red]/!\\\\[/red] - Late messages. Increase SC latency][/yellow]") + print( + f"[yellow][[red]/!\\\\[/red] - Late messages. Increase SC latency][/yellow]" + ) if "listening on port 57120" in decoded_line: print(f"[yellow][[green]/!\\\\[/green] - Audio server ready!][/yellow]") if self._synth_directory is not None: From 3b36295d0a80e06a15abed3895dd8add12be0ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 16 Jun 2024 00:54:08 +0200 Subject: [PATCH 14/15] add the convenience 'runners' function --- sardine_core/run.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sardine_core/run.py b/sardine_core/run.py index cec55ac6..89043eef 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -379,6 +379,12 @@ def solo(*args): silence(runner) +def runners(): + """Return all currently active AsyncRunners""" + condition = lambda x: x.name if x.name != "tidal_loop" else "internal" + return list(map(condition, bowl.scheduler.runners)) + + def panic(*runners: AsyncRunner) -> None: """ If SuperCollider/SuperDirt is booted, panic acts as a more powerful alternative to From b19877e45197e12773be7d6e2c508a34b50abc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 16 Jun 2024 12:30:28 +0200 Subject: [PATCH 15/15] Add as_text argument in P object to print patterns Adding the 'as_text' parameter to the Pat function in order to print patterns in a textual format. This can be quite useful for debugging. --- sardine_core/run.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/sardine_core/run.py b/sardine_core/run.py index 89043eef..dde4cadb 100644 --- a/sardine_core/run.py +++ b/sardine_core/run.py @@ -397,7 +397,13 @@ def panic(*runners: AsyncRunner) -> None: D("superpanic") -def Pat(pattern: str, i: int = 0, div: int = 1, rate: int = 1) -> Any: +def Pat( + pattern: str, + i: int = 0, + div: int = 1, + rate: int = 1, + as_text: bool = False + ) -> Any: """ General purpose pattern interface. This function can be used to summon the global parser stored in the fish_bowl. It is generally used to pattern outside of the @@ -405,6 +411,9 @@ def Pat(pattern: str, i: int = 0, div: int = 1, rate: int = 1) -> Any: if you want to take the best of the patterning system without having to deal with all the built-in I/O. + The as_text argument allows the study of patterns in textual format. If as_text is + true, the pattern will print from index 0 up to i. + Args: pattern (str): A pattern to be parsed i (int, optional): Index for iterators. Defaults to 0. @@ -413,7 +422,14 @@ def Pat(pattern: str, i: int = 0, div: int = 1, rate: int = 1) -> Any: int: The ith element from the resulting pattern """ result = bowl.parser.parse(pattern) - return Sender.pattern_element(result, i, div, rate) + if print: + pattern = [] + for iterator in range(i): + pattern.append(Sender.pattern_element(result, iterator, div, rate)) + print(pattern) + return pattern + else: + return Sender.pattern_element(result, i, div, rate) class Delay: