from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from mesonic.events import SynthEvent, SynthEventType
if TYPE_CHECKING:
from mesonic.context import Context
[docs]
@dataclass
class ParameterInfo:
name: str
default: Any
min_value = None
max_value = None
[docs]
class Parameter:
"""A Parameter of a Synth.
Parameters
----------
synth : Synth
The Synth to which this Parameter belongs.
name : str
The name of the Parameter.
default : Any
The default Paramter value.
min_value : Any, optional
The min value possible for this Parameter, by default None
None means there is no check.
max_value : Any, optional
The max value possible for this Parameter, by default None
None means there is no check.
"""
def __init__(
self,
synth: "Synth",
name: str,
default,
min_value=None,
max_value=None,
):
self._synth = synth
self._name = name
self._value = None
self._default = default
self._bounds = (min_value, max_value)
# properties
@property
def default(self):
"""Default value of the Parameter."""
return self._default
@property
def max(self) -> Any:
"""Maximum allowed value of the Parameter.
If it is None there is no check when setting."""
return self._bounds[1]
@max.setter
def max(self, value):
self._bounds = (self._bounds[0], value)
@property
def min(self) -> Any:
"""Maximum allowed value of the Parameter.
If it is None there is no check when setting."""
return self._bounds[0]
@min.setter
def min(self, value):
self._bounds = (value, self._bounds[1])
@property
def bounds(self) -> Tuple[Any, Any]:
"""The bounds (min, max) of the Parameter"""
return self._bounds
@bounds.setter
def bounds(self, value):
min, max = value
self.min = min
self.max = max
@property
def name(self) -> str:
"""str: The Paramter name."""
return self._name
@property
def value(self) -> Any:
"""The Parameter value.
If no explicit value is set return the default Paramter value.
Setting value is the same as using the set method.
"""
return self._default if self._value is None else self._value
@value.setter
def value(self, value):
if value is self:
return
self.set(value, None)
[docs]
def set(self, value, info=None):
"""Set the Parameter value.
Parameters
----------
value : Any
The new Parameter value.
info : Any
Additional information about the Event.
"""
# create event data based on old and new values
data = {"name": self._name, "new_value": value, "old_value": self._value}
cache_only = not self._synth.mutable
self._verify_and_adjust(value, cache_only)
if not cache_only:
self._synth._send_event(SynthEventType.SET, data=data, info=info)
def _verify_and_adjust(self, value, cache_only: bool = False):
"""Verify and adjust the value.
Parameters
----------
value : Any
The new Parameter value.
cache_only : bool, optional
True when we want to ignore mutable, by default False
Raises
------
ValueError
If value is out of bounds or Synth is not mutable and cache_only is True
"""
if not self._synth.mutable and not cache_only:
raise ValueError("Synth not mutable")
if self.max is not None and value > self.max:
raise ValueError(
f"Value larger then allowed value: {value} > {self.max} (max)"
)
if self.min is not None and value < self.min:
raise ValueError(
f"Value smaller then allowed value: {value} < {self.min} (min)"
)
self._value = value
def __iadd__(self, other) -> "Parameter":
self.value += other
return self
def __isub__(self, other) -> "Parameter":
self.value -= other
return self
def __imul__(self, other) -> "Parameter":
self.value *= other
return self
def __itruediv__(self, other) -> "Parameter":
self.value /= other
return self
def __ifloordiv__(self, other) -> "Parameter":
self.value //= other
return self
def __repr__(self) -> str:
return (
f"Parameter({self.name}={self.value:.2f}, "
f"default={self.default:.2f}, bounds={self._bounds})"
)
[docs]
class Synth:
"""A controllable audio source.
Parameters
----------
context : Context
Context for this Synth
name : str
name of this Synth
mutable : bool
True if this Synth is mutable
param_info : List[ParameterInfo]
Parameter information of the Synth
track : int, optional
track of the Synth, by default 0
metadata : Optional[Dict], optional
additional metadata, by default None
"""
def __init__(
self,
context: "Context",
name: str,
mutable: bool,
param_info: List[ParameterInfo],
track: int = 0,
metadata: Optional[Dict] = None,
):
# using object.__settattr__ to avoid the overwritten __setattr__
# for details see https://github.com/dreinsch/mesonic/issues/6
object.__setattr__(self, "_context", context)
object.__setattr__(self, "_name", name)
object.__setattr__(self, "_mutable", mutable)
object.__setattr__(
self,
"_params",
{
pi.name: Parameter(
self, pi.name, pi.default, pi.min_value, pi.max_value
)
for pi in param_info
},
)
object.__setattr__(self, "_track", track)
object.__setattr__(self, "metadata", metadata or {})
@property
def name(self) -> str:
"""str: name of the Synth."""
return self._name
@property
def track(self) -> int:
"""int: track of the Synth."""
return self._track
@property
def context(self) -> "Context":
"""Context: Context in which the Synth happens."""
return self._context
@property
def mutable(self) -> bool:
"""bool: True if this is a mutable Synth."""
return self._mutable
@property
def params(self) -> Dict[str, "Parameter"]:
"""Dict[str, "Parameter"]: The Parameters of this Synths."""
return self._params
# scheduled methods
[docs]
def start(self, params: Optional[Dict[str, Any]] = None, info=None, **kwargs):
"""Start the Synth.
Parameters
----------
params : Optional[Dict[str, Any]], optional
A dict with (name, value) pairs for the Parameters, by default None
info : Any, optional
Additional information about the Event, by default None, by default None
kwargs: Any, optional
Additional keyword arguments are added to params.
"""
if params or kwargs:
kwargs |= params or {}
for name, value in kwargs.items():
assert (
name in self._params
), f"'{name}' is not in this Synths Parameters"
self._params[name]._verify_and_adjust(value, cache_only=True)
data = self._param_values()
self._send_event(SynthEventType.START, data, info)
[docs]
def stop(self, info=None):
"""Stop the Synth.
Parameters
----------
info : Any, optional
Additional information about the Event, by default None, by default None
"""
self._send_event(SynthEventType.STOP, self._param_values(), info)
[docs]
def pause(self, info=None):
"""Pause the Synth.
Parameters
----------
info : Any, optional
Additional information about the Event, by default None, by default None
"""
self._send_event(SynthEventType.PAUSE, self._param_values(), info)
[docs]
def resume(self, info=None):
"""Resume the Synth.
Parameters
----------
info : Any, optional
Additional information about the Event, by default None, by default None
"""
self._send_event(SynthEventType.RESUME, self._param_values(), info)
[docs]
def set(self, params: Optional[Dict[str, Any]] = None, info=None, **kwargs):
"""Set the Parameters of the Synth.
Parameters
----------
params : Optional[Dict[str, Any]], optional
A dict with (name, value) pairs for the Parameters.
info : Any, optional
Additional information about the Event, by default None, by default None
kwargs: Any, optional
Additional keyword arguments are added to params.
"""
if params or kwargs:
kwargs |= params or {}
for name, value in kwargs.items():
self._params[name].set(value, info=info)
def _param_values(self) -> Dict[str, Any]:
"""Get the current best value for each Parameter.
Returns
-------
Dict[str, Any]
A dict with the param names and values.
"""
return {n: p.value for n, p in self._params.items()}
def _send_event(
self,
etype: SynthEventType,
data: Dict[str, Any],
info: Optional[Dict[str, Any]] = None,
):
"""Send a SynthEvent
Parameters
----------
etype : SynthEventType
Type of SynthEvent
data : Dict[str, Any]
Data about the SynthEvent
info : Any
Additional information about the Event.
Raises
------
RuntimeError
If a immutable Synth tries to send another SynthEventType as START
"""
if not self._mutable and etype is not SynthEventType.START:
raise RuntimeError(
f"Immutable Synth can only produce {SynthEventType.START} "
f"but got {etype}"
)
einfo: Dict = {}
einfo |= self.metadata
if info:
einfo |= info
event = SynthEvent(
track=self._track,
info=einfo,
synth=self,
etype=etype,
data=data,
)
self._context.receive_event(event)
def __getattr__(self, name: str):
if name in self._params:
return self._params[name]
return object.__getattribute__(self, name)
def __getitem__(self, name: str):
return self._params[name]
def __setattr__(self, name, value):
# First try regular attribute access.
try:
object.__getattribute__(self, name)
except AttributeError:
if name not in self._params:
raise AttributeError("can't set attribute")
self._params[name].value = value
else:
object.__setattr__(self, name, value)
def __repr__(self) -> str:
values = {
k: round(v, 2) if isinstance(v, float) else v
for k, v in self._param_values().items()
}
return f"Synth({self.name}, {values})"