"""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]
LOAD_DIR = "/d_loadDir"
[docs]
class SynthDef:
"""Wrapper for SuperCollider SynthDef"""
@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})"