Source code for maelzel.core.workspace

from __future__ import annotations
import os
import appdirs as _appdirs
from functools import cache
import pitchtools

from ._common import logger
from .config import CoreConfig

from maelzel.common import F
from maelzel.dynamiccurve import DynamicCurve
from maelzel.scorestruct import ScoreStruct

import typing as _t
if _t.TYPE_CHECKING:
    from maelzel.core.renderer import Renderer
    from typing import Any
    import csoundengine.session
    from . import presetmanager


def _clearCache() -> None:
    from .mobj import clearImageCache
    clearImageCache()


__all__ = (
    'Workspace',

    'getWorkspace',
    'getConfig',
    'getScoreStruct',
    'setTempo',

    'logger'
)


[docs] class Workspace: """ Create a new Workspace Args: scorestruct: the ScoreStruct. If None, a default scorestruct (4/4, q=60) is used config: the active config for this workspace. If None, a copy of the root config is used updates: if given, these are applied to the config dynamicCurve: a DynamicCurve used to map amplitude to dynamic expressions active: if True, make this Workpsace active A Workspace can also be used as a context manager, in which case it will be activated when entering the context and deactivated at exit .. code:: from maelzel.core import * scorestruct = ScoreStruct(r''' 4/4, 60 . 3/4 5/8, 72 ''') notes = Chain([Note(m, start=i) for i, m in enumerate(range(60, 72))]) # Create a temporary Workspace with the given scorestruct and a clone # of the active config with Workspace(scorestruct=scorestruct, config=getConfig()) as w: notes.show() """ root: _t.ClassVar[Workspace] """The root workspace. This is the workspace active at the start of a session and is always kept alive since it holds a reference to the root config. It should actually never be None""" active: _t.ClassVar[Workspace] """The currently active workspace""" _initdone: _t.ClassVar[bool] = False def __init__(self, config: CoreConfig | None = None, scorestruct: ScoreStruct | None = None, dynamicCurve: DynamicCurve | None = None, updates: dict[str, Any] | None = None, active=False): assert self._initdone if config is None: config = CoreConfig(updates=updates or None) elif updates: config = config.clone(updates=updates) if dynamicCurve is None: mindb = config['dynamicCurveMindb'] maxdb = config['dynamicCurveMaxdb'] dynamics = config['dynamicCurveDynamics'].split() dynamicCurve = DynamicCurve.fromdescr(shape=config['dynamicCurveShape'], mindb=mindb, maxdb=maxdb, dynamics=dynamics) if scorestruct is None: scorestruct = ScoreStruct((4, 4), tempo=60) self._config: CoreConfig = config """The CoreConfig attached to this Workspace""" self.renderer: Renderer | None = None """The active renderer, if any""" self.dynamicCurve: DynamicCurve = dynamicCurve """The dynamic curve used to convert dynamics to amplitudes""" self._scorestruct: ScoreStruct = scorestruct """The scorestruct attached to this workspace""" self._previousWorkspace: Workspace | None = None """The previous workspace. Will be activated when this one is desactivated""" if active: self.activate() @property def config(self) -> CoreConfig: """The CoreConfig for this workspace""" return self._config @config.setter def config(self, config: CoreConfig) -> None: self._config = config if self.isActive(): self._activateConfig() def _activateConfig(self) -> None: config = self._config pitchtools.set_reference_freq(config['A4']) _clearCache()
[docs] @staticmethod def getConfig() -> CoreConfig: """ Get the active config """ wspace = Workspace.active assert wspace is not None return wspace.config
[docs] @staticmethod def clearCache() -> None: """ Cleat the Workspace cache. At the moment this cache includes only the image generated via .show """ _clearCache()
@staticmethod def _initclass() -> None: if Workspace._initdone: logger.debug("init was already done") return Workspace._initdone = True CoreConfig._root = rootconfig = CoreConfig(source='load') # The root config itself should never be active since it is read-only Workspace.root = Workspace(config=rootconfig.copy(), active=True) assert Workspace.active is Workspace.root
[docs] def deactivate(self) -> None: """ Deactivates this Workspace and sets the previous Workspace as active .. note:: There is always an active Workspace. An attempt to deactivate the root Workspace will be ignored Returns: the now active workspace """ if Workspace.active is not self: logger.warning("Cannot deactivate this Workspace since it is not active") elif self is Workspace.root: logger.warning("Cannot deactivate the root Workspace") elif self._previousWorkspace is None: logger.warning("This Workspace has not previous workspace, activating the root" " Workspace instead") root = Workspace.root assert root is not None root.activate() else: self._previousWorkspace.activate()
def __del__(self): if self.isActive() and self is not Workspace.root: self.deactivate() def __enter__(self): if getWorkspace() is self: return self._previousWorkspace = getWorkspace() self.activate() def __exit__(self, *args, **kws): if self._previousWorkspace is not None: self._previousWorkspace.activate() self._previousWorkspace = None def __repr__(self): return (f"Workspace(scorestruct={repr(self.scorestruct)}, " f"config={self.config.diff()}, " f"dynamicCurve={self.dynamicCurve})") @property def scorestruct(self) -> ScoreStruct: """The default ScoreSctruct for this Workspace""" return self._scorestruct @scorestruct.setter def scorestruct(self, s: ScoreStruct): if s.beatWeightTempoThresh is None: s.beatWeightTempoThresh = self.config['quant.beatWeightTempoThresh'] if s.subdivTempoThresh is None: s.subdivTempoThresh = self.config['quant.subdivTempoThresh'] self._scorestruct = s self.clearCache()
[docs] @staticmethod def setScoreStruct(score: str | ScoreStruct | tuple[int, int] = (4, 4), tempo: F | int | float = 60) -> None: """ Sets the current score structure This is the same as `ScoreStruct(...).activate()` If given a ScoreStruct, it sets it as the active score structure. As an alternative a score structure as string can be given, or simply a time signature and/or tempo, in which case it will create the ScoreStruct and set it as active Args: score: the scorestruct as a ScoreStruct, a string score (see ScoreStruct for more information about the format) or simply a time signature. tempo: the quarter-note tempo. Only used if no score is given .. seealso:: * :func:`~maelzel.core.workpsace.getScoreStruct` * :func:`~maelzel.core.workpsace.setTempo` (modifies the tempo of the active scorestruct) * :class:`~maelzel.scorestruct.ScoreStruct` * :func:`~maelzel.core.workpsace.getWorkspace` Example ~~~~~~~ >>> from maelzel.core import * >>> Workspace.setScoreStruct(ScoreStruct(tempo=72)) >>> Workspace.setScoreStruct(r''' ... 4/4, 72 ... 3/8 ... 5/4 ... . # Same time-signature and tempo ... , 112 # Same time-signature, faster tempo ... 20, 3/4, 60 # At measure index 20, set the time-signature to 3/4 and tempo to 60 ... ... # Endless score ... ''') """ setScoreStruct(score=score, tempo=tempo)
@property def a4(self) -> float: """The reference frequency in this Workspace""" return self.config['A4'] @a4.setter def a4(self, value: float): self.config.bypassCallbacks = True self.config['A4'] = value self.config.bypassCallbacks = False if self.isActive(): pitchtools.set_reference_freq(value)
[docs] def getTempo(self, measureNum=0) -> float: """Get the quarternote tempo at the given measure""" return float(self.scorestruct.getMeasureDef(measureNum).quarterTempo)
[docs] def activate(self) -> Workspace: """Make this the active Workspace This method returns self in order to allow chaining Example ------- >>> from maelzel.core import * >>> from pitchtools import * >>> w = Workspace(updates={'A4': 432}).activate() >>> n2f("A4") 432 >>> w.deactivate() >>> n2f("A4") 442 """ pitchtools.set_reference_freq(self.a4) if hasattr(Workspace, "active"): self._previousWorkspace = Workspace.active Workspace.active = self return self
[docs] def isActive(self) -> bool: """Is this the active Workspace?""" return Workspace.active is self
[docs] def clone(self, config: CoreConfig | None = None, scorestruct: ScoreStruct | None = None, active=False ) -> Workspace: """ Clone this Workspace Args: config: the config to use. **Leave unset** to clone this Workspace's config. scorestruct: if unset, use this Workspace's scorestruct active: if True, activate the cloned Workspace Returns: the cloned Workspace Example ------- >>> from maelzel.core import * >>> myworkspace = getWorkspace().clone() >>> myworkspace.config['A4'] = 432 >>> with myworkspace as w: ... # This will activate the workspace and deactivate it at exit ... # Now do something baroque """ if config is None: assert isinstance(self.config, CoreConfig) config = self.config.copy() if scorestruct is None: scorestruct = self.scorestruct.copy() return Workspace(config=config, scorestruct=scorestruct or self.scorestruct, active=active)
[docs] @staticmethod def activeScoreStruct() -> ScoreStruct: """ Returns the active score structure Returns: ScoreStruct: The active score structure Example ~~~~~~~ Running in linux .. code-block:: python >>> from maelzel.core import * >>> scorestruct = getWorkspace().activeScoreStruct() """ return Workspace.active.scorestruct
[docs] @staticmethod def presetsPath() -> str: """ Returns the path where instrument presets are read/written Example ~~~~~~~ Running in linux .. code-block:: python >>> from maelzel.core import * >>> path = getWorkspace().presetsPath() >>> path '/home/XXX/.local/share/maelzel/core/presets' >>> os.listdir(path) ['.click.yaml', 'click.yaml', 'noise.yaml', 'accordion.yaml', 'piano.yaml', 'voiceclick.yaml'] """ return _presetsPath()
[docs] def recordPath(self) -> str: """ The path where temporary recordings are saved We do not use the temporary folder because it is wiped regularly and the user might want to access a recording after rebooting. The returned folder is guaranteed to exist The default record path can be customized by modifying the config 'rec.path' """ userpath = self.config['rec.path'] if userpath: path = userpath else: path = _appdirs.user_data_dir(appname="maelzel", version="recordings") if not os.path.exists(path): os.makedirs(path) return path
[docs] def setRecordPath(self, path: str) -> None: self.config['rec.path'] = path
[docs] def setDynamicsCurve(self, shape='expon(0.5)', mindb=-80, maxdb=0) -> Workspace: """ Set a new dynamics curve for this Workspace Args: shape: the shape of the curve mindb: the db value mapped to the softest dynamic maxdb: the db value mapped to the loudest dynamic Returns: self """ self.dynamicCurve = DynamicCurve.fromdescr(shape=shape, mindb=mindb, maxdb=maxdb) return self
[docs] def amp2dyn(self, amp: float) -> str: return self.dynamicCurve.amp2dyn(amp)
[docs] def playSession(self, outdev='', backend='', numchannels: int | None = None, buffersize: int = 0, numbuffers: int = 0, **kws) -> csoundengine.session.Session: """ Get the audio Session used for playback Arguments are ignored if a session is already active Args: outdev: output device used. Depends on the backend used. List all devices via maelzel.core.playback.getAudioDevices, use '?' to select from a list of devices. backend: backend, depends on your platform. Use '?' to interactively select one numchannels: number of channels used. Defaults to the number of channels of the audio device used buffersize: buffer size to use, depends on the backend and device used numbuffers: number of buffers, determines the blocksize depending on the backend **kws: any keyword argument is passed to maelzel.core.playback.getSession Returns: the csoundengine.Session active for this workspace. If not already created, a new session is started """ from maelzel.core import playback return playback.getSession(name=self.config['play.engineName'], outdev=outdev, backend=backend, numchannels=numchannels, buffersize=buffersize, numbuffers=numbuffers, **kws)
[docs] def isPlaySessionActive(self) -> bool: """ Returns True if the sound engine is active """ name = self.config['play.engineName'] return name in csoundengine.Engine.activeEngines
[docs] def presetManager(self) -> presetmanager.PresetManager: from . import presetmanager return presetmanager.presetManager
[docs] def getWorkspace() -> Workspace: """ Get the active workspace The active Workspace can be accessed via ``Workspace.active``. This function is simply a shortcut, placed here for visibility Example ~~~~~~~ Create a new Workspace based on the active Workspace and activate it >>> from maelzel.core import * >>> w = getWorkspace().clone(active=True) The active workspace can always be accessed directly: >>> w = Workspace.active >>> w is getWorkspace() True """ assert Workspace.active is not None return Workspace.active
[docs] def setTempo(tempo: float, reference=1, measureIndex=0) -> None: """ Set the current tempo. Args: tempo: the new tempo. reference: the reference value (1=quarternote, 2=halfnote, 0.5: 8th note) measureIndex: the measure number to modify. The scorestruct's tempo is modified until the next tempo See Also ~~~~~~~~ * :meth:`ScoreStruct.setTempo <maelzel.scorestruct.ScoreStruct.setTempo>` * :ref:`setTempo notebook <setTempo_notebook>` Example ~~~~~~~ .. code-block:: python from maelzel.core import * # A chromatic scale of eighth notes scale = Chain(Note(m, dur=0.5) for m in range(60, 72)) # Will play 8th notes at 60 scale.play() setTempo(120) # Will play at twice the speed scale.play() # setTempo is a shortcut to ScoreStruct's setTempo method struct = getScoreStruct() struct.setTempo(40) .. code-block:: python >>> setScoreStruct(ScoreStruct(r''' ... 3/4, 120 ... 4/4, 66 ... 5/8, 132 ... ''')) >>> setTempo(40) >>> getScoreStruct().dump() 0, 3/4, 40 1, 4/4, 66 2, 5/8, 132 """ active = Workspace.active assert active is not None active.scorestruct.setTempo(tempo, reference=reference, measureIndex=measureIndex)
[docs] def getConfig() -> CoreConfig: """ Return the active config. This function is here for visibility. The preferred way to access the active config is via ``Workspace.active.config`` """ active = Workspace.active assert active is not None return active.config
[docs] def getScoreStruct() -> ScoreStruct: """ Returns the active ScoreStruct (which defines tempo and time signatures) If no ScoreStruct has been set explicitely, a default struct is always active. To create a new scorestruct and set it as active use ``ScoreStruct(...).activate()`` This function is here for visibility. The preferred way to access the active scorestruct is via ``Workspace.active.scorestruct`` .. seealso:: * :func:`~maelzel.core.workpsace.setTempo` * :class:`~maelzel.scorestruct.ScoreStruct` """ active = Workspace.active assert active is not None return active.scorestruct
@cache def _presetsPath() -> str: datadirbase = _appdirs.user_data_dir("maelzel") path = os.path.join(datadirbase, "core", "presets") return path Workspace._initclass()