"""Andesite audio filters.
Attributes:
FILTER_MAP (Mapping[str, Type[Filter]]): Mapping from filter name to filter class.
See: `get_filter_model`.
FilterMapLike (Union[FilterMap, Dict[str, Union[Filter, RawDataType]]]): (Type alias) Type of objects which
can be used as filter maps. This includes the `FilterMap`.
"""
import abc
from dataclasses import dataclass, field
from operator import eq
from typing import Any, Dict, Iterable, Iterator, List, Mapping, MutableMapping, Optional, Set, Type, TypeVar, Union, \
overload
from andesite.transform import RawDataType, build_from_raw, convert_to_raw
__all__ = ["Filter",
"EqualizerBand", "Equalizer",
"Karaoke",
"Timescale",
"Tremolo",
"Vibrato",
"VolumeFilter",
"get_filter_model",
"FilterMap", "FilterMapLike"]
def _ensure_in_interval(value: float, *,
low: float = None, low_inc: float = None,
up: float = None, up_inc: float = None) -> None:
"""Ensure a value is within an interval.
Raises:
ValueError: If the provided value isn't within the given
constraints.
"""
low_symbol: Optional[str] = None
up_symbol: Optional[str] = None
valid: bool = True
if low_inc is not None:
low_symbol = f"[{low_inc}"
if not value >= low:
valid = False
elif low is not None:
low_symbol = f"({low}"
if not value > low:
valid = False
if up_inc is not None:
up_symbol = f"{up_inc}]"
if not value <= up_inc:
valid = False
elif up is not None:
up_symbol = f"{up})"
if value < up_inc:
valid = False
if not valid:
low_symbol = low_symbol or "[-INF"
up_symbol = up_symbol or "INF]"
raise ValueError(f"Provided value ({value}) not in interval {low_symbol}, {up_symbol}!")
class _Filter(abc.ABC):
"""Filter with name.
Attributes:
__filter_name__ (str): Name of the filter.
This is a magic attribute used by the library
to convert the filter into its Andesite representation.
"""
__filter_name__: str
[docs]@dataclass
class Filter(_Filter, abc.ABC):
"""Audio filter for Andesite.
Attributes:
enabled (bool): Whether or not the filter is enabled.
This value is mostly useful when receiving the filters from Andesite.
However you can also set it to `False` when sending filters. This
will cause the settings to be ignored and instead the default values
are sent to Andesite which will cause the filter to be disabled.
When creating a new `Filter` instance its values are set to the
default value.
"""
enabled: bool = True
[docs] def reset(self) -> None:
"""Reset the filter settings back to their default values."""
self.__init__()
@classmethod
def __transform_output__(cls, data: RawDataType) -> RawDataType:
enabled = data.pop("enabled")
if enabled:
return data
else:
# create a new instance (which uses the defaults) and return its data
return convert_to_raw(cls())
[docs]@dataclass
class EqualizerBand:
"""
Attributes:
band (int): band number to configure ( 0 - 14 )
gain (float): value to set for the band ( [-0.25, 1.0] )
"""
band: int
gain: float = 0.0
[docs] def set_band(self, value: int) -> None:
"""Setter for :py:attr:`band` which performs a value check.
Args:
value: Value to set for the band. ( [0, 14] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low_inc=0, up_inc=14)
self.band = value
[docs] def set_gain(self, value: float) -> None:
"""Setter for :py:attr:`gain` which performs a value check.
Args:
value: Value to set for the gain. ( [-0.25, 1] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low_inc=-.25, up_inc=1)
self.gain = value
[docs]@dataclass
class Equalizer(Filter):
"""
Attributes:
bands (List[EqualizerBand]): array of bands to configure
"""
__filter_name__ = "equalizer"
bands: List[EqualizerBand] = field(default_factory=list)
def __iter__(self) -> Iterator[EqualizerBand]:
return iter(self.bands)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Equalizer):
for my_gain, other_gain in zip(self.iter_band_gains(), other.iter_band_gains()):
if my_gain != other_gain:
return False
return True
else:
return NotImplemented
def __hash__(self) -> int:
return hash(self.iter_band_gains())
@classmethod
def __transform_input__(cls, data: RawDataType) -> None:
# Andesite sends the equalizer filter as an array of floats
bands = data["bands"]
for i, gain in enumerate(bands):
bands[i] = EqualizerBand(i, gain)
[docs] @classmethod
def from_gains(cls, gains: Iterable[Optional[float]]) -> "Equalizer":
"""Create an `Equalizer` filter from a list of gains.
Args:
gains: Iterable of `float` which correspond to the gain for the band,
or `None` if the band doesn't specify a gain.
"""
bands: List[EqualizerBand] = []
for i, gain in enumerate(gains):
if gain is not None:
bands.append(EqualizerBand(i, gain))
return cls(True, bands)
@overload
def get_band(self, band: int) -> EqualizerBand:
...
@overload
def get_band(self, band: int, create: bool) -> Optional[EqualizerBand]:
...
[docs] def get_band(self, band: int, create: bool = True) -> Optional[EqualizerBand]:
"""Get the specified band from the bands list.
If the band doesn't exist it is created.
If you don't want to automatically create a band, pass
`create=False`.
Args:
band: Band number to get
create: Whether or not to create a new band if it doesn't exist. (Defaults to True)
"""
try:
return next(band for band in self.bands if band.band == band)
except StopIteration:
pass
if not create:
return None
band = EqualizerBand(band)
self.bands.append(band)
return band
[docs] def get_band_gain(self, band: int) -> Optional[float]:
"""Get the gain of a band.
Returns:
Gain of the band or `None` if it doesn't exist.
"""
band = self.get_band(band, create=False)
if band:
return band.gain
else:
return None
[docs] def set_band_gain(self, band: int, gain: float) -> None:
"""Set the gain of a band to the specified value.
If the band does not exist it is created.
Args:
band: Band number to set the gain for.
gain: Value to set for the gain. ( [-0.25, 1] )
Raises:
ValueError: if the provided gain is invalid.
"""
self.get_band(band).set_gain(gain)
@overload
def iter_band_gains(self, use_default: bool) -> List[Optional[float]]:
...
@overload
def iter_band_gains(self) -> List[float]:
...
[docs] def iter_band_gains(self, use_default: bool = True) -> List[Optional[float]]:
"""Get a list of all the bands' gains in order.
Args:
use_default: Whether or not to replace non-existent values
with the default gain.
If `False` and band doesn't have a gain set, `None`
is used instead.
"""
default_value: Union[float, None] = EqualizerBand.gain if use_default else None
gains: List[Optional[float]] = 15 * [default_value]
for band in self:
gain = band.gain
if use_default and gain is None:
continue
gains[band.band] = gain
return gains
[docs]@dataclass
class Karaoke(Filter):
"""
Attributes:
level (float)
mono_level (float)
filter_band (float)
filter_width (float)
"""
__filter_name__ = "karaoke"
level: float = 1.0
mono_level: float = 1.0
filter_band: float = 220.0
filter_width: float = 100.0
[docs]@dataclass
class Timescale(Filter):
"""
Attributes:
speed (float): speed to play music at (> 0)
pitch (float): pitch to set (> 0)
rate (float): rate to set (> 0)
"""
__filter_name__ = "timescale"
speed: float = 1.0
pitch: float = 1.0
rate: float = 1.0
[docs] def set_speed(self, value: float) -> None:
"""Setter for :py:attr:`speed` which performs a value check.
Args:
value: Value to set for the speed. ( (0, INF] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0)
self.speed = value
[docs] def set_pitch(self, value: float) -> None:
"""Setter for :py:attr:`pitch` which performs a value check.
Args:
value: Value to set for the pitch. ( (0, INF] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0)
self.pitch = value
[docs] def set_rate(self, value: float) -> None:
"""Setter for :py:attr:`rate` which performs a value check.
Args:
value: Value to set for the rate. ( (0, INF] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0)
self.rate = value
[docs]@dataclass
class Tremolo(Filter):
"""
Attributes:
frequency (float): (> 0)
depth (float): ( (0, 1] )
"""
__filter_name__ = "tremolo"
frequency: float = 2.0
depth: float = 0.5
[docs] def set_frequency(self, value: float) -> None:
"""Setter for :py:attr:`frequency` which performs a value check.
Args:
value: Value to set for the frequency. ( (0, INF] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0)
self.frequency = value
[docs] def set_depth(self, value: float) -> None:
"""Setter for :py:attr:`depth` which performs a value check.
Args:
value: Value to set for the depth. ( (0, 1] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0, up_inc=1)
self.depth = value
[docs]@dataclass
class Vibrato(Filter):
"""
Attributes:
frequency (float): ( (0, 14] )
depth (float): ( (0, 1] )
"""
__filter_name__ = "vibrato"
frequency: float = 2.0
depth: float = 0.5
[docs] def set_frequency(self, value: float) -> None:
"""Setter for :py:attr:`frequency` which performs a value check.
Args:
value: Value to set for the frequency. ( (0, 14] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0, up_inc=14)
self.frequency = value
[docs] def set_depth(self, value: float) -> None:
"""Setter for :py:attr:`depth` which performs a value check.
Args:
value: Value to set for the depth. ( (0, 1] )
Raises:
ValueError: if the provided value is invalid.
"""
_ensure_in_interval(value, low=0, up_inc=1)
self.depth = value
[docs]@dataclass
class VolumeFilter(Filter):
"""Volume filter settings.
Attributes:
volume (float): Volume modifier. This acts as a factor for the actual volume.
"""
__filter_name__ = "volume"
volume: float = 1.0
_FILTERS: Set[Type[Filter]] = {Equalizer, Karaoke, Timescale, Tremolo, Vibrato, VolumeFilter}
FILTER_MAP: Mapping[str, Type[Filter]] = {andesite_filter.__filter_name__: andesite_filter for andesite_filter in
_FILTERS}
[docs]def get_filter_model(name: str) -> Optional[Type[Filter]]:
"""Get the corresponding filter model for the given name.
If no model for the name exists, `None` is returned.
Args:
name: Name of the filter
"""
return FILTER_MAP.get(name)
FT = TypeVar("FT", bound=Filter)
[docs]@dataclass
class FilterMap(MutableMapping):
"""Custom mapping type for filters.
Attributes:
filters (Dict[str, Any]): Dictionary containing all filters.
Theoretically this is just a wrapper around the `filters` dictionary which
contains the actual filter data. The class exposes the known filters as
properties, but it also supports unknown filters should the library
become outdated.
You can also use this as a wrapper for an existing filter dict.
"""
filters: Dict[str, Any] = field(default_factory=dict)
def __init__(self, filters: "FilterMapLike") -> None:
if isinstance(filters, FilterMap):
self.filters = filters.filters.copy()
else:
self.filters = filters
def __eq__(self, other) -> bool:
if isinstance(other, FilterMap):
return eq(self.filters, other.filters)
elif isinstance(other, Mapping):
return eq(self.filters, other)
else:
return NotImplemented
def __hash__(self) -> int:
return hash(self.filters)
def __len__(self) -> int:
return len(self.filters)
def __iter__(self) -> Iterator[str]:
return iter(self.filters)
def __getitem__(self, item: str) -> Any:
return self.filters[item]
def __setitem__(self, key: str, value: Any) -> None:
self.filters[key] = value
def __delitem__(self, key: str) -> None:
del self.filters[key]
[docs] def get_filter(self, name: str, cls: Type[FT]) -> FT:
"""Get the filter with the name.
Args:
name: Name of the filter to get
cls: `Filter` class to use for the filter.
"""
try:
value = self[name]
except KeyError:
value = self[name] = cls()
else:
if not isinstance(value, cls):
if isinstance(value, dict):
value = self[name] = build_from_raw(cls, value)
else:
raise TypeError(f"Expected {cls}, found {type(value)!r}: {value}")
return value
@property
def equalizer(self) -> Equalizer:
"""Equalizer filter settings"""
return self.get_filter("equalizer", Equalizer)
@property
def karaoke(self) -> Karaoke:
"""Karaoke filter settings"""
return self.get_filter("karaoke", Karaoke)
@property
def timescale(self) -> Timescale:
"""Timescale filter settings"""
return self.get_filter("timescale", Timescale)
@property
def tremolo(self) -> Tremolo:
"""Tremolo filter settings"""
return self.get_filter("tremolo", Tremolo)
@property
def vibrato(self) -> Vibrato:
"""Vibrato filter settings"""
return self.get_filter("vibrato", Vibrato)
@property
def volume(self) -> VolumeFilter:
"""Volume filter settings"""
return self.get_filter("equalizer", VolumeFilter)
[docs] def set_filter(self, andesite_filter: Filter) -> None:
"""Set the value for a filter.
Args:
andesite_filter: Filter to set
"""
self[andesite_filter.__filter_name__] = andesite_filter
@classmethod
def __transform_input__(cls, data: RawDataType) -> RawDataType:
filters: RawDataType = {}
for name, filter_value in data.items():
filter_cls = get_filter_model(name)
if filter_cls is not None:
filter_value = build_from_raw(filter_cls, filter_value)
filters[name] = filter_value
return dict(filters=filters)
@classmethod
def __transform_output__(cls, data: RawDataType) -> RawDataType:
return data["filters"]
FilterMapLike = Union[FilterMap, Dict[str, Union[Filter, RawDataType]]]