Source code for FoxDot.lib.Effects.Util

"""
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