Skip to content

Commit

Permalink
* removed music21 as a dependency.
Browse files Browse the repository at this point in the history
core
----

* clip: redesigned the duration to incorporate looping. Added the
  possibility to give a Clip an explicit duration. This is needed, when
  looping, to set the duration of the clip independent of the duration
  of the source/selection.

* fixed chain.flat,

scoring
-------

* renamed Part to UnquantizedPart to make obvious what it is
* renamed Arrangement (a list of Parts) to UnquantizedScore
* renamed stackNotations... to resolveOffsets since, because now
  all events have a duration, stacking all that it does is fill in
  offsets
* renamed cloneAsPart to cloneAsTie to make more clear the purpose
* fixed some edge cases where the node merging algorithm was confused
* renderoptions now validates most options at creation time
* removed config from snd.plotting. The user should pass explicit options
*

documentation
-------------

* include documentation on scoring.render
* removed references regarding MEvents (notes, chord) having the
  possibility to let the duration undetermined. This was the previous
  design which was changed to enforce a duration on every object. But
  this was not always mentioned in the docs
  • Loading branch information
gesellkammer committed Jun 29, 2023
1 parent f7a280b commit 520c5f9
Show file tree
Hide file tree
Showing 30 changed files with 1,228 additions and 2,255 deletions.
1 change: 1 addition & 0 deletions docs/Reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ multiple backends (*lilypond*, *musescore*)

scoringcore
scoringquant
scoringrender

--------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/chain.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ the following differences:

* A :class:`Voice` can contain a Chain, but **a Voice cannot contain another Voice**
* A :class:`Voice` does not have a time offset, **its offset is always 0**
* A :class:`Voice` is used to represent a *Part* or *Instrument* within a score.
* A :class:`Voice` is used to represent a *UnquantizedPart* or *Instrument* within a score.
It includes multiple attributes and methods to customize its representation and playback.

.. automodapi:: maelzel.core.chain
Expand Down
2 changes: 1 addition & 1 deletion docs/clip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Clip

.. automodapi:: maelzel.core.clip
:no-inherited-members:
:no-heading:

16 changes: 7 additions & 9 deletions docs/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,16 @@ Key Concepts

MObj
All classes defined in **maelzel.core** inherit from :class:`~maelzel.core.mobj.MObj` (*Maelzel Object*, or
*Music Object*). A :class:`~maelzel.core.mobj.MObj` **exists in time** (it has a time offset and
duration attributes), it **can be displayed as notation** (:meth:`~maelzel.core.mobj.MObj.show`)
*Music Object*). A :class:`~maelzel.core.mobj.MObj` **exists in time** (it has a duration and a
time offset), it **can be displayed as notation** (:meth:`~maelzel.core.mobj.MObj.show`)
and **played as audio** (:meth:`~maelzel.core.mobj.MObj.play`)


Explicit / Implicit Time
A :class:`~maelzel.core.mobj.MObj` has always an *offset* and *dur* attributes.
These can be unset (``None``), meaning that they are **not explicitely determined** and depend
on the context. For example, a :class:`~maelzel.core.event.Note` might have no offset or
duration set. When adding such a note to a sequence of notes (a :class:`~maelzel.core.chain.Chain`)
its start time will be set to the end of the previous note/chord in the chain, or 0 if this is
the first note. Its duration will be determined to last until the next event in the sequence.
A :class:`~maelzel.core.mobj.MObj` always has an explicit duration (the *dur* attribute). The
*offset* can be undetermined (``None``), meaning that it is **not explicitely set** and depends
on the context. A :class:`~maelzel.core.event.Note` without an explicit offset
will be stacked left to the previous event.

Absolute Time / Relative Time
The time attributes (*offset*, *dur*, *end*) of a :class:`~maelzel.core.mobj.MObj` refer to a
Expand Down Expand Up @@ -134,7 +132,7 @@ Table of Contents
Containers: Chain, Voice <chain>
Score Structure: interfacing symbolic and real time <scorestruct>
Score <api/maelzel.core.score.Score>
Clip <api/maelzel.core.clip.Clip>
Clip <clip>
coreplayintro
config
workspace
Expand Down
1 change: 0 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ emlib>=1.7.0
numpy
scipy
matplotlib
music21
bpf4
configdict>=2.5
appdirs
Expand Down
12 changes: 6 additions & 6 deletions docs/scoringquant.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
===================
Quantization Engine
===================


===================================
maelzel.scoring.quant: Quantization
===================================

.. automodapi:: maelzel.scoring.quant
:no-inheritance-diagram:
Expand All @@ -13,9 +15,7 @@ Other Classes


:class:`~maelzel.scoring.node.Node`
-----------------------------------

A Node is a container, grouping Notation and other Nodes under one time modifier
A Node is a container, grouping Notation and other Nodes under one time modifier


.. toctree::
Expand Down
2 changes: 1 addition & 1 deletion maelzel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Only check dependencies on first run
if _state.isFirstSession() and not "sphinx" in sys.modules:

import logging
logging.basicConfig(level=logging.WARNING,
format='[%(name)s:%(filename)s:%(lineno)s - %(funcName)s] %(message)s')
Expand Down
14 changes: 7 additions & 7 deletions maelzel/core/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def flat(self, forcecopy=False) -> Chain:
return self

flatevents = self.eventsWithOffset()
events = [ev if not forcecopy or ev.offset == offset else ev.clone(offset=offset)
events = [ev.clone(offset=offset) if forcecopy or ev.offset != offset else ev
for ev, offset in flatevents]
return self.clone(items=events)

Expand Down Expand Up @@ -848,7 +848,7 @@ def scoringEvents(self,
config = Workspace.active.config

if parentOffset is None:
parentOffset = self.parent.absoluteOffset() if self.parent else F(0)
parentOffset = self.parent.absoluteOffset() if self.parent else F0

offset = parentOffset + self.resolveOffset()
chain = self.flat()
Expand All @@ -857,7 +857,7 @@ def scoringEvents(self,
notations.append(scoring.makeRest(duration=chain[0].resolveOffset()))
for item in chain.items:
notations.extend(item.scoringEvents(groupid=groupid, config=config, parentOffset=offset))
scoring.stackNotationsInPlace(notations)
scoring.resolveOffsetsInPlace(notations)

for n0, n1 in iterlib.pairwise(notations):
if n0.tiedNext and not n1.isRest:
Expand Down Expand Up @@ -888,16 +888,16 @@ def _solveOrfanHairpins(self, currentDynamic='mf'):

def scoringParts(self,
config: CoreConfig = None
) -> list[scoring.Part]:
) -> list[scoring.UnquantizedPart]:
if config is None:
config = Workspace.active.config
self._update()
notations = self.scoringEvents(config=config)
if not notations:
return []
scoring.stackNotationsInPlace(notations)
scoring.resolveOffsetsInPlace(notations)
if config['show.voiceMaxStaves'] == 1:
parts = [scoring.Part(notations, name=self.label)]
parts = [scoring.UnquantizedPart(notations, name=self.label)]
else:
groupid = scoring.makeGroupId()
parts = scoring.distributeNotationsByClef(notations, groupid=groupid)
Expand Down Expand Up @@ -1458,7 +1458,7 @@ def clone(self, **kws) -> Voice:
return out

def scoringParts(self, config: CoreConfig = None
) -> list[scoring.Part]:
) -> list[scoring.UnquantizedPart]:
if not self.name and self.label:
logger.debug("This Voice has a set label ({self.label}) but no name. If you "
"need to set the staff name/shortname, set those attributes "
Expand Down
82 changes: 62 additions & 20 deletions maelzel/core/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,28 @@ class Clip(event.MEvent):
Args:
source: the source of the clip (a filename, audiosample, samples as numpy array)
dur: the duration of the clip, in quarternotes. If not given, the duration will
be the duration of the source. If loop==True, then the duration **needs** to
be given
pitch: the pitch representation of this clip. It has no influence in the playback
itself, it is only for notation purposes
amp: the playback gain
offset: the time offset of this clip. Like in a Note, if not given,
the start time depends on the context (previous events) where this
clip is evaluated
label: a label str to identify this clip
dynamic: allows to attach a dynamic expression to this Clip. This is
only for notation purposes, it does not modify playback
startsecs: selection start time (in seconds)
endsecs: selection end time (in seconds). If 0., play until the end of the source
loop: if True, playback of this Clip should be looped
label: a label str to identify this clip
speed: playback speed of the clip
dynamic: allows to attach a dynamic expression to this Clip. This is
only for notation purposes, it does not modify playback
tied: this clip should be tied to the next one. This is only valid if the clips
share the same source (same soundfile or samples) and allows to automate
parameters such as playback speed or amplitude.
noteheadShape: the notehead shape to use for notation, one of 'normal', 'cross',
'harmonic', 'triangle', 'xcircle', 'rhombus', 'square', 'rectangle', 'slash', 'cluster'.
(see :ref:`config['show.clipNoteheadShape'] <config_show_clipnoteheadshape>`)
"""
_excludedPlayKeys: tuple[str] = ('instr', 'args')
Expand All @@ -71,22 +82,23 @@ class Clip(event.MEvent):
'_engine',
'_csoundTable',
'_sample',
'_durContext'
'_durContext',
'_explicitDur'
)

def __init__(self,
source: str | audiosample.Sample | tuple[np.ndarray, int],
dur: time_t = None,
pitch: pitch_t = None,
amp: float = 1.,
offset: time_t = None,
label: str = '',
dynamic: str = '',
startsecs: float | F = 0.,
endsecs: float | F = 0.,
channel: int = None,
speed: F | float = F1,
parent: MContainer | None = None,
loop=False,
speed: F | float = F1,
label: str = '',
dynamic: str = '',
tied=False,
noteheadShape: str = None
):
Expand All @@ -95,6 +107,9 @@ def __init__(self,
if not source:
raise ValueError("No source selected")

if loop and dur is None:
raise ValueError(f"The duration of a looping Clip needs to be given (source: {source})")

self.soundfile = ''
"""The soundfile holding the audio data (if any)"""

Expand Down Expand Up @@ -128,6 +143,8 @@ def __init__(self,
self.noteheadShape = noteheadShape
"""The shape to use as notehead"""

self._explicitDur: F | None = dur

if isinstance(source, tuple) and len(source) == 2 and isinstance(source[0], np.ndarray):
data, sr = source
assert isinstance(data, np.ndarray)
Expand Down Expand Up @@ -191,7 +208,8 @@ def __init__(self,
if offset is not None:
offset = asF(offset)

super().__init__(offset=offset, dur=None, label=label, parent=parent)
super().__init__(offset=offset, dur=dur, label=label)


@property
def sr(self) -> float:
Expand Down Expand Up @@ -249,7 +267,7 @@ def asSample(self) -> audiosample.Sample:
Return a :class:`maelzel.snd.audiosample.Sample` with the audio data of this Clip
Returns:
a Sample with the audio data of this Clip. The returned Sample is read-only
a Sample with the audio data of this Clip. The returned Sample is read-only.
Example
~~~~~~~
Expand All @@ -258,38 +276,61 @@ def asSample(self) -> audiosample.Sample:
"""
if self._sample is not None:
return self._sample

if isinstance(self.source, audiosample.Sample):
return self.source
else:
sample = audiosample.Sample(self.source, readonly=True, engine=self._engine)
assert isinstance(self.source, str)
sample = audiosample.Sample(self.source,
readonly=True,
engine=self._engine,
start=float(self.selectionStartSecs),
end=float(self.selectionEndSecs))
self._sample = sample
return sample

def isRest(self) -> bool:
return False

def durSecs(self) -> F:
assert isinstance(self.selectionEndSecs, F)
assert isinstance(self.selectionStartSecs, F)
assert isinstance(self.speed, F)
return (self.selectionEndSecs - self.selectionStartSecs) / self.speed

def pitchRange(self) -> tuple[float, float]:
return (self.pitch, self.pitch)

def _durationInBeats(self,
absoffset: F | None = None,
scorestruct: ScoreStruct = None) -> F:
"""
Calculate the duration in beats without considering looping or explicit duration
Args:
scorestruct: the score structure
Returns:
the duration in quarternotes
"""
absoffset = absoffset if absoffset is not None else self.absoluteOffset()
struct = scorestruct or self.scorestruct() or Workspace.active.scorestruct
starttime = struct.beatToTime(absoffset)
endbeat = struct.timeToBeat(starttime + self.durSecs())
return endbeat - absoffset

@property
def dur(self) -> F:
"The duration of this Clip, in quarter notes"
if self._explicitDur:
return self._explicitDur

absoffset = self.absoluteOffset()
struct = self.scorestruct() or Workspace.active.scorestruct

if self._dur is not None and self._durContext is not None:
cachedstruct, cachedbeat = self._durContext
if struct is cachedstruct and cachedbeat == absoffset:
return self._dur
starttime = struct.beatToTime(absoffset)
dursecs = self.durSecs()
endbeat = struct.timeToBeat(starttime + dursecs)
dur = endbeat - absoffset

dur = self._durationInBeats(absoffset=absoffset, scorestruct=struct)
self._dur = dur
self._durContext = (struct, absoffset)
return dur
Expand All @@ -308,7 +349,7 @@ def _synthEvents(self,
reloffset = self.resolveOffset()
offset = reloffset + parentOffset
starttime = float(scorestruct.beatToTime(offset))
endtime = float(starttime + self.durSecs())
endtime = float(scorestruct.beatToTime(offset + self.dur))
amp = firstval(self.amp, 1.0)
bps = [[starttime, self.pitch, amp],
[endtime, self.pitch, amp]]
Expand All @@ -320,7 +361,8 @@ def _synthEvents(self,
args = {'ipath': self.soundfile,
'isndfilechan': -1 if self.channel is None else self.channel,
'kspeed': self.speed,
'iskip': skip}
'iskip': skip,
'iwrap': 1 if self.loop else 0}
playargs = playargs.clone(instr='_clip_diskin', args=args)

elif self._playbackMethod == 'table':
Expand Down
2 changes: 1 addition & 1 deletion maelzel/core/configdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
'quant.minBeatFractionAcrossBeats': 0.5,
'quant.nestedTuplets': None,
'quant.nestedTupletsInMusicxml': False,
'quant.breakSyncopationsLevel': 'weak',
'quant.breakSyncopationsLevel': 'none',
'quant.complexity': 'high',
'quant.divisionErrorWeight': None,
'quant.gridErrorWeight': None,
Expand Down
Loading

0 comments on commit 520c5f9

Please sign in to comment.