Source code for csoundengine.synth

from __future__ import annotations
import time
from abc import abstractmethod, ABC
from functools import cache
import numpy as np

import emlib.iterlib as _iterlib
import emlib.misc as _misc

from . import internal
from . import jupytertools
from .config import logger, config
from .baseschedevent import BaseSchedEvent
from .schedevent import SchedEvent
from ._common import EMPTYSET, EMPTYDICT

from typing import TYPE_CHECKING, Sequence
if TYPE_CHECKING:
    from .engine import Engine
    from .instr import Instr
    from .session import Session


__all__ = (
    'Synth',
    'SynthGroup',
    'ui'
)


class ISynth(ABC):

    """
    Minimal interface defining a Synth

    This is not used inside csoundengine at the moment
    but is used in downstream projects like maelzel
    """

    @abstractmethod
    def playing(self) -> bool:
        """ Is this synth playing? """
        raise NotImplementedError

    @abstractmethod
    def finished(self) -> bool:
        """ Has this synth ceased to play? """
        raise NotImplementedError

    def wait(self, pollinterval: float = 0.02, sleepfunc=time.sleep) -> None:
        """
        Wait until this synth has stopped

        Args:
            pollinterval: polling interval in seconds
            sleepfunc: the function to call when sleeping, defaults to time.sleep
        """
        internal.setSigintHandler()
        while self.playing():
            sleepfunc(pollinterval)
        internal.removeSigintHandler()

    @abstractmethod
    def ui(self, **specs: tuple[float, float]) -> None:
        """
        Modify dynamic (named) arguments through an interactive user-interface

        If run inside a jupyter notebook, this method will create embedded widgets
        to control the values of the dynamic pfields of an event. Dynamic pfields
        are those assigned to a k-variable or declared as ``|karg|`` (see below)

        Args:
            specs: map named arg to a tuple (minvalue, maxvalue), the keyword
                is the name of the parameter, the value is a tuple with the
                range

        Example
        ~~~~~~~

        .. code::

            # Inside jupyter
            from csoundengine import *
            s = Engine().session()
            s.defInstr('vco', r'''
              |kmidinote, kampdb=-12, kcutoff=3000, kres=0.9|
              kfreq = mtof:k(kmidinote)
              asig = vco2:a(ampdb(kampdb), kfreq)
              asig = moogladder2(asig, kcutoff, kres)
              asig *= linsegr:a(0, 0.1, 1, 0.1, 0)
              outs asig, asig
            ''')
            synth = s.sched('vco', kmidinote=67)
            # Specify the ranges for some sliders. All named parameters
            # are assigned a widget
            synth.ui(kampdb=(-48, 0), kres=(0, 1))

        .. figure:: assets/synthui.png

        .. seealso::

            - :meth:`Engine.eventui`

        """
        return ui(self, specs=specs)


def ui(event, specs: dict[str, tuple[float, float]]):
    from . import interact
    dynparams = event.dynamicParamNames(aliases=True, aliased=False)
    if not dynparams:
        logger.error(f"No named parameters for {event}")
        return
    params = {param: event.paramValue(param) for param in sorted(dynparams)}
    paramspecs = interact.guessParamSpecs(params, ranges=specs)
    return interact.interactSynth(event, specs=paramspecs)


_synthStatusIcon = {
    'playing': '▶',
    'stopped': '■',
    'future': '𝍪'
}


[docs] class Synth(SchedEvent, ISynth): """ A Synth represents a realtime csound event A user never creates a Synth directly, it is created by a Session when :meth:`Session.sched <csoundengine.session.Session.sched>` is called Args: session: the Session this synth belongs to p1: the p1 assigned instr: the Instr of this synth start: start time (absolute) dur: duration of the synth (-1 if no end) args: the pfields used to create the actual event, starting with p5 (p4 is reserved for the controlsSlot autostop: if True, the underlying csound event is stopped when this object is deallocated priority: the priority at which this event was scheduled controls: the dynamic controls used to schedule this synth controlsSlot: the control slot assigned to this synth, if the instrument defines named controls uniqueId: an integer identifying this synth, if applicable Example ~~~~~~~ .. code:: from csoundengine import * from pitchtools import n2m session = Engine().session() session.defInstr('vco', r''' |kamp=0.1, kmidi=60, ktransp=0| asig vco2 kamp, mtof:k(kmidi+ktransp) asig *= linsegr:a(0, 0.1, 1, 0.1, 0) outch 1, asig ''') notes = ['4C', '4E', '4G'] synths = [session.sched('vco', kamp=0.2, kmidi=n2m(n)) for n in notes] # synths is a list of Synth # automate ktransp in synth 1 to produce 10 second gliss of 1 semitone downwards synths[1].automate('ktransp', [0, 0, 10, -1]) """ __slots__ = ('session', 'autostop', '_scheduled') def __init__(self, session: Session, p1: float, instr: Instr, start: float, dur: float = -1, args: list[float|str] | None = None, autostop=False, priority: int = 1, controls: dict[str, float] | None = None, controlsSlot: int = -1, uniqueId=0, name='' ) -> None: SchedEvent.__init__(self, instrname=instr.name, start=start, dur=dur, args=args, p1=p1, uniqueId=uniqueId, parent=session, priority=priority, controlsSlot=controlsSlot, controls=controls, username=name) # AbstrSynth.__init__(self, start=start, dur=dur, session=session, autostop=autostop) if controlsSlot < 0 and instr.dynamicParams(): raise ValueError("Synth has dynamic args but was not assigned a control slot") elif controlsSlot >= 1 and not instr.dynamicParams(): logger.warning("A control slot was assigned but this synth does not have any controls") self._scheduled: bool = True self.p1: float = p1 """Event id for this synth""" self.session = session """The Session to which this Synth belongs""" self.autostop = autostop """If True, stop the underlying csound event when this object is freed""" if name and autostop: logger.warning(f"Autostop is disabled for named synths") def __del__(self): if self.autostop: self.stop()
[docs] def wait(self, pollinterval: float = 0.02, sleepfunc=time.sleep) -> None: """ Wait until this synth has stopped Args: pollinterval: polling interval in seconds sleepfunc: the function to call when sleeping, defaults to time.sleep """ internal.waitWhileTrue(self.playing, pollinterval=pollinterval, sleepfunc=sleepfunc)
[docs] def aliases(self) -> dict[str, str]: """The parameter aliases of this synth, or an empty dict if no aliases defined""" return self.instr.aliases
@property def body(self) -> str: return self.session.generateInstrBody(self.instr) #def controlNames(self, aliases=True, aliased=False) -> frozenset[str]: # return self.instr.controlNames(aliases=aliases, aliased=aliased)
[docs] def ui(self, **specs: tuple[float, float]) -> None: """ Modify dynamic (named) arguments through an interactive user-interface If run inside a jupyter notebook, this method will create embedded widgets to control the values of the dynamic pfields of an event. Dynamic pfields are those assigned to a k-variable or declared as ``|karg|`` (see below) Args: specs: map named arg to a tuple (minvalue, maxvalue), the keyword is the name of the parameter, the value is a tuple with the range Example ~~~~~~~ .. code:: # Inside jupyter from csoundengine import * s = Engine().session() s.defInstr('vco', r''' |kmidinote, kampdb=-12, kcutoff=3000, kres=0.9| kfreq = mtof:k(kmidinote) asig = vco2:a(ampdb(kampdb), kfreq) asig = moogladder2(asig, kcutoff, kres) asig *= linsegr:a(0, 0.1, 1, 0.1, 0) outs asig, asig ''') synth = s.sched('vco', kmidinote=67) # Specify the ranges for some sliders. All named parameters # are assigned a widget synth.ui(kampdb=(-48, 0), kres=(0, 1)) .. figure:: assets/synthui.png .. seealso:: - :meth:`Engine.eventui` """ return ui(event=self, specs=specs)
def _html(self, playstatus: str = '') -> str: argsfontsize = config['html_args_fontsize'] maxi = config['synth_repr_max_args'] style = jupytertools.defaultPalette if not playstatus: playstatus = self.playStatus() playstr = _synthStatusIcon[playstatus] parts = [ f'{playstr} <strong style="color:{style["name.color"]}">' f'{self.instr.name}</strong>:{self.p1:.4f}', ] if self.args and len(self.args) > 1: i2n = self.instr.pfieldIndexToName argsstrs = [] pargs = self.args if any(arg.startswith('k') for arg in self.instr.pfieldNameToIndex): maxi = max(i+4 for i, n in i2n.items() if n.startswith('k')) for i, parg in enumerate(pargs, start=5): if i > maxi: argsstrs.append("…") break name = i2n.get(i) if not isinstance(parg, str): parg = f'{parg:.6g}' if name: idxstr = str(i) if self.instr.aliases and (alias := self.instr.aliases.get(name)): s = f"{idxstr}:<b>{alias}({name})</b>=<code>{parg}</code>" else: s = f"{idxstr}:<b>{name}</b>=<code>{parg}</code>" else: s = f"<b>{i}</b>=<code>{parg}</code>" argsstrs.append(s) argsstr = " ".join(argsstrs) argsstr = fr'<span style="font-size:{argsfontsize};">{argsstr}</span>' parts.append(argsstr) # return '<span style="font-size:12px;">∿(' + ', '.join(parts) + ')</span>' return '<span style="font-size:12px;">Synth('+', '.join(parts)+')</span>' def _repr_html_(self) -> str: status = self.playStatus() if status != 'stopped' and jupytertools.inside_jupyter(): if config['jupyter_synth_repr_stopbutton']: jupytertools.displayButton("Stop", self.stop) return f"<p>{self._html(playstatus=status)}</p>" def __repr__(self): playstr = _synthStatusIcon[self.playStatus()] parts = [f'{playstr} {self.instr.name}={self.p1} start={self.start:.3f} dur={self.dur:.3f}'] if self.instr.hasControls(): parts.append(f'slot={self.controlsSlot}') ctrlparts = [] for k, v in self.instr.controls.items(): if self.controls is not None and k in self.controls: v = self.controls[k] ctrlparts.append(f'{k}={v}') parts.append(f"|{' '.join(ctrlparts)}|") if self.args: showpidx = config['synth_repr_show_pfield_index'] maxi = config['synth_repr_max_args'] i2n = self.instr.pfieldIndexToName maxi = max((i for i, name in i2n.items() if name.startswith("k")), default=maxi) argsstrs = [] pargs = self.args[0:] for i, parg in enumerate(pargs): if i > maxi: argsstrs.append("…") break name = i2n.get(i+4) if not isinstance(parg, str): parg = f'{parg:.6g}' if name: if showpidx: s = f"{name}:{i+4}={parg}" else: s = f"{name}={parg}" else: # pargs start at 5 s = f"p{i+5}={parg}" argsstrs.append(s) argsstr = " ".join(argsstrs) parts.append(argsstr) lines = ["Synth(" + " ".join(parts) + ")"] # add a line for k- pargs return "\n".join(lines)
[docs] def playStatus(self) -> str: """ Returns the playing status of this synth (playing, stopped or future) Returns: 'playing' if currently playing, 'stopped' if this synth has already stopped or 'future' if it has not started """ if not self._scheduled or self.p1 not in self.session._synths: return "stopped" now = self.session.engine.realElapsedTime() return "playing" if now >= self.start else "future"
[docs] def playing(self) -> bool: """ Is this Synth playing """ return self.playStatus() == "playing"
[docs] def finished(self) -> bool: return self.playStatus() == 'stopped'
#def pfieldNames(self, aliases=True, aliased=False) -> frozenset[str]: # return self.instr.pfieldNames(aliases=aliases, aliased=aliased) def _sliceStart(self) -> int: return self.controlsSlot * self.session.maxDynamicArgs def _setPfield(self, param: str, value: float, delay=0.) -> None: """ Modify a pfield of this synth. This makes only sense if the pfield is assigned to a krate variable. A pfield can be referred as 'p4', 'p5', etc., or to the name of the assigned k-rate variable as a string (for example, if there is a line "kfreq = p6", both 'p6' and 'kfreq' refer to the same pfield). If the parameter name does not fit any known parameter a KeyError exception is raised Example ~~~~~~~ >>> session = Engine(...).session() >>> session.defInstr("sine", ''' kamp = p5 kfreq = p6 outch 1, oscili:ar(kamp, kfreq) ''' ) >>> synth = session.sched('sine', args=dict(p5=0.1, p6=440)) >>> synth.set(kfreq=880) >>> synth.set(p5=0.1, p6=1000) >>> synth.set(kamp=0.2, p6=440) .. seealso:: - :meth:`AbstrSynth.set` - :meth:`Synth.automate` - :meth:`Synth.getp` - :meth:`Synth.automate` """ if self.playStatus() == 'stopped': logger.error(f"Synth {self} has already stopped, cannot " f"set param '{param}'") return self.session._setPfield(event=self, delay=delay, param=param, value=value) def _setTable(self, param: str, value: float, delay=0.) -> None: if self.playStatus() == 'stopped': logger.error(f"Synth {self} has already stopped, cannot " f"set param '{param}'") else: self.session._setNamedControl(event=self, param=param, value=value, delay=delay)
[docs] def paramValue(self, param: str | int) -> float | str | None: """ Get the value of a parameter Args: param: the parameter name or a pfield index Returns: the value, or None if the parameter has no value """ if isinstance(param, int): paramidx = param - 4 return self.args[paramidx] if self.args and 0 <= paramidx < len(self.args) else None elif isinstance(param, str): param = self.unaliasParam(param, param) if (paramidx := self.instr.pfieldIndex(param, -1)) >= 0: paramidx0 = paramidx - 5 if self.args and paramidx0 < len(self.args): return self.args[paramidx0] else: return None elif param in self.instr.controls: if self.playing(): return self.session._getNamedControl(slicenum=self.controlsSlot, paramslot=self.instr.controlIndex(param)) else: assert self.controls is not None value = self.controls.get(param) if value is not None: return value return self.instr.controls.get(param) return None else: raise TypeError(f"Expected an integer index or a parameter name, got {param}")
[docs] def relativeStart(self) -> float: """ The relative start time of this Synth The .start attribute of the synth carries the absolute timestamp (since the start of the engine) at which this Synth was scheduled. The relative start time is an offset from the current elapsed time. **If this synth has already started then the returned value will be negative** Returns: the relative start time of the synth """ return self.start - self.session.engine.elapsedTime()
[docs] def automate(self, param: str, pairs: Sequence[float] | np.ndarray | tuple[np.ndarray, np.ndarray], mode='linear', delay: float | None = 0., overtake=False, ) -> float: """ Automate any named parameter of this Synth Raises KeyError if the parameter is unknown Args: param: the name of the parameter to automate pairs: automation data as a flat array with the form [time0, value0, time1, value1, ...] or a tuple of the form (times, values) mode: one of 'linear', 'cos'. Determines the curve between values delay: when to start the automation, relative to the current time. If None is given, the delay is set to the start of this synth. To set an absolute start time, use ``abstime - engine.elapsedTime()`` as delay overtake: if True, do not use the first value in pairs but overtake the current value Returns: the eventid of the automation event. """ return self.session.automate(event=self, param=param, pairs=pairs, mode=mode, delay=delay, overtake=overtake)
[docs] def stop(self, delay=0.) -> None: self.session.unsched(self.p1, delay=delay)
def _synthsCreateHtmlTable(synths: list[Synth], maxrows: int | None = None, tablestyle='', ) -> str: synth0 = synths[0] instr0 = synth0.instr if any(synth.instr.name != instr0.name for synth in synths): # multiple instrs per group, not allowed here raise ValueError("Only synths of the same instr allowed here") colnames = ["p1", "start", "dur", "p4"] if maxrows is None: maxrows = config['synthgroup_repr_max_rows'] if maxrows and len(synths) > maxrows: limitSynths = True synths = synths[:maxrows] else: limitSynths = False rows: list[list[str]] = [[] for _ in synths] now = synth0.session.engine.elapsedTime() for row, synth in zip(rows, synths): row.append(f'{synth.p1} <b>{_synthStatusIcon[synth.playStatus()]}</b>') row.append("%.3f" % (synth.start - now)) row.append("%.3f" % synth.dur) row.append(str(synth.controlsSlot)) if keys := synth0.controlNames(): colnames.extend(keys) for row, synth in zip(rows, synths): if synth.playStatus() != 'stopped': values = [synth.paramValue(param) for param in keys] for value in values: row.append(f'<code>{value}</code>') else: row.extend(["-"] * len(keys)) if synth0.args: maxi = config['synth_repr_max_args'] i2n = instr0.pfieldIndexToName maxi = max((i for i, name in i2n.items() if name.startswith("k")), default=maxi) for i, parg in enumerate(synth0.args): if i > maxi: colnames.append("...") break pidx = i + 5 name = i2n.get(pidx) if config['synth_repr_show_pfield_index']: colname = f"{pidx}:{name}" if name else str(pidx) else: colname = name if name else str(pidx) colnames.append(colname) for row, synth in zip(rows, synths): if synth.args: row.extend(f"{parg:.5g}" if not isinstance(parg, str) else parg for parg in synth.args[:maxi]) if len(synth.args) > maxi: row.append("...") if limitSynths: rows.append(["..."]) return _misc.html_table(rows, headers=colnames, tablestyle=tablestyle)
[docs] class SynthGroup(BaseSchedEvent): """ A SynthGroup is used to control multiple synths Such multiple synths can be groups of similar synths, as in additive synthesis, or processing chains which work as an unity. Attributes: synths (list[AbstrSynth]): the list of synths in this group Example ~~~~~~~ >>> import csoundengine as ce >>> session = ce.Engine().session() >>> session.defInstr('oscil', r''' ... |kfreq, kamp=0.1, kcutoffratio=5, kresonance=0.9| ... a0 = vco2(kamp, kfreq) ... a0 = moogladder2(a0, kfreq * kcutoffratio, kresonance) ... outch 1, a0 ... ''') >>> synths = [session.sched('oscil', kfreq=freq) ... for freq in range(200, 1000, 75)] >>> group = ce.synth.SynthGroup(synths) >>> group.set(kcutoffratio=3, delay=3) >>> group.automate('kresonance', (1, 0.3, 10, 0.99)) >>> group.stop(delay=11) """ __slots__ = ('synths', 'session', 'autostop', '__weakref__') def __init__(self, synths: list[Synth], autostop=False) -> None: if not synths: start = 0. end = 0. dur = 0. else: start = min(synth.start for synth in synths) end = max(synth.end for synth in synths) dur = end - start BaseSchedEvent.__init__(self, start=start, dur=dur) flatsynths: list[Synth] = [] for synth in synths: if isinstance(synth, SynthGroup): flatsynths.extend(synth) else: flatsynths.append(synth) self.synths: list[Synth] = flatsynths self.autostop = autostop self.session = self.synths[0].session if synths else None def __del__(self): if self.autostop: for synth in self: if synth.playStatus() != 'stopped': synth.stop()
[docs] def extend(self, synths: list[Synth]) -> None: """ Add the given synths to the synths in this group """ self.synths.extend(synths)
[docs] def stop(self, delay=0.) -> None: for s in self.synths: s.stop(delay=delay)
def playing(self) -> bool: return any(s.playing() for s in self) def finished(self) -> bool: return all(s.finished() for s in self) def _automateTable(self, param: str, pairs, mode="linear", delay=0., overtake=False) -> list[float]: synthids = [] for synth in self.synths: if isinstance(synth, Synth): controls = synth.dynamicParamNames() if controls and param in controls: synthid = synth._automateTable(param, pairs, mode=mode, delay=delay, overtake=overtake) synthids.append(synthid) elif isinstance(synth, SynthGroup): controls = synth.dynamicParamNames() if controls and param in controls: synthid = synth._automateTable(param=param, pairs=pairs, mode=mode, delay=delay, overtake=overtake) synthids.append(synthid) if not synthids: raise KeyError(f"Parameter '{param}' not known. " f"Possible parameters: {self.dynamicParamNames()}") return synthids
[docs] @cache def dynamicParamNames(self, aliases=True, aliased=False) -> set[str]: out: set[str] = set() for synth in self: dynamicParams = synth.dynamicParamNames(aliases=aliases, aliased=aliased) out.update(dynamicParams) return out
@cache def aliases(self) -> dict[str, str]: out = {} for synth in self: if synth.instr.aliases: out.update(synth.instr.aliases) return out
[docs] @cache def pfieldNames(self, aliases=True, aliased=False) -> frozenset[str]: out: set[str] = set() for synth in self: namedPargs = synth.pfieldNames(aliases=aliases, aliased=aliased) if namedPargs: out.update(namedPargs) return frozenset(out)
[docs] def automate(self, param: int | str, pairs: Sequence[float] | np.ndarray | tuple[np.ndarray, np.ndarray], mode="linear", delay=0., overtake=False, ) -> list[float]: """ Automate the given parameter for all the synths in this group If the parameter is not found in a given synth, the automation is skipped for the given synth. This is useful when a group includes synths using different instruments so an automation would only adress those synths which support a given parameter. Synths which have no time overlap with the automation are also skipped. Raises KeyError if the param used is not supported by any synth in this group Supported parameters can be checked via :meth:`SynthGroup.dynamicParams` Args: param: the parameter to automate pairs: a flat list of pairs (time0, value0, time1, value1, ...) mode: the iterpolation mode delay: the delay to start the automation overtake: if True, the first value is dropped and instead the current value of the given parameter is used. The same effect is achieved if the first value is given as 'nan', in this case also the current value of the synth is overtaken. Returns: a list of synthids. There will be one synthid per synth in this group. A synthid of 0 indicates that for that given synth no automation was scheduled, either because that synth does not support the given param or because the automation times have no intersection with the synth """ synthids = [] for synth in self: if param in synth.instr.dynamicParams(): synthid = synth.automate(param=param, pairs=pairs, mode=mode, delay=delay, overtake=overtake) else: synthid = 0 synthids.append(synthid) if all(synthid == 0 for synthid in synthids): raise KeyError(f"Parameter '{param}' not known. Possible parameters: " f"{self.dynamicParamNames(aliases=True, aliased=True)}") return synthids
def _automatePfield(self, param: int | str, pairs: list[float] | np.ndarray, mode="linear", delay=0., overtake=False) -> list[float]: eventids = [synth._automatePfield(param, pairs, mode=mode, delay=delay, overtake=overtake) for synth in self if param in synth.instr.dynamicPfieldNames()] if not eventids: raise ValueError(f"Parameter '{param}' unknown for group, possible " f"parameters: {self.dynamicParamNames()}") return eventids def _htmlTable(self, style='', maxrows: int | None = None) -> str: subgroups = _iterlib.classify(self.synths, lambda synth: synth.instr.name) lines = [] instrcol = jupytertools.defaultPalette["name.color"] for instrname, synths in subgroups.items(): lines.append(f'<p><small>Instr: <strong style="color:{instrcol}">' f'{instrname}' f'</strong> - <b>{len(synths)}</b> synths</small></p>') htmltable = _synthsCreateHtmlTable(synths, maxrows=maxrows, tablestyle=style) lines.append(htmltable) out = '\n'.join(lines) return out
[docs] def ui(self, **specs: tuple[float, float]) -> None: """ Modify dynamic (named) arguments through an interactive user-interface If run inside a jupyter notebook, this method will create embedded widgets to control the values of the dynamic pfields of an event. Dynamic pfields are those assigned to a k-variable or declared as ``|karg|`` (see below) Args: specs: map named arg to a tuple (minvalue, maxvalue), the keyword is the name of the parameter, the value is a tuple with the range Example ~~~~~~~ .. code:: # Inside jupyter from csoundengine import * s = Engine().session() s.defInstr('vco', r''' |kmidinote, kampdb=-12, kcutoff=3000, kres=0.9| kfreq = mtof:k(kmidinote) asig = vco2:a(ampdb(kampdb), kfreq) asig = moogladder2(asig, kcutoff, kres) asig *= linsegr:a(0, 0.1, 1, 0.1, 0) outs asig, asig ''') synths = [ s.sched('vco', kmidinote=67) s.sched('vco', kmidinote=69) ] group = SynthGroup(synths) # Specify the ranges for some sliders. All named parameters # are assigned a widget group.ui(kampdb=(-48, 0), kres=(0, 1)) .. seealso:: - :meth:`Engine.eventui` """ ui(event=self, specs=specs)
def _repr_html_(self) -> str: assert jupytertools.inside_jupyter() bold = lambda txt: span(txt, bold=True) span = jupytertools.htmlSpan if not self.synths: return f'{bold("SynthGroup")}(synths=[])' if config['jupyter_synth_repr_stopbutton']: jupytertools.displayButton("Stop", self.stop) now = self[0].session.engine.elapsedTime() start = min(max(0., s.start - now) for s in self) end = max(s.dur + s.start - now for s in self) if any(s.dur < 0 for s in self): end = float('inf') dur = end - start header = f'{bold("SynthGroup")}(synths={span(len(self), tag="code")})' lines = [f'<small>{header}</small>'] numrows = config['synthgroup_repr_max_rows'] style = config['synthgroup_html_table_style'] lines.append(self._htmlTable(style=style, maxrows=numrows)) return "\n".join(lines) def __repr__(self) -> str: lines = [f"SynthGroup(n={len(self.synths)})"] for synth in self: lines.append(" "+repr(synth)) return "\n".join(lines) def __len__(self) -> int: return len(self.synths) def __getitem__(self, idx) -> Synth: return self.synths.__getitem__(idx) def __iter__(self): return iter(self.synths) def _setTable(self, param: str, value: float, delay=0) -> None: count = 0 for synth in self: if param in synth.dynamicParamNames(aliases=True, aliased=True): synth._setTable(param=param, value=value, delay=delay) count += 1 if count == 0: params = list(self.dynamicParamNames(aliases=False)) if aliases := self.aliases(): params.extend(f'{alias}>{orig}' for alias, orig in aliases.items()) raise KeyError(f"Parameter '{param}' unknown. " f"Possible parameters: {params}")
[docs] def paramValue(self, param: str) -> float | str | None: """ Returns the parameter value for the given parameter Within a group the first synth which has the given parameter will be used to determine the parameter value """ if param not in self.paramNames(): raise KeyError(f"Parameter '{param}' not known. Possible parameters: " f"{self.paramNames()}") for synth in self: value = synth.paramValue(param) if value is not None: return value return None
def _setPfield(self, param: str, value: float, delay=0.) -> None: count = 0 for synth in self: if param in synth.instr.pfieldNames(aliases=False): synth._setPfield(param=param, value=value, delay=delay) count += 1 if count == 0: raise KeyError(f"Parameter {param} unknown. " f"Possible parameters: {self.dynamicParamNames(aliased=True)}")
[docs] def controlNames(self, aliases=True, aliased=False) -> set[str]: """ Returns a set of available table named parameters for this group """ allparams = set() for synth in self: params = synth.controlNames(aliases=aliases, aliased=aliased) if params: allparams.update(params) return allparams
[docs] def wait(self, pollinterval: float = 0.02, sleepfunc=time.sleep) -> None: """ Wait until this synth has stopped Args: pollinterval: polling interval in seconds sleepfunc: the function to call when sleeping, defaults to time.sleep """ internal.waitWhileTrue(self.playing, pollinterval=pollinterval, sleepfunc=sleepfunc)