from __future__ import annotations
import time
from abc import ABC, abstractmethod
from functools import cache
from emlib.envir import inside_jupyter
from . import internal
from .baseschedevent import BaseSchedEvent
from .config import config, logger
from .schedevent import SchedEvent
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence, Mapping, Callable
from .instr import Instr
from .session import Session
from . import interact
import numpy as np
__all__ = (
'Synth',
'SynthGroup',
'ISynth'
)
[docs]
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
"""
[docs]
@abstractmethod
def playing(self) -> bool:
""" Is this synth playing? """
raise NotImplementedError
[docs]
@abstractmethod
def finished(self) -> bool:
""" Has this synth ceased to play? """
raise NotImplementedError
[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.setSigintHandler()
while self.playing():
sleepfunc(pollinterval)
internal.removeSigintHandler()
@abstractmethod
def stop(self, delay=0.) -> None:
raise NotImplementedError
[docs]
@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`
"""
raise NotImplementedError
_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', '_setCallback')
def __init__(self,
session: Session,
p1: float,
start: float,
dur: float = -1,
instr: Instr | None = None,
args: Sequence[float|str] = (),
autostop=False,
priority: int = 1,
controls: Mapping[str, float] | None = {},
controlsSlot: int = -1,
uniqueId=0,
name='',
setCallback: Callable[[Synth, str, float, float]] | None = None
) -> None:
assert controls is None or isinstance(controls, dict)
SchedEvent.__init__(self, instrname=instr.name if instr else '', 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 instr:
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.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("Autostop is disabled for named synths")
self._instr: Instr | None = None
self._setCallback = setCallback
def __del__(self):
if self.autostop:
self.stop()
@staticmethod
def makeGroup(synths: list[Synth]) -> SynthGroup:
return SynthGroup(synths)
[docs]
def set(self, param='', value: float = 0, delay=0, **kws) -> None:
proceed = True
if param:
# One callback shadows the other. If needed the synth callback
# can call the instr callback
if self._setCallback:
proceed = self._setCallback(self, param, value, delay)
elif func := self.instr._setCallback:
proceed = func(self, param, value, delay)
if proceed:
return super().set(param, value, delay, **kws)
[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)
[docs]
def ui(self, **specs: tuple[float, float] | tuple[float, float, str]) -> 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`
"""
if not self.playing():
logger.error("ui can only be shown for playing events, synth=%s", self)
raise ValueError("This synth is not playing, cannot show ui")
from . import interact
paramspecs = self._makeParamSpecs(specs)
return interact.interactSynth(self, paramspecs)
[docs]
def paramSpecs(self) -> dict[str, interact.ParamSpec]:
instr = self.instr
return instr.specs or {}
def _html(self, playstatus: str = '') -> str:
from . import _palette
argsfontsize = config['html_args_fontsize']
maxi = config['synth_repr_max_args']
style = _palette.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 playstatus == 'future':
delay = self.start - self.session.engine._realElapsedTime[0]
if delay > self.session.engine.extraLatency * 1.05:
parts.append(f"start={self.start - self.session.engine._realElapsedTime[0]:.3f}")
durstr = f"{self.dur:.3f}".rstrip("0").rstrip(".")
parts.append("dur=" + durstr)
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 inside_jupyter():
if config['jupyter_synth_repr_stopbutton']:
from . import jupytertools
jupytertools.displayButton("Stop", self.stop)
return f"<p>{self._html(playstatus=status)}</p>"
def __repr__(self):
playstr = _synthStatusIcon[self.playStatus()]
def f3(x) -> str:
return f"{x:.3f}".strip('0').rstrip('.')
parts = [f'{playstr} {self.instr.name}={self.p1} start={f3(self.start)} dur={f3(self.dur)}']
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
for i, parg in enumerate(pargs):
if i > maxi:
argsstrs.append("…")
break
pindex = i+5
name = i2n.get(pindex)
if not isinstance(parg, str):
parg = f'{parg:.6g}'
if name:
if showpidx:
s = f"{name}:{pindex}={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 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 _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("Synth %s has already stopped, cannot "
"set param '%s'", self, 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(["..."])
import emlib.misc
return emlib.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=False) -> set[str]:
out: set[str] = set()
for synth in self:
dynamicParams = synth.dynamicParamNames(aliases=aliases)
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=False) -> frozenset[str]:
out: set[str] = set()
for synth in self:
namedPargs = synth.pfieldNames(aliases=aliases)
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.paramNames()}")
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:
import emlib.iterlib
from . import _palette
subgroups = emlib.iterlib.classify(self.synths, lambda synth: synth.instr.name)
lines = []
instrcol = _palette.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 paramSpecs(self) -> dict[str, interact.ParamSpec]:
"""Returns the parameter specs, if defined
The returned dict maps a dynamic parameter name
to a :class``csoundengine.interact.ParamSpec``,
which defines the value range, start value, scale
type (linear, log), etc.
"""
allspecs = {}
for synth in self.synths:
specs = synth.paramSpecs()
allspecs.update(specs)
return allspecs
[docs]
def ui(self, **specs: tuple[float, float] | tuple[float, float, str]) -> 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`
"""
from . import interact
paramspecs = self._makeParamSpecs(specs)
return interact.interactSynth(self, paramspecs)
# 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:
from . import jupytertools
def bold(txt):
return 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)
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.aliases() or param in synth.dynamicParamNames(aliases=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
"""
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:
found = False
for synth in self.synths:
if synth.instr.pfieldIndex(param) is not None:
if synth.playStatus() != 'stopped':
synth._setPfield(param=param, value=value, delay=delay)
found = True
if not found:
raise KeyError(f"Parameter {param} unknown. "
f"Possible parameters: {self.dynamicParamNames()}")
[docs]
def controlNames(self, aliases=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)
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)