Skip to content

Commit

Permalink
* make automation more ergonomic.
Browse files Browse the repository at this point in the history
* automation example and documentation
* optimize some imports to reduce start up time
* remove old/unused code from maelzel.distribute
* update dependencies
*
  • Loading branch information
gesellkammer committed Aug 30, 2023
1 parent dfa7c60 commit 8cb43f5
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 142 deletions.
1 change: 1 addition & 0 deletions maelzel/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import numbers as _numbers
import logging as _logging
import pitchtools as pt
import typing as t

from quicktions import Fraction as F
Expand Down
17 changes: 14 additions & 3 deletions maelzel/core/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,18 @@ def makeSynthAutomation(self,
return SynthAutomation(param=self.param, delay=float(delay), data=data)

@staticmethod
def normalizeBreakpoints(breakpoints: list[tuple], interpolation='linear'):
def normalizeBreakpoints(breakpoints: list[tuple] | list[num_t],
interpolation='linear'
) -> list[tuple[F | location_t, num_t, str]]:
"""
Normalize breakpoints, ensuring that all have the form (time, value, interpolation)
Args:
breakpoints: a list of tuples of the form (time, value) or
(time, value, interpolation)
interpolation: the default interpolation
(time, value, interpolation) or a flat list of the form
[time0, value0, time1, value1, ...]. In this case all breakpoints
use the interpolation given as fallback
interpolation: the default/fallback interpolation
Returns:
a list of tuples of the form (time, value, interpolation). The interpolation
Expand All @@ -147,6 +151,13 @@ def normalizeBreakpoints(breakpoints: list[tuple], interpolation='linear'):
"""
if not interpolation:
interpolation = 'linear'

if not isinstance(breakpoints[0], tuple):
# a flat list
assert len(breakpoints) % 2 == 0, "A flat list of breakpoints needs to be even"
assert all(isinstance(x, (int, float, F)) for x in breakpoints)
breakpoints = list(iterlib.window(breakpoints, 2, 2))

normalized = []
for bp in breakpoints:
l = len(bp)
Expand Down
4 changes: 2 additions & 2 deletions maelzel/core/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from typing import TYPE_CHECKING, overload
if TYPE_CHECKING:
from typing import Any, Iterator, overload, TypeVar, Callable
from ._typedefs import time_t, location_t
from ._typedefs import time_t, location_t, num_t
ChainT = TypeVar("ChainT", bound="Chain")


Expand Down Expand Up @@ -1367,7 +1367,7 @@ def matchOrfanSpanners(self, removeUnmatched=False) -> None:

def automate(self,
param: str,
breakpoints: list[tuple[time_t|location_t, float]],
breakpoints: list[tuple[time_t|location_t, float]] | list[num_t],
relative=True,
interpolation='linear'
) -> None:
Expand Down
8 changes: 4 additions & 4 deletions maelzel/core/configdata.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import math
from maelzel import textstyle
from maelzel.textstyle import TextStyle
from maelzel import dynamiccurve


Expand Down Expand Up @@ -149,9 +149,9 @@
'show.jupyterMaxImageWidth::type': int,
'show.voiceMaxStaves::type': int,
'show.voiceMaxStaves::range': (1, 4),
'show.measureAnnotationStyle': lambda cfg, key, val: textstyle.validateStyle(val),
'show.centsAnnotationStyle': lambda cfg, key, val: textstyle.validateStyle(val),
'show.rehearsalMarkStyle': lambda cfg, key, val: textstyle.validateStyle(val),
'show.measureAnnotationStyle': lambda cfg, key, val: TextStyle.validate(val),
'show.centsAnnotationStyle': lambda cfg, key, val: TextStyle.validate(val),
'show.rehearsalMarkStyle': lambda cfg, key, val: TextStyle.validate(val),
'show.clipNoteheadShape::choices': ('', 'square', 'normal', 'cross', 'harmonic', 'triangle',
'xcircle', 'rhombus', 'rectangle', 'slash', 'diamond',
'cluster'),
Expand Down
57 changes: 53 additions & 4 deletions maelzel/core/eventbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import TypeVar, Any, Callable
from ._typedefs import time_t, location_t
from ._typedefs import time_t, location_t, num_t
MEventT = TypeVar("MEventT", bound="MEvent")


Expand Down Expand Up @@ -242,7 +242,18 @@ def addSpanner(self: MEventT,
spanner.setAnchor(self)
return self

def timeTransform(self: MEventT, timemap: Callable[[F], F], inplace=False) -> MEventT:
def timeTransform(self: MEventT, timemap: Callable[[F], F], inplace=False
) -> MEventT:
"""
Apply a transformation to the time axes of this
Args:
timemap: a callable mapping time (in quarterbeats) to time (in quarterbeats)
inplace:
Returns:
"""
offset = self.relOffset()
dur = self.dur
offset2 = timemap(offset)
Expand All @@ -257,7 +268,7 @@ def timeTransform(self: MEventT, timemap: Callable[[F], F], inplace=False) -> ME

def automate(self,
param: str,
breakpoints: list[tuple[time_t | location_t, float]] | list[tuple[time_t|location_t, float, str]],
breakpoints: list[tuple[time_t | location_t, float]] | list[tuple[time_t|location_t, float, str]] | list[num_t],
interpolation='linear',
relative=True,
) -> None:
Expand All @@ -272,13 +283,20 @@ def automate(self,
breakpoints: the data, a list of pairs in the form (time, value),
or (time, value, interpolation). time is given in quarternotes or as a
location (measure, beatoffset); value is any valid valud for the given
parameter; interpolation is one of 'linear', 'cos'
parameter; interpolation is one of 'linear', 'cos'. As a shortcut
it is possible to also pass a flat list of the form
[time0, value0, time1, value1, ...]
interpolation: default interpolation used for breakpoints without interpolation
relative: if True, the time positions are relative to the absolute offset
of this event. If False, these times are absolute times
Example
~~~~~~~
.. code::
note = Note("4c", 10)
# Automate position starting at beat 5 after this event has started
note.automate('position', [(5, 0.), (6, 1.)], relative=True)
Expand All @@ -287,7 +305,38 @@ def automate(self,
# Time position can be also given as a tuple (measure num, beat offset),
# and the time mode can be set to absolute
# In this case, this automation indicates a modification of the
# pan position, from 0 to 1 starting at the 4th measure (index 3) and
# ending at the 5th measure (index 4)
note.automate('position', [(3, 0), 0., (4, 0), 1.], relative=False)
Any dynamic parameter can be automated:
.. code::
# Define a preset with some dynamic parameter
defPreset('detuned', '''
|kdetune=2|
aout1 = oscili:a(kamp, kfreq) + oscili:a(kamp, kfreq+kdetune)
''')
# Automate the kdetune param. When automating a Note/Chord, the times
# are given in quarterbeats, which means that the real time in seconds
# depend on the tempo structure. In this case the kdetune param will
# be shifted from 0 to 20 starting at the moment the note is played
# and ending 4 **quarterbeats** after.
note.automate('kdetune', (0, 0, 4, 20))
# When the note is actually played the automation takes effect
synth = note.play(instr='detuned')
# The synth itself can be automated. In this case, we are already
# in the real-time realm and any times are given in seconds. If
# needed the active scorestruct can be used to convert between
# quarterbeats and seconds
synth.automate('position', (2, 0.5, 4, 1))
"""
if self.playargs is None:
self.playargs = PlayArgs()
Expand Down
4 changes: 2 additions & 2 deletions maelzel/core/mobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
import csoundengine

from maelzel.common import asmidi, F, asF, F0, F1
import maelzel.textstyle as _textstyle
from maelzel.textstyle import TextStyle

from ._common import logger
from ._typedefs import *
Expand Down Expand Up @@ -950,7 +950,7 @@ def _scoringAnnotation(self, text: str = None, config: CoreConfig = None
if text is None:
assert self.label
text = self.label
labelstyle = _textstyle.parseTextStyle(config['show.labelStyle'])
labelstyle = TextStyle.parse(config['show.labelStyle'])
return scoring.attachment.Text(text,
fontsize=labelstyle.fontsize,
italic=labelstyle.italic,
Expand Down
4 changes: 2 additions & 2 deletions maelzel/core/notation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from __future__ import annotations
from maelzel import textstyle
from maelzel.textstyle import TextStyle
from .config import CoreConfig
from .workspace import getConfig, getWorkspace
from maelzel import scoring
Expand Down Expand Up @@ -46,7 +46,7 @@ def makeRenderOptionsFromConfig(cfg: CoreConfig = None,
if cfg is None:
cfg = getConfig()

centsAnnotationStyle = textstyle.parseTextStyle(cfg['show.centsAnnotationStyle'])
centsAnnotationStyle = TextStyle.parse(cfg['show.centsAnnotationStyle'])

renderOptions = scoring.render.RenderOptions(
centsAnnotationFontsize=centsAnnotationStyle.fontsize or 8,
Expand Down
9 changes: 8 additions & 1 deletion maelzel/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Module to check dependencies of maelzel
"""
from __future__ import annotations
import shutil
from pathlib import Path
Expand Down Expand Up @@ -28,10 +31,14 @@ def csoundLibVersion() -> int | None:

def checkCsound(minversion="6.18", checkBinaryInPath=True) -> str:
"""
Returns True if csound is installed
Returns an empty string if csound is installed, otherwise an error message
Args:
minversion: min. csound version, as "{major}.{minor}"
Returns:
an error message, or an empty str if csound is installed and the version
is >= than the version given
"""
logger.debug("Checking the csound installation")
if not isinstance(minversion, str) or minversion.count('.') != 1:
Expand Down
Loading

0 comments on commit 8cb43f5

Please sign in to comment.