Source code for sc3nb.sc_objects.synthdef

"""Module to for using SuperCollider SynthDefs and Synths in Python"""

import re
import sys
import warnings
from enum import Enum, unique
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

if sys.version_info < (3, 9):
    # `importlib.resources` backported to PY<37 as `importlib_resources`.
    import importlib_resources as libresources
else:
    # only PY>=39 `importlib.resources` offers .files.
    import importlib.resources as libresources

import sc3nb
import sc3nb.resources
from sc3nb.util import parse_pyvars, replace_vars

if TYPE_CHECKING:
    from sc3nb.sc import SC
    from sc3nb.sc_objects.server import SCServer
    from sc3nb.sclang import SCLang, SynthArgument


@unique
[docs] class SynthDefinitionCommand(str, Enum): """OSC Commands for Synth Definitions"""
[docs] RECV = "/d_recv"
[docs] LOAD = "/d_load"
[docs] LOAD_DIR = "/d_loadDir"
[docs] FREE = "/d_free"
[docs] class SynthDef: """Wrapper for SuperCollider SynthDef"""
[docs] synth_descs = {}
[docs] synth_defs = {}
@classmethod
[docs] def get_description( cls, name: str, lang: Optional["SCLang"] = None ) -> Optional[Dict[str, "SynthArgument"]]: """Get Synth description Parameters ---------- name : str name of SynthDef Returns ------- Dict dict with SynthArguments """ if name in cls.synth_descs: return cls.synth_descs[name] synth_desc = None if lang is None: try: lang = sc3nb.SC.get_default().lang except RuntimeError: warnings.warn(f"SynthDesc '{name}' is unknown. sclang is not running.") return synth_desc try: synth_desc = lang.get_synth_description(name) except ValueError: warnings.warn( f"SynthDesc '{name}' is unknown. sclang does not know this SynthDef" ) if synth_desc is not None: cls.synth_descs[name] = synth_desc return synth_desc
@classmethod
[docs] def send( cls, synthdef_bytes: bytes, server: Optional["SCServer"] = None, ): """Send a SynthDef as bytes. Parameters ---------- synthdef_bytes : bytes SynthDef bytes wait : bool If True wait for server reply. server : SCServer, optional Server instance that gets the SynthDefs, by default use the SC default server """ if server is None: server = sc3nb.SC.get_default().server server.msg( SynthDefinitionCommand.RECV, synthdef_bytes, await_reply=True, bundle=True, )
@classmethod
[docs] def load( cls, synthdef_path: str, server: Optional["SCServer"] = None, ): """Load SynthDef file at path. Parameters ---------- synthdef_path : str Path with the SynthDefs server : SCServer, optional Server that gets the SynthDefs, by default use the SC default server """ if server is None: server = sc3nb.SC.get_default().server server.msg( SynthDefinitionCommand.LOAD, synthdef_path, await_reply=True, bundle=True, )
@classmethod
[docs] def load_dir( cls, synthdef_dir: Optional[str] = None, completion_msg: Optional[bytes] = None, server: Optional["SCServer"] = None, ): """Load all SynthDefs from directory. Parameters ---------- synthdef_dir : str, optional directory with SynthDefs, by default sc3nb default SynthDefs completion_msg : bytes, optional Message to be executed by the server when loaded, by default None server : SCServer, optional Server that gets the SynthDefs, by default use the SC default server """ if server is None: server = sc3nb.SC.get_default().server def _load_synthdefs(path): cmd_args: List[Union[str, bytes]] = [path.as_posix()] if completion_msg is not None: cmd_args.append(completion_msg) server.msg( SynthDefinitionCommand.LOAD_DIR, cmd_args, await_reply=True, bundle=True, ) if synthdef_dir is None: ref = libresources.files(sc3nb.resources) / "synthdefs" with libresources.as_file(ref) as path: _load_synthdefs(path) else: path = Path(synthdef_dir) if path.exists() and path.is_dir(): _load_synthdefs(path) else: raise ValueError(f"Provided path {path} does not exist or is not a dir")
def __init__(self, name: str, definition: str, sc: Optional["SC"] = None) -> None: """Create a dynamic synth definition in sc. Parameters ---------- name : string default name of the synthdef creation. The naming convention will be name+int, where int is the amount of already created synths of this definition definition : string Pass the default synthdef definition here. Flexible content should be in double brackets ("...{{flexibleContent}}..."). This flexible content, you can dynamic replace with set_context() sc : SC object SC instance where the synthdef should be created, by default use the default SC instance """ self.sc = sc self.definition = definition self.name = name self.current_def = definition
[docs] def reset(self) -> "SynthDef": """Reset the current synthdef configuration to the self.definition value. After this you can restart your configuration with the same root definition Returns ------- object of type SynthDef the SynthDef object """ self.current_def = self.definition return self
[docs] def set_context(self, searchpattern: str, value) -> "SynthDef": """Set context in SynthDef. This method will replace a given key (format: "...{{key}}...") in the synthdef definition with the given value. Parameters ---------- searchpattern : string search pattern in the current_def string value : string or something with can parsed to string Replacement of search pattern Returns ------- self : object of type SynthDef the SynthDef object """ self.current_def = self.current_def.replace( "{{" + searchpattern + "}}", str(value) ) return self
[docs] def set_contexts(self, dictionary: Dict[str, Any]) -> "SynthDef": """Set multiple values at onces when you give a dictionary. Because dictionaries are unsorted, keep in mind, that the order is sometimes ignored in this method. Parameters ---------- dictionary : dict {searchpattern: replacement} Returns ------- self : object of type SynthDef the SynthDef object """ for searchpattern, replacement in dictionary.items(): self.set_context(searchpattern, replacement) return self
[docs] def unset_remaining(self) -> "SynthDef": """This method will remove all existing placeholders in the current def. You can use this at the end of definition to make sure, that your definition is clean. Hint: This method will not remove pyvars Returns ------- self : object of type SynthDef the SynthDef object """ self.current_def = re.sub(r"{{[^}]+}}", "", self.current_def) return self
[docs] def add( self, pyvars=None, name: Optional[str] = None, server: Optional["SCServer"] = None, ) -> str: """This method will add the current_def to SuperCollider.s If a synth with the same definition was already in sc, this method will only return the name. Parameters ---------- pyvars : dict SC pyvars dict, to inject python variables name : str, optional name which this SynthDef will get server : SCServer, optional Server where this SynthDef will be send to, by default use the SC default server Returns ------- str Name of the SynthDef """ if name is not None: self.name = name if self.name in SynthDef.synth_descs: del SynthDef.synth_descs[self.name] if pyvars is None: pyvars = sc3nb.sclang.parse_pyvars(self.current_def) # TODO should check if there is context/pyvars that can't be set # Create new SynthDef add it to SynthDescLib and get bytes if self.sc is None: self.sc = sc3nb.SC.get_default() synth_def_blob, output = self.sc.lang.cmd( f""" "sc3nb - Creating SynthDef {self.name}".postln; r.tmpSynthDef = SynthDef("{self.name}", {self.current_def}); SynthDescLib.global.add(r.tmpSynthDef.asSynthDesc); r.tmpSynthDef.asBytes();""", pyvars=pyvars, verbose=False, get_result=True, get_output=True, ) if synth_def_blob == 0: print(output) raise RuntimeError(f"Adding SynthDef failed. - {output}") else: SynthDef.synth_defs[self.name] = synth_def_blob if server is not None: server.send_synthdef(synth_def_blob) else: self.sc.server.send_synthdef(synth_def_blob) return self.name
[docs] def free(self) -> "SynthDef": """Free this SynthDef from the server. Returns ------- self : object of type SynthDef the SynthDef object """ if self.sc is None: self.sc = sc3nb.SC.get_default() self.sc.server.msg(SynthDefinitionCommand.FREE, [self.name], bundle=True) return self
[docs] def __repr__(self): try: pyvars = parse_pyvars(self.current_def) except NameError: current_def = self.current_def else: current_def = replace_vars(self.current_def, pyvars) return f"SynthDef('{self.name}', {current_def})"