"""
Filter Effects
--------------
Effects are added to Player objects as keywords instructions like `dur`
or `amp` but are a little more tricky. Each effect has a "title" keyword,
which requires a nonzero value to add the effect to a Player. Effects
also have other attribute keywords which can be any value and may have
a default value which is set when a Player is created.
::
# Example. Reverb effect "title" is `room` and attribute is `mix`, which
# defaults to 0.25. The following adds a reverb effect
p1 >> pads(room=0.5)
# This still adds the effect, but a mix of 0 doesn't actually do anything
p1 >> pads(room=0.5, mix=0)
# This effect is not added as the "title" keyword, room, is 0
p1 >> pads(room=0, mix=0.5)
Other effects are outlined below:
*High Pass Filter* - Title keyword: `hpf`, Attribute keyword(s): `hpr`
Only frequences **above** the value of `hpf` are kept in the final signal. Use `hpr` to set the resonance (usually a value between 0 and 1)
*Low Pass Filter* - Title keyword: `lpf`, Attribute keyword(s): `lpr`
Only frequences **below** the value of `lpf` are kept in final signal. Use `lpr` to set the resonance (usually a value between 0 and 1)
*Bitcrush* - Title keyword: `bits`, Attribute keyword(s): `crush`
The bit depth, in number of `bits`, that the signal is reduced to; this is a value between 1 and 24 where other values are ignored. Use `crush` to set the amount of reduction to the bitrate (defaults to 8)
*Reverb* - Title keyword: `room`, Attribute keyword(s): `mix`
The `room` argument specifies the size of the room and `mix` is the dry/wet mix of reverb; this should be a value between 0 and 1 (defalts to 0.25)
*Chop* - Title keyword: `chop`, Attribute keyword(s): `sus`
'Chops' the signal into chunks using a low frequency pulse wave over the sustain of a note.
*Slide To* - Title keyword: `slide`, Attribute keyword(s):
Slides' the frequency value of a signal to `freq * (slide+1)` over the duration of a note (defaults to 0)
*Slide From* - Title keyword: `slidefrom`, Attribute keyword(s):
Slides' the frequency value of a signal from `freq * (slidefrom)` over the duration of a note (defaults to 1)
*Comb delay (echo)* - Title keyword: `echo`, Attribute keyword(s): `decay`
Sets the decay time for any echo effect in beats, works best on Sample Player (defaults to 0)
*Panning* - Title keyword: `pan`, Attribute keyword(s):
Panning, where -1 is far left, 1 is far right (defaults to 0)
*Vibrato* - Title keyword: `vib`, Attribute keyword(s):
Vibrato (defaults to 0)
Undocumented: Spin, Shape, Formant, BandPassFilter, Echo
"""
from enum import Enum
from pathlib import Path
from collections import defaultdict
from typing import Callable
from ..ServerManager import Server
from ..Settings import EFFECTS_DIR, SC3_PLUGINS
from ..SCLang._Generator import generate_effect
from ..Utils import deprecation
from ..Translation import _
[docs]
class Order(int, Enum):
IGNORE = -1
CONTROL = 0
PRE_ENV = 1
POST_ENV = 2
class _Effect:
server = Server
@classmethod
def set_server(cls, server):
cls.server = server
def __init__(self, foxdot_name, synthdef, args=None, control=False):
self.name = foxdot_name
self.synthdef = synthdef
self.filename = EFFECTS_DIR + f'/{self.synthdef}.scd'
self.args = args.keys()
self.vars = ['osc']
self.defaults = args or {}
self.effects = []
self.control = control
self.suffix = 'kr' if self.control else 'ar'
self.channels = 1 if self.control else 2
self.input = f'osc = In.{self.suffix}(bus, {self.channels});\n'
self.output = f'ReplaceOut.{self.suffix}'
def __repr__(self):
other_args = ['{}'.format(arg) for arg in self.args if arg != self.name]
other_args = ", other args={}".format(other_args) if other_args else ""
return "<'{}': keyword='{}'{}>".format(self.synthdef, self.name, other_args)
def __str__(self):
s = "SynthDef.new(\\{},\n".format(self.synthdef)
s += "{" + "|bus, {}|\n".format(", ".join(self.args))
s += "var {};\n".format(",".join(self.vars))
s += self.input
s += self.list_effects()
s += self.output
s += "(bus, osc)}).add;"
return s
def add(self, string):
self.effects.append(string)
def doc(self, string):
"""Set a docstring for the effects."""
def list_effects(self):
return ''.join(f'{p};\n' for p in self.effects)
def add_var(self, name):
if name not in self.vars:
self.vars.append(name)
def save(self):
"""Writes to file and sends to server."""
# 1. See if the file exists
path = Path(self.filename)
contents = path.read_text() if path.is_file() else ''
# 2. If it does, check contents
this_string = str(self)
if contents != this_string:
path.write_text(this_string)
def load(self):
"""Loadd effect to SC."""
self.server.loadSynthDef(self.filename)
[docs]
class Fx(_Effect):
def __init__(self, func: Callable[..., None]):
self.func = func
self._generator = generate_effect(self.func)
self._code = self._generator.code()
self.args = []
self.defaults = self._generator.defaults
for arg in self._generator.args:
arg, *default = arg.split('=')
if arg in ['bus']:
continue
self.args.append(arg)
self.name = self.args[0]
self.synthdef = self._generator.name
self.filename = EFFECTS_DIR + f'/{self.synthdef}.scd'
self.save()
self.load()
def __str__(self):
return self._code
[docs]
class EffectManager(dict[str, _Effect]):
server = Server
IGNORE = Order.IGNORE
CONTROL = Order.CONTROL
PRE_ENV = Order.PRE_ENV
POST_ENV = Order.POST_ENV
[docs]
@classmethod
def set_server(cls, server):
cls.server = server
def __init__(self):
super().__init__()
self.kw: list[str] = []
self.all_kw: list[str] = []
self.defaults: defaultdict[str, int] = defaultdict(lambda: 0)
self.order: defaultdict[int, list[str]] = defaultdict(list)
self.server.setFx(self)
def __call__(
self, order: Order | int = Order.IGNORE
) -> Callable[[Callable[..., None]], Fx]:
"""Adding a new effect."""
if not isinstance(order, int):
raise TypeError(
_(
f'order={order} is an invalid type, use the Order enum or numbers.'
).format(order=order)
)
if order not in (orders := list(Order)):
raise ValueError(
_('"order" is not in the expected objects: {orders}').format(
orders=orders
)
)
def decorator(func):
fx = Fx(func)
name = fx.name
self[name] = fx
self.order[order].append(name)
self.kw.append(name)
for arg in fx.args:
if arg not in self.all_kw:
self.all_kw.append(arg)
self.defaults |= fx.defaults
self.server.fx_names[name] = fx.synthdef
return fx
return decorator
[docs]
def new(self, name, synthdef, args, order=2):
deprecation(_('new() is deprecated. Use @Effect(order=2) instead'))
fx = _Effect(name, synthdef, args, order == 0)
self.order[order].append(name)
# Store other sub-keys
for arg in args:
if arg not in self.all_kw:
self.all_kw.append(arg)
# Store the main keywords together
self.kw.append(name)
# Store the default value
self.defaults |= args
self[name] = fx
self.server.fx_names[name] = fx.synthdef
return fx
[docs]
def values(self):
return [self[key] for key in self.sort_by("synthdef")]
[docs]
def sort_by(self, attr: str):
"""Returns the keys sorted by attribute name."""
return sorted(
self.keys(), key=lambda effect: getattr(self[effect], attr)
)
[docs]
def kwargs(self):
"""Returns the title keywords for each effect."""
return tuple(self.kw)
[docs]
def all_kwargs(self):
"""Returns *all* keywords for all effects."""
return tuple(self.all_kw)
[docs]
def reload(self):
"""Re-sends each effect to SC."""
for effect in self.values():
effect.load()
Effect = EffectManager()
# deprecated
Effects = FxList = Effect # for compatibility