#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
This module defines an ontology of musical elements to represent
musical scores, such as measures, notes, slurs, words, tempo and
loudness directions. A score is defined at the highest level by a
`Part` object (or a hierarchy of `Part` objects, in a `PartGroup`
object). This object serves as a timeline at which musical elements
are registered in terms of their start and end times.
"""
from copy import copy, deepcopy
from collections import defaultdict
from collections.abc import Iterable
from numbers import Number
# import copy
from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES
import warnings, sys
import numpy as np
from scipy.interpolate import PPoly
from typing import Union, List, Optional, Iterator, Iterable as Itertype
from partitura.utils import (
ComparableMixin,
ReplaceRefMixin,
iter_subclasses,
iter_current_next,
sorted_dict_items,
PrettyPrintTree,
ALTER_SIGNS,
find_tie_split,
format_symbolic_duration,
estimate_symbolic_duration,
symbolic_to_numeric_duration,
fifths_mode_to_key_name,
pitch_spelling_to_midi_pitch,
note_array_from_part,
rest_array_from_part,
rest_array_from_part_list,
note_array_from_part_list,
to_quarter_tempo,
key_mode_to_int,
_OrderedSet,
update_note_ids_after_unfolding,
)
from partitura.utils.generic import interp1d
[docs]class Part(object):
"""Represents a score part, e.g. all notes of one single instrument
(or multiple instruments written in the same staff). Note that
there may be more than one staff per score part.
Parameters
----------
id : str
The identifier of the part. In order to be compatible with
MusicXML the identifier should not start with a number.
part_name : str or None, optional
Name for the part. Defaults to None
part_abbreviation : str or None, optional
Abbreviated name for part
quarter_duration : int, optional
The default quarter duration. See
:meth:`~partitura.score.Part.set_quarter_duration` for
details.
Attributes
----------
id : str
See parameters
part_name : str
See parameters
part_abbreviation : str
See parameters
"""
def __init__(self, id, part_name=None, part_abbreviation=None, quarter_duration=1):
super().__init__()
self.id = id
self.parent = None
self.part_name = part_name
self.part_abbreviation = part_abbreviation
# timeline init
self._points = np.array([], dtype=TimePoint)
self._quarter_times = [0]
self._quarter_durations = [quarter_duration]
self._quarter_map = self.quarter_duration_map
# set beat reference
self._use_musical_beat = False
# store number of staves
self._number_of_staves = None
def __str__(self):
return 'Part id="{}" name="{}"'.format(self.id, self.part_name)
def _pp(self, tree):
result = [self.__str__()]
tree.push()
N = len(self._points)
for i, timepoint in enumerate(self._points):
result.append("{}".format(tree).rstrip())
if i == N - 1:
tree.last_item()
else:
tree.next_item()
result.extend(timepoint._pp(tree))
tree.pop()
return result
[docs] def pretty(self):
"""Return a pretty representation of this object.
Returns
-------
str
A pretty representation
"""
return "\n".join(self._pp(PrettyPrintTree()))
@property
def time_signature_map(self):
"""A function mapping timeline times to the beats and beat_type
of the time signature at that time. The function can take
scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
tss = np.array(
[
(ts.start.t, ts.beats, ts.beat_type, ts.musical_beats)
for ts in self.iter_all(TimeSignature)
]
)
if len(tss) == 0:
# default time sig
beats, beat_type, musical_beats = 4, 4, 4
warnings.warn(
"No time signatures found, assuming {}/{}".format(beats, beat_type)
)
if self.first_point is None:
t0, tN = 0, 0
else:
t0 = self.first_point.t
tN = self.last_point.t
tss = np.array(
[
(t0, beats, beat_type, musical_beats),
(tN, beats, beat_type, musical_beats),
]
)
elif len(tss) == 1:
# If there is only a single time signature
tss = np.array([tss[0, :], tss[0, :]])
elif tss[0, 0] > self.first_point.t:
tss = np.vstack(
((self.first_point.t, tss[0, 1], tss[0, 2], tss[0, 3]), tss)
)
return interp1d(
tss[:, 0],
tss[:, 1:],
axis=0,
kind="previous",
bounds_error=False,
fill_value="extrapolate",
)
@property
def key_signature_map(self):
"""A function mappting timeline times to the key and mode of
the key signature at that time. The function can take scalar
values or lists/arrays of values
Returns
-------
function
The mapping function
"""
kss = np.array(
[
(ks.start.t, ks.fifths, key_mode_to_int(ks.mode))
for ks in self.iter_all(KeySignature)
]
)
if len(kss) == 0:
# default key signature
fifths, mode = 0, 1
warnings.warn("No key signature found, assuming C major")
if self.first_point is None:
t0, tN = 0, 0
else:
t0 = self.first_point.t
tN = self.first_point.t
kss = np.array([(t0, fifths, mode), (tN, fifths, mode)])
elif kss[0, 0] > self.first_point.t:
kss = np.vstack(((self.first_point.t, kss[0, 1], kss[0, 2]), kss))
return interp1d(
kss[:, 0],
kss[:, 1:],
axis=0,
kind="previous",
bounds_error=False,
fill_value="extrapolate",
)
@property
def measure_map(self):
"""A function mapping timeline times to the start and end of
the measure they are contained in. The function can take
scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
measures = np.array([(m.start.t, m.end.t) for m in self.iter_all(Measure)])
# correct for anacrusis
divs_per_beat = self.inv_beat_map(
1 + self.beat_map(0)
) # find the divs per beat in the first measure
if (
measures[0][1] - measures[0][0]
< self.time_signature_map(0)[0] * divs_per_beat
):
measures[0][0] = (
measures[0][1] - self.time_signature_map(0)[0] * divs_per_beat
)
if len(measures) == 0: # no measures in the piece
# default only one measure spanning the entire timeline
warnings.warn("No measures found, assuming only one measure")
if self.first_point is None:
t0, tN = 0, 0
else:
t0 = self.first_point.t
tN = self.last_point.t
measures = np.array([(t0, tN)])
inter_function = interp1d(
measures[:, 0],
measures[:, :].astype(int),
kind="previous",
axis=0,
fill_value="extrapolate",
dtype=int,
)
return inter_function
@property
def measure_number_map(self):
"""A function mapping timeline times to the measure number of
the measure they are contained in. The function can take
scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
# operations to avoid None values and filter them efficiently.
m_it = self.measures
measures = np.array(
[
[
m.start.t,
m.end.t,
(m_it[i - 1].number if m.number == None else m.number),
]
for i, m in enumerate(m_it)
]
)
# correct for anacrusis
divs_per_beat = self.inv_beat_map(
1 + self.beat_map(0)
) # find the divs per beat in the first measure
if (
measures[0][1] - measures[0][0]
< self.time_signature_map(0)[0] * divs_per_beat
):
measures[0][0] = (
measures[0][1] - self.time_signature_map(0)[0] * divs_per_beat
)
if len(measures) == 0: # no measures in the piece
# default only one measure spanning the entire timeline
warnings.warn("No measures found, assuming only one measure")
if self.first_point is None:
t0, tN = 0, 0
else:
t0 = self.first_point.t
tN = self.last_point.t
measures = np.array([(t0, tN, 1)])
inter_function = interp1d(
measures[:, 0],
measures[:, 2],
kind="previous",
fill_value="extrapolate",
dtype=int,
)
return inter_function
@property
def metrical_position_map(self):
"""A function mapping timeline times to their relative position in
the measure they are contained in. The function can take
scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
measure_map = self.measure_map
ms = [measure_map(m.start.t)[0] for m in self.iter_all(Measure)]
me = [measure_map(m.start.t)[1] for m in self.iter_all(Measure)]
if len(ms) < 2:
warnings.warn("No or single measures found, metrical position 0 everywhere")
zero_interpolator = interp1d(
np.arange(0, 2),
np.zeros((2, 2)),
axis=0,
kind="linear",
fill_value="extrapolate",
dtype=int,
)
return zero_interpolator
else:
barlines = np.array(ms + me[-1:])
bar_durations = np.diff(barlines)
measure_inter_function = interp1d(
barlines[:-1],
bar_durations,
axis=0,
kind="previous",
fill_value="extrapolate",
)
lin_poly_coeff = np.row_stack(
(np.ones(bar_durations.shape[0]), np.zeros(bar_durations.shape[0]))
)
inter_function = PPoly(lin_poly_coeff, barlines)
def int_interp1d(input):
if isinstance(input, Iterable):
return np.column_stack(
(
inter_function(input).astype(int),
measure_inter_function(input).astype(int),
)
)
else:
return (
inter_function(input).astype(int),
measure_inter_function(input).astype(int),
)
return int_interp1d
def _time_interpolator(self, quarter=False, inv=False, musical_beat=False):
if len(self._points) < 2:
return lambda x: np.zeros(len(x))
keypoints = defaultdict(lambda: [None, None])
_ = keypoints[self.first_point.t]
_ = keypoints[self.last_point.t]
for t, q in zip(self._quarter_times, self._quarter_durations):
keypoints[t][0] = q
if not quarter:
for ts in self.iter_all(TimeSignature):
# keypoints[ts.start.t][1] = int(np.log2(ts.beat_type))
if musical_beat:
keypoints[ts.start.t][1] = (ts.beat_type / 4) * (
ts.musical_beats / ts.beats
)
else:
keypoints[ts.start.t][1] = ts.beat_type / 4
cur_div = 1
cur_bt = 1
keypoints_list = []
for t in sorted(keypoints.keys()):
kp = keypoints[t]
if kp[0] is None:
kp[0] = cur_div
else:
cur_div = kp[0]
if kp[1] is None:
kp[1] = cur_bt
else:
cur_bt = kp[1]
if not keypoints_list or kp != keypoints_list[-1]:
keypoints_list.append([t] + kp)
keypoints = np.array(keypoints_list, dtype=float)
x = keypoints[:, 0]
y = np.r_[
0,
np.cumsum(
(keypoints[:-1, 2] * np.diff(keypoints[:, 0])) / keypoints[:-1, 1]
),
]
m1 = next(self.first_point.iter_starting(Measure), None)
if m1 and m1.start is not None and m1.end is not None:
f = interp1d(x, y)
actual_dur = np.diff(f((m1.start.t, m1.end.t)))[0]
ts = next(m1.start.iter_starting(TimeSignature), None)
if ts:
normal_dur = ts.beats
if quarter:
normal_dur *= 4 / ts.beat_type
if musical_beat:
normal_dur = ts.musical_beats
if actual_dur < normal_dur:
y -= actual_dur
else:
# warn
pass
if inv:
return interp1d(y, x)
else:
return interp1d(x, y)
@property
def beat_map(self):
"""A function mapping timeline times to beat times. The function
can take scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
if self._use_musical_beat:
return self._time_interpolator(musical_beat=True)
else:
return self._time_interpolator()
@property
def inv_beat_map(self):
"""A function mapping beat times to timeline times. The function
can take scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
if self._use_musical_beat:
return self._time_interpolator(inv=True, musical_beat=True)
else:
return self._time_interpolator(inv=True)
@property
def quarter_map(self):
"""A function mapping timeline times to quarter times. The
function can take scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
return self._time_interpolator(quarter=True)
@property
def inv_quarter_map(self):
"""A function mapping quarter times to timeline times. The
function can take scalar values or lists/arrays of values.
Returns
-------
function
The mapping function
"""
return self._time_interpolator(quarter=True, inv=True)
@property
def notes(self):
"""Return a list of all Note objects in the part. This list includes
GraceNote objects but not Rest objects.
Returns
-------
list
list of Note objects
"""
return list(self.iter_all(Note, include_subclasses=True))
@property
def notes_tied(self):
"""Return a list of all Note objects in the part that are
either not tied, or the first note of a group of tied notes.
This list includes GraceNote objects but not Rest objects.
Returns
-------
list
List of Note objects
"""
return [
note
for note in self.iter_all(Note, include_subclasses=True)
if note.tie_prev is None
]
@property
def measures(self):
"""Return a list of all Measure objects in the part
Returns
-------
list
List of Measure objects
"""
return [e for e in self.iter_all(Measure, include_subclasses=False)]
@property
def rests(self):
"""Return a list of all rest objects in the part
Returns
-------
list
List of Rest objects
"""
return [e for e in self.iter_all(Rest, include_subclasses=False)]
@property
def repeats(self):
"""Return a list of all Repeat objects in the part
Returns
-------
list
List of Repeat objects
"""
return [e for e in self.iter_all(Repeat, include_subclasses=False)]
@property
def key_sigs(self):
"""Return a list of all Key Signature objects in the part
Returns
-------
list
List of Key Signature objects
"""
return [e for e in self.iter_all(KeySignature, include_subclasses=False)]
@property
def time_sigs(self):
"""Return a list of all Time Signature objects in the part
Returns
-------
list
List of Time Signature objects
"""
return [e for e in self.iter_all(TimeSignature, include_subclasses=False)]
@property
def dynamics(self):
"""Return a list of all Dynamics markings in the part
Returns
-------
list
List of Dynamics objects
"""
return [e for e in self.iter_all(LoudnessDirection, include_subclasses=True)]
@property
def tempo_directions(self):
"""Return a list of all tempo direction in the part
Returns
-------
list
List of TempoDirection objects
"""
return [e for e in self.iter_all(TempoDirection, include_subclasses=True)]
@property
def articulations(self):
"""Return a list of all Articulation markings in the part
Returns
-------
list
List of Articulation objects
"""
return [
e for e in self.iter_all(ArticulationDirection, include_subclasses=True)
]
@property
def segments(self):
"""Return a list of all segments in the part
Returns
-------
list
List of Segment objects
"""
add_segments(self)
return [e for e in self.iter_all(Segment, include_subclasses=False)]
[docs] def quarter_durations(self, start=None, end=None):
"""Return an Nx2 array with quarter duration (second column)
and their respective times (first column).
When a start and or end time is specified, the returned
array will contain only the entries within those bounds.
Parameters
----------
start : number, optional
Start of range
end : number, optional
End of range
Returns
-------
ndarray
An array with quarter durations and times
"""
qd = np.column_stack((self._quarter_times, self._quarter_durations))
if start is not None:
qd = qd[qd[:, 0] >= start, :]
if end is not None:
qd = qd[qd[:, 0] < end, :]
return qd
@property
def quarter_duration_map(self):
"""A function mapping timeline times to quarter durations in
effect at those times. The function can take scalar values or
lists/arrays of values.
Returns
-------
function
The mapping function
"""
x = self._quarter_times
y = self._quarter_durations
if len(x) == 1:
x = x + x
y = y + y
return interp1d(
x, y, kind="previous", bounds_error=False, fill_value=(y[0], y[-1])
)
[docs] def set_quarter_duration(self, t, quarter):
"""Set the duration of a quarter note from timepoint `t`
onwards.
Setting the quarter note duration defines how intervals
between timepoints are related to musical durations. For
example when two timepoints `t1` and `t2` have associated
times 10 and 20 respecively, then the interval between `t1`
and `t2` corresponds to a half note when the quarter duration
equals 5 during that interval.
The quarter duration can vary throughout the part. When
setting a quarter duration at time t, then that value takes
effect until the time of the next quarter duration. If a
different quarter duration was already set at time t, it wil
be replaced.
Note setting the quarter duration does not change the
timepoints, only the relation to musical time. For
illustration: in the example above, when changing the current
quarter duration from 5 to 10, a note that starts at `t1` and
ends at `t2` will change from being a half note to being a
quarter note.
Parameters
----------
t : int
Time at which to set the quarter duration
quarter : int
The quarter duration
"""
# add quarter duration at time t, unless it is redundant. If another
# quarter duration is at t, replace it.
# shorthand
times = self._quarter_times
quarters = self._quarter_durations
i = np.searchsorted(times, t)
changed = False
if i == 0 or quarters[i - 1] != quarter:
# add or replace
if i == len(times) or times[i] != t:
# add
times.insert(i, t)
quarters.insert(i, quarter)
changed = True
elif quarters[i] != quarter:
# replace
quarters[i] = quarter
changed = True
else:
# times[i] == t, quarters[i] == quarter
pass
if not changed:
return
if i + 1 == len(times):
t_next = np.inf
else:
t_next = times[i + 1]
# update quarter attribute of all timepoints in the range [t, t_next]
start_idx = np.searchsorted(self._points, TimePoint(t))
end_idx = np.searchsorted(self._points, TimePoint(t_next))
for tp in self._points[start_idx:end_idx]:
tp.quarter = quarter
# update the interpolation function
self._quarter_map = self.quarter_duration_map
def _add_point(self, tp):
# Add `TimePoint` object `tp` to the part, unless there is
# already a timepoint at the same time.
i = np.searchsorted(self._points, tp)
if i == len(self._points) or self._points[i].t != tp.t:
self._points = np.insert(self._points, i, tp)
if i > 0:
self._points[i - 1].next = self._points[i]
self._points[i].prev = self._points[i - 1]
if i < len(self._points) - 1:
self._points[i].next = self._points[i + 1]
self._points[i + 1].prev = self._points[i]
@property
def number_of_staves(self):
if self._number_of_staves is not None:
return self._number_of_staves
else:
return self.compute_number_of_staves()
def compute_number_of_staves(self):
max_staves = 1
for e in self.iter_all(GenericNote, include_subclasses=True):
if e.staff is not None and e.staff > max_staves:
max_staves = e.staff
for e in self.iter_all(Clef):
if e.staff is not None and e.staff > max_staves:
max_staves = e.staff
for e in self.iter_all(Direction, include_subclasses=True):
if e.staff is not None and e.staff > max_staves:
max_staves = e.staff
for e in self.iter_all(Words):
if e.staff is not None and e.staff > max_staves:
max_staves = e.staff
self._number_of_staves = max_staves
return max_staves
def _remove_point(self, tp):
i = np.searchsorted(self._points, tp)
if self._points[i] == tp:
self._points = np.delete(self._points, i)
if i > 0:
self._points[i - 1].next = self._points[i]
self._points[i].prev = self._points[i - 1]
if i < len(self._points) - 1:
self._points[i].next = self._points[i + 1]
self._points[i + 1].prev = self._points[i]
[docs] def get_point(self, t):
"""Return the `TimePoint` object with time `t`, or None if
there is no such object.
"""
if t < 0:
raise InvalidTimePointException(
"TimePoints should have non-negative integer values"
)
i = np.searchsorted(self._points, TimePoint(t))
if i < len(self._points) and self._points[i].t == t:
return self._points[i]
else:
return None
[docs] def get_or_add_point(self, t):
"""Return the `TimePoint` object with time `t`; if there is no
such object, create it, add it to the time line, and return
it.
Parameters
----------
t : int
Time value `t`
Returns
-------
:class:`TimePoint`
a TimePoint object with time `t`
"""
if t < 0:
raise InvalidTimePointException(
"TimePoints should have non-negative integer values"
)
tp = self.get_point(t)
if tp is None:
tp = TimePoint(t, int(self._quarter_map(t)))
self._add_point(tp)
return tp
[docs] def add(self, o, start=None, end=None):
"""Add an object to the timeline.
An object can be added by start time, end time, or both,
depending on which of the `start` and `end` keywords are
provided. If neither is provided this method does nothing.
`start` and `end` should be non-negative integers.
Parameters
----------
o : :class:`TimedObject`
Object to be removed
start : int, optional
The start time of the object
end : int, optional
The end time of the object
"""
if start is not None:
if start < 0:
raise InvalidTimePointException(
"TimePoints should have non-negative integer values"
)
self.get_or_add_point(start).add_starting_object(o)
if end is not None:
if end < 0:
raise InvalidTimePointException(
"TimePoints should have non-negative integer values"
)
self.get_or_add_point(end).add_ending_object(o)
[docs] def remove(self, o, which="both"):
"""Remove an object from the timeline.
An object can be removed by start time, end time, or both.
Parameters
----------
o : :class:`TimedObject`
Object to be removed
which : {'start', 'end', 'both'}, optional
Whether to remove o as a starting object, an ending
object, or both. Defaults to 'both'.
"""
if which in ("start", "both") and o.start:
try:
o.start.starting_objects[o.__class__].remove(o)
except (KeyError, ValueError):
raise Exception(
"Not implemented: removing an object "
"that is registered by its superclass"
)
# cleanup timepoint if no starting/ending objects are left
self._cleanup_point(o.start)
o.start = None
if which in ("end", "both") and o.end:
try:
o.end.ending_objects[o.__class__].remove(o)
except (KeyError, ValueError):
raise Exception(
"Not implemented: removing an object "
"that is registered by its superclass"
)
# cleanup timepoint if no starting/ending objects are left
self._cleanup_point(o.end)
o.end = None
def _cleanup_point(self, tp):
# remove tp when it has no starting or ending objects
if (
sum(len(oo) for oo in tp.starting_objects.values())
+ sum(len(oo) for oo in tp.ending_objects.values())
) == 0:
self._remove_point(tp)
[docs] def iter_all(
self, cls=None, start=None, end=None, include_subclasses=False, mode="starting"
):
"""Iterate (in direction of increasing time) over all
instances of `cls` that either start or end (depending on
`mode`) in the interval `start` to `end`. When `start` and
`end` are omitted, the whole timeline is searched.
Parameters
----------
cls : class, optional
The class of objects to iterate over. If omitted, iterate
over all objects in the part.
start : :class:`TimePoint`, optional
The start of the interval to search. If omitted or None,
the search starts at the start of the timeline. Defaults
to None.
end : :class:`TimePoint`, optional
The end of the interval to search. If omitted or None, the
search ends at the end of the timeline. Defaults to None.
include_subclasses : bool, optional
If True also return instances that are subclasses of
`cls`. Defaults to False.
mode : {'starting', 'ending'}, optional
Flag indicating whether to search for starting or ending
objects. Defaults to 'starting'.
Yields
------
object
Instances of the specified type.
"""
if mode not in ("starting", "ending"):
warnings.warn('unknown mode "{}", using "starting" instead'.format(mode))
mode = "starting"
if start is None:
start_idx = 0
else:
if not isinstance(start, TimePoint):
start = TimePoint(start)
start_idx = np.searchsorted(self._points, start)
if end is None:
end_idx = len(self._points)
else:
if not isinstance(end, TimePoint):
end = TimePoint(end)
end_idx = np.searchsorted(self._points, end)
if cls is None:
cls = object
include_subclasses = True
if mode == "ending":
for tp in self._points[start_idx:end_idx]:
yield from tp.iter_ending(cls, include_subclasses)
else:
for tp in self._points[start_idx:end_idx]:
yield from tp.iter_starting(cls, include_subclasses)
[docs] def apply(self):
"""Apply all changes to the timeline for objects like octave Shift."""
pass
@property
def last_point(self):
"""The last TimePoint on the timeline, or None if the timeline
is empty.
Returns
-------
:class:`TimePoint`
"""
return self._points[-1] if len(self._points) > 0 else None
@property
def first_point(self):
"""The first TimePoint on the timeline, or None if the
timeline is empty.
Returns
-------
:class:`TimePoint`
"""
return self._points[0] if len(self._points) > 0 else None
[docs] def note_array(self, **kwargs):
"""
Create a structured array with note information
from a `Part` object.
Parameters
----------
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
note. Default is False
include_key_signature : bool (optional)
If `True`, includes key signature information, i.e.,
the key signature at the onset time of each note (all
notes starting at the same time have the same key signature).
Default is False
include_time_signature : bool (optional)
If `True`, includes time signature information, i.e.,
the time signature at the onset time of each note (all
notes starting at the same time have the same time signature).
Default is False
include_metrical_position : bool (optional)
If `True`, includes metrical position information, i.e.,
the position of the onset time of each note with respect to its
measure (all notes starting at the same time have the same metrical
position).
Default is False
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. if a note is a
grace note and the grace type "" for non grace notes).
Default is False
include_divs_per_quarter : bool (optional)
If `True`, includes the number of divs per quarter note.
Default is False
Returns:
note_array : structured array
"""
return note_array_from_part(self, **kwargs)
[docs] def rest_array(
self,
include_pitch_spelling=False,
include_key_signature=False,
include_time_signature=False,
include_metrical_position=False,
include_grace_notes=False,
include_staff=False,
collapse=False,
):
"""
Create a structured array with rest information
from a `Part` object.
Parameters
----------
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
rest, i.e. all information is 0. Default is False
include_key_signature : bool (optional)
If `True`, includes key signature information, i.e.,
the key signature at the onset time of each rest (all
notes starting at the same time have the same key signature).
Default is False
include_time_signature : bool (optional)
If `True`, includes time signature information, i.e.,
the time signature at the onset time of each note (all
notes starting at the same time have the same time signature).
Default is False
include_metrical_position : bool (optional)
If `True`, includes metrical position information, i.e.,
the position of the onset time of each rest with respect to its
measure (all notes starting at the same time have the same metrical
position).
Default is False
include_grace_notes : bool (optional)
If `True`, includes returns empty strings as type and false.
feature_functions : list or str
A list of feature functions. Elements of the list can be either
the functions themselves or the names of a feature function as
strings (or a mix). The feature functions specified by name are
looked up in the `featuremixer.featurefunctions` module.
Returns:
rest_array : structured array
"""
return rest_array_from_part(
self,
include_pitch_spelling=include_pitch_spelling,
include_key_signature=include_key_signature,
include_time_signature=include_time_signature,
include_metrical_position=include_metrical_position,
include_grace_notes=include_grace_notes,
include_staff=include_staff,
collapse=collapse,
)
[docs] def set_musical_beat_per_ts(self, mbeats_per_ts={}):
"""Set the number of musical beats for each time signature.
If no musical beat is specified for a certain time signature,
the default one is used, i.e. 2 for 6/X, 3 for 9/X, 4 for 12/X,
and the number of beats for the others ts. Each musical beat
has equal duration.
Parameters
----------
mbeats_per_ts : dict, optional
A dict where the keys are time signature strings
(e.g. "3/4") and the values are the number of musical beats.
If a certain time signature is not specified, the defaults
values are used.
Defaults to an empty dict.
"""
if not isinstance(mbeats_per_ts, dict):
raise TypeError("mbeats_per_ts must be either a dictionary")
# correctly set the musical beat for all time signatures
for ts in self.iter_all(TimeSignature):
ts_string = "{}/{}".format(ts.beats, ts.beat_type)
if ts_string in mbeats_per_ts:
ts.musical_beats = mbeats_per_ts[ts_string]
else: # set to default if not specified
if ts.beats in MUSICAL_BEATS:
ts.musical_beats = MUSICAL_BEATS[ts.beats]
else:
ts.musical_beats = ts.beats
[docs] def use_musical_beat(self, mbeats_per_ts={}):
"""Consider the musical beat as the reference for all elements
that concern the number and position of beats.
An optional parameter can set the number of musical beats for
specific time signatures, otherwise the default values are
used.
Parameters
----------
mbeats_per_ts : dict, optional
A dict where the keys are time signature strings
(e.g. "3/4") and the values are the number of musical beats.
If a certain time signature is not specified, the defaults
values are used.
Defaults to an empty dict.
"""
if not self._use_musical_beat:
self._use_musical_beat = True
if mbeats_per_ts != {}: # set the number of nbeats if specified
self.set_musical_beat_per_ts(mbeats_per_ts)
else:
warnings.warn("Musical beats were already being used!")
[docs] def use_notated_beat(self):
"""Consider the notated beat (numerator of time signature)
as the reference for all elements that concern the number
and position of beats.
It also reset the number of musical beats for each time signature
to default values.
"""
if self._use_musical_beat:
self._use_musical_beat = False
# reset the number of musical beats to default values
self.set_musical_beat_per_ts()
else:
warnings.warn("Notated beats were already being used!")
# @property
# def part_names(self):
# # get instrument name parts recursively
# chunks = []
# if self.part_name is not None:
# chunks.append(self.part_name)
# yield self.part_name
# pg = self.parent
# while pg is not None:
# if pg.group_name is not None:
# chunks.insert(0, pg.group_name)
# yield ' '.join(chunks)
# pg = pg.parent
[docs]class TimePoint(ComparableMixin):
"""A TimePoint represents a temporal position within a
:class:`Part`.
TimePoints are used to keep track of the starting and ending of
musical elements in the part. They are created automatically when
adding musical elements to a part using its :meth:`~Part.add`
method, so there should be normally no reason to instantiate
TimePoints manually.
Parameters
----------
t : int
The time associated to this TimePoint. Should be a non-
negative integer.
quarter : int
The duration of a quarter note at this TimePoint
Attributes
----------
t : int
See parameters
quarter : int
See parameters
starting_objects : dictionary
A dictionary where the musical objects starting at this time
are grouped by class.
ending_objects : dictionary
A dictionary where the musical objects ending at this time are
grouped by class.
prev : TimePoint
The preceding TimePoint (or None if there is none)
next : TimePoint
The succeding TimePoint (or None if there is none)
"""
def __init__(self, t, quarter=None):
self.t = t
self.quarter = quarter
self.starting_objects = defaultdict(_OrderedSet)
self.ending_objects = defaultdict(_OrderedSet)
# prev and next are dynamically updated once the timepoint is part of a timeline
self.next = None
self.prev = None
def __iadd__(self, value):
assert isinstance(value, Number)
self.t += value
return self
def __isub__(self, value):
assert isinstance(value, Number)
self.t -= value
return self
def __add__(self, value):
assert isinstance(value, Number)
new = copy(self)
new += value
return new
def __sub__(self, value):
assert isinstance(value, Number)
new = copy(self)
new -= value
return new
def __str__(self):
return "TimePoint t={} quarter={}".format(self.t, self.quarter)
[docs] def add_starting_object(self, obj):
"""Add object `obj` to the list of starting objects."""
obj.start = self
self.starting_objects[type(obj)].add(obj)
[docs] def remove_starting_object(self, obj):
"""Remove object `obj` from the list of starting objects."""
# TODO: check if object is stored under a superclass
obj.start = None
if type(obj) in self.starting_objects:
try:
self.starting_objects[type(obj)].remove(obj)
except ValueError:
# don't complain if the object isn't in starting_objects
pass
[docs] def remove_ending_object(self, obj):
"""Remove object `obj` from the list of ending objects."""
# TODO: check if object is stored under a superclass
obj.end = None
if type(obj) in self.ending_objects:
try:
self.ending_objects[type(obj)].remove(obj)
except ValueError:
# don't complain if the object isn't in ending_objects
pass
[docs] def add_ending_object(self, obj):
"""Add object `obj` to the list of ending objects."""
obj.end = self
self.ending_objects[type(obj)].add(obj)
[docs] def iter_starting(self, cls, include_subclasses=False):
"""Iterate over all objects of type `cls` that start at this
time point.
Parameters
----------
cls : class
The type of objects to iterate over
include_subclasses : bool, optional
When True, include all objects of all subclasses of `cls`
in the iteration. Defaults to False.
Yields
-------
cls
Instance of type `cls`
"""
yield from self.starting_objects[cls]
if include_subclasses:
for subcls in iter_subclasses(cls):
yield from self.starting_objects[subcls]
[docs] def iter_ending(self, cls, include_subclasses=False):
"""Iterate over all objects of type `cls` that end at this
time point.
Parameters
----------
cls : class
The type of objects to iterate over
include_subclasses : bool, optional
When True, include all objects of all subclasses of `cls`
in the iteration. Defaults to False.
Yields
------
cls
Instance of type `cls`
"""
yield from self.ending_objects[cls]
if include_subclasses:
for subcls in iter_subclasses(cls):
yield from self.ending_objects[subcls]
[docs] def iter_prev(self, cls, eq=False, include_subclasses=False):
"""Iterate backwards in time from the current timepoint over
starting object(s) of type `cls`.
Parameters
----------
cls : class
Class of objects to iterate over
eq : bool, optional
If True start iterating at the current timepoint, rather
than its predecessor. Defaults to False.
include_subclasses : bool, optional
If True include subclasses of `cls` in the iteration.
Defaults to False.
Yields
------
cls
Instances of `cls`
"""
if eq:
tp = self
else:
tp = self.prev
while tp:
yield from tp.iter_starting(cls, include_subclasses)
tp = tp.prev
[docs] def iter_next(self, cls, eq=False, include_subclasses=False):
"""Iterate forwards in time from the current timepoint over
starting object(s) of type `cls`.
Parameters
----------
cls : class
Class of objects to iterate over
eq : bool, optional
If True start iterating at the current timepoint, rather
than its successor. Defaults to False.
include_subclasses : bool, optional
If True include subclasses of `cls` in the iteration.
Defaults to False.
Yields
------
cls
Instances of `cls`
"""
if eq:
tp = self
else:
tp = self.next
while tp:
yield from tp.iter_starting(cls, include_subclasses)
tp = tp.next
def _cmpkey(self):
# This method returns the value to be compared (code for that is in
# the ComparableMixin class)
return self.t
def _pp(self, tree):
# pretty print the timepoint, including its starting and ending
# objects
result = ["{}{}".format(tree, self.__str__())]
tree.push()
ending_items_lists = sorted_dict_items(
self.ending_objects.items(), key=lambda x: x[0].__name__
)
starting_items_lists = sorted_dict_items(
self.starting_objects.items(), key=lambda x: x[0].__name__
)
ending_items = [
o
for _, oo in ending_items_lists
for o in sorted(oo, key=lambda x: x.duration or -1, reverse=True)
]
starting_items = [
o
for _, oo in starting_items_lists
for o in sorted(oo, key=lambda x: x.duration or -1)
]
if ending_items:
result.append("{}".format(tree).rstrip())
if starting_items:
tree.next_item()
else:
tree.last_item()
result.append("{}ending objects".format(tree))
tree.push()
result.append("{}".format(tree).rstrip())
for i, item in enumerate(ending_items):
if i == (len(ending_items) - 1):
tree.last_item()
else:
tree.next_item()
result.append("{}{}".format(tree, item))
tree.pop()
if starting_items:
result.append("{}".format(tree).rstrip())
tree.last_item()
result.append("{}starting objects".format(tree))
tree.push()
result.append("{}".format(tree).rstrip())
for i, item in enumerate(starting_items):
if i == (len(starting_items) - 1):
tree.last_item()
else:
tree.next_item()
result.append("{}{}".format(tree, item))
tree.pop()
tree.pop()
return result
[docs]class TimedObject(ReplaceRefMixin):
"""This is the base class of all classes that have a start and end
point. The start and end attributes initialized to None, and are
set/unset when the object is added to/removed from a Part, using
its :meth:`~Part.add` and :meth:`~Part.remove` methods,
respectively.
Attributes
----------
start : :class:`TimePoint`
Start time of the object
end : :class:`TimePoint`
End time of the object
"""
def __init__(self):
super().__init__()
self.start = None
self.end = None
def __str__(self):
start = "" if self.start is None else f"{self.start.t}"
end = "" if self.end is None else f"{self.end.t}"
return start + "--" + end + " " + type(self).__name__
@property
def duration(self):
"""The duration of the timed object in divisions. When either
the start or the end property of the object are None, the
duration is None.
Returns
-------
int or None
"""
if self.start is None or self.end is None:
return None
else:
return self.end.t - self.start.t
[docs]class GenericNote(TimedObject):
"""Represents the common aspects of notes, rests, and unpitched
notes.
Parameters
----------
id : str, optional (default: None)
A string identifying the note. To be compatible with the
MusicXML format, the id must be unique within a part and must
not start with a number.
voice : int, optional
An integer representing the voice to which the note belongs.
Defaults to None.
staff : str, optional
An integer representing the staff to which the note belongs.
Defaults to None.
doc_order : int, optional
The document order index (zero-based), expressing the order of
appearance of this note (with respect to other notes) in the
document in case the Note belongs to a part that was imported
from MusicXML. Defaults to None.
"""
def __init__(
self,
id=None,
voice=None,
staff=None,
symbolic_duration=None,
articulations=None,
ornaments=None,
doc_order=None,
):
self._sym_dur = None
super().__init__()
self.voice = voice
self.id = id
self.staff = staff
self.symbolic_duration = symbolic_duration
self.articulations = articulations
self.ornaments = ornaments
self.doc_order = doc_order
# these attributes are set after the instance is constructed
self.fermata = None
self.tie_prev = None
self.tie_next = None
self.slur_stops = []
self.slur_starts = []
self.tuplet_stops = []
self.tuplet_starts = []
# maintain a list of attributes to update when cloning this instance
self._ref_attrs.extend(
[
"tie_prev",
"tie_next",
"slur_stops",
"slur_starts",
"tuplet_stops",
"tuplet_starts",
]
)
@property
def symbolic_duration(self):
"""The symbolic duration of the note.
This property returns a dictionary specifying the symbolic
duration of the note. The dictionary may have the following
keys:
* type : the note type as a string, e.g. 'quarter', 'half'
* dots : an integer specifying the number of dots. When
this key is missing it means there are no dots.
* actual_notes : Specifies the number of actual notes in a
rhythmical tuplet. Used in conjunction with `normal_notes`.
* normal_notes : Specifies the normal number of notes in a
rhythmical tuplet. For example a triplet of eights in the
time of two eights would correspond to actual_notes=3,
normal_notes=2.
The symbolic duration dictionary of a note can either be
set manually (for example by specifying the
`symbolic_duration` constructor keyword argument), or left
unspecified (i.e. None). In the latter case the symbolic
duration is estimated dynamically based on the note start and
end times. Note that this latter case is generally preferrable
because it ensures that the symbolic duration is consistent
with the numeric duration.
If the symbolic duration cannot be estimated from the
numeric duration None is returned.
Returns
-------
dict or None
A dictionary specifying the symbolic duration of the note, or
None if the symbolic duration could not be estimated from the
numeric duration.
"""
if self._sym_dur is None:
# compute value
if not self.start or not self.end:
warnings.warn(
"Cannot estimate symbolic duration for notes that "
"are not added to a Part"
)
return None
if self.start.quarter is None:
warnings.warn(
"Cannot estimate symbolic duration when not "
"quarter_duration has been set. "
"See Part.set_quarter_duration."
)
return None
return estimate_symbolic_duration(self.duration, self.start.quarter)
else:
# return set value
return self._sym_dur
@symbolic_duration.setter
def symbolic_duration(self, v):
self._sym_dur = v
@property
def end_tied(self):
"""The `Timepoint` corresponding to the end of the note, or---
when this note belongs to a group of tied notes---the end of
the last note in the group.
Returns
-------
TimePoint
End of note
"""
if self.tie_next is None:
return self.end
else:
return self.tie_next.end_tied
@property
def duration_tied(self):
"""Time difference of the start of the note to the end of the
note, or---when this note belongs to a group of tied notes---
the end of the last note in the group.
Returns
-------
int
Duration of note
"""
if self.tie_next is None:
return self.duration
else:
return self.duration + self.tie_next.duration_tied
@property
def duration_from_symbolic(self):
"""Return the numeric duration given the symbolic duration of
the note and the quarter_duration in effect.
Returns
-------
int or None
"""
if self.symbolic_duration:
# check for self.start, and self.start.quarter
return symbolic_to_numeric_duration(
self.symbolic_duration, self.start.quarter
)
else:
return None
@property
def tie_prev_notes(self):
"""TODO
Parameters
----------
Returns
-------
type
Description of return value
"""
if self.tie_prev:
return self.tie_prev.tie_prev_notes + [self.tie_prev]
else:
return []
@property
def tie_next_notes(self):
"""TODO
Parameters
----------
Returns
-------
type
Description of return value
"""
if self.tie_next:
return [self.tie_next] + self.tie_next.tie_next_notes
else:
return []
# def iter_voice_prev(self):
# """TODO
# Parameters
# ----------
# Returns
# -------
# type
# Description of return value
# """
# for n in self.start.iter_prev(GenericNote, include_subclasses=True):
# if n.voice == n.voice:
# yield n
# def iter_voice_next(self):
# """TODO
# Parameters
# ----------
# Returns
# -------
# type
# Description of return value
# """
# for n in self.start.iter_next(GenericNote, include_subclasses=True):
# if n.voice == n.voice:
# yield n
[docs] def iter_chord(self, same_duration=True, same_voice=True):
"""Iterate over notes with coinciding start times.
Parameters
----------
same_duration : bool, optional
When True limit the iteration to notes that have the same
duration as the current note. Defaults to True.
same_voice : bool, optional
When True limit the iteration to notes that have the same
voice as the current note. Defaults to True.
Yields
------
GenericNote
"""
for n in self.start.iter_starting(GenericNote, include_subclasses=True):
if ((not same_voice) or n.voice == self.voice) and (
(not same_duration) or (n.duration == self.duration)
):
yield n
def __str__(self):
s = "{} id={} voice={} staff={} type={}".format(
super().__str__(),
self.id,
self.voice,
self.staff,
format_symbolic_duration(self.symbolic_duration),
)
if self.articulations:
s += " articulations=({})".format(", ".join(self.articulations))
if self.tie_prev or self.tie_next:
all_tied = self.tie_prev_notes + [self] + self.tie_next_notes
tied_id = "+".join(n.id or "None" for n in all_tied)
return s + " tie_group={}".format(tied_id)
else:
return s
[docs]class Note(GenericNote):
"""Subclass of GenericNote representing pitched notes.
Parameters
----------
step : {'C', 'D', 'E', 'F', 'G', 'A', 'B'}
The note name of the pitch (in upper case). If a lower case
note name is given, it will be converted to upper case.
octave : int
An integer representing the octave of the pitch
alter : int, optional
An integer (or None) representing the alteration of the pitch as
follows:
-2
double flat
-1
flat
0 or None
unaltered
1
sharp
2
double sharp
Defaults to None.
"""
def __init__(self, step, octave, alter=None, beam=None, **kwargs):
super().__init__(**kwargs)
self.step = step.upper()
self.octave = octave
self.alter = alter
self.beam = beam
if self.beam is not None:
self.beam.append(self)
def __str__(self):
return " ".join(
(
super().__str__(),
"pitch={}{}{}".format(self.step, self.alter_sign, self.octave),
)
)
@property
def midi_pitch(self):
"""The midi pitch value of the note (MIDI note number). C4
(middle C, in german: c') is note number 60.
Returns
-------
integer
The note's pitch as MIDI note number.
"""
return pitch_spelling_to_midi_pitch(
step=self.step, octave=self.octave, alter=self.alter
)
@property
def alter_sign(self):
"""The alteration of the note
Returns
-------
str
"""
return ALTER_SIGNS[self.alter]
[docs]class UnpitchedNote(GenericNote):
"""Subclass of GenericNote representing unpitched notes.
Parameters
----------
Parameters
----------
step : {'C', 'D', 'E', 'F', 'G', 'A', 'B'}
The note name of the pitch (in upper case). If a lower case
note name is given, it will be converted to upper case.
octave : int
An integer representing the octave of the pitch
notehead : string
A string representing the notehead.
Defaults to None
noteheadstyle : bool
A boolean indicating whether the notehead is filled.
Defaults to true
"""
def __init__(
self, step, octave, beam=None, notehead=None, noteheadstyle=True, **kwargs
):
super().__init__(**kwargs)
self.step = step.upper()
self.octave = octave
self.beam = beam
self.notehead = notehead
self.noteheadstyle = noteheadstyle
if self.beam is not None:
self.beam.append(self)
def __str__(self):
return " ".join(
(
super().__str__(),
"pitch={}{}{}".format(self.step, "", self.octave),
)
)
@property
def midi_pitch(self):
"""The midi pitch value of the note (MIDI note number).
Returns
-------
integer
The note's position as MIDI note number.
"""
return pitch_spelling_to_midi_pitch(step=self.step, octave=self.octave, alter=0)
[docs]class Rest(GenericNote):
"""A subclass of GenericNote representing a rest."""
def __init__(self, hidden=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.hidden = hidden
[docs]class Beam(TimedObject):
"""Represent beams (for MEI)"""
def __init__(self, id=None):
super().__init__()
self.id = id
self.notes = []
def append(self, note):
note.beam = self
self.notes.append(note)
self.update_time()
def update_time(self):
start_idx = np.argmin([n.start.t for n in self.notes])
end_idx = np.argmax([n.end.t for n in self.notes])
self.start = self.notes[start_idx].start
self.end = self.notes[end_idx].end
[docs]class GraceNote(Note):
"""A subclass of Note representing a grace note.
Parameters
----------
grace_type : {'grace', 'acciaccatura', 'appoggiatura'}
The type of grace note. Use 'grace' for a unspecified grace
note type.
steal_proportion : float, optional
The proportion of the previous (acciaccatura) or next
(appoggiatura) note duration that is occupied by the grace
note. Defaults to None.
Attributes
----------
main_note : :class:`Note`
The (non-grace) note to which this grace note belongs.
grace_seq_len : list
The length of the sequence of grace notes to which this grace
note belongs.
"""
def __init__(self, grace_type, *args, steal_proportion=None, **kwargs):
super().__init__(*args, **kwargs)
self.grace_type = grace_type
self.steal_proportion = steal_proportion
self.grace_next = None
self.grace_prev = None
self._ref_attrs.extend(["grace_next", "grace_prev"])
@property
def main_note(self):
n = self.grace_next
while isinstance(n, GraceNote):
n = n.grace_next
return n
@property
def grace_seq_len(self):
return (
sum(1 for _ in self.iter_grace_seq(backwards=True))
+ sum(1 for _ in self.iter_grace_seq())
- 1
) # subtract one because self is counted twice
@property
def last_grace_note_in_seq(self):
n = self
while isinstance(n.grace_next, GraceNote):
n = n.grace_next
return n
[docs] def iter_grace_seq(self, backwards=False):
"""Iterate over this and all subsequent/preceding grace notes,
excluding the main note.
Parameters
----------
backwards : bool, optional
When True, iterate over preceding grace notes. Otherwise
iterate over subsequent grace notes. Defaults to False.
Yields
------
GraceNote
"""
yield self
if backwards:
n = self.grace_prev
else:
n = self.grace_next
while isinstance(n, GraceNote):
yield n
if backwards:
n = n.grace_prev
else:
n = n.grace_next
def __str__(self):
return f"{super().__str__()} main_note={self.main_note}"
[docs]class Page(TimedObject):
"""A page in a musical score. Its start and end times describe the
range of musical time that is spanned by the page.
Parameters
----------
number : int, optional
The number of the system. Defaults to 0.
Attributes
----------
number : int
See parameters
"""
def __init__(self, number=0):
super().__init__()
self.number = number
def __str__(self):
return f"{super().__str__()} number={self.number}"
[docs]class System(TimedObject):
"""A system in a musical score. Its start and end times describe
the range of musical time that is spanned by the system.
Parameters
----------
number : int, optional
The number of the system. Defaults to 0.
Attributes
----------
number : int
See parameters
"""
def __init__(self, number=0):
super().__init__()
self.number = number
def __str__(self):
return f"{super().__str__()} number={self.number}"
[docs]class Clef(TimedObject):
"""Clefs associate the lines of a staff to musical pitches.
Parameters
----------
staff : int, optional
The number of the staff to which this clef belongs.
sign : {'G', 'F', 'C', 'percussion', 'TAB', 'jianpu', 'none'}
The sign of the clef
line : int
The staff line at which the sign is positioned
octave_change : int
The number of octaves to shift the pitches up (postive) or
down (negative)
Attributes
----------
staff : int
See parameters
sign : {'G', 'F', 'C', 'percussion', 'TAB', 'jianpu', 'none'}
See parameters
line : int
See parameters
octave_change : int
See parameters
"""
def __init__(self, staff, sign, line, octave_change):
super().__init__()
self.staff = staff
self.sign = sign
self.line = line
self.octave_change = octave_change
def __str__(self):
return (
f"{super().__str__()} sign={self.sign} "
f"line={self.line} number={self.staff}"
)
[docs]class Slur(TimedObject):
"""Slurs indicate musical grouping across notes.
Parameters
----------
start_note : :class:`Note`, optional
The note at which this slur starts. Defaults to None.
end_note : :class:`Note`, optional
The note at which this slur ends. Defaults to None.
Attributes
----------
start_note : :class:`Note` or None
See parameters
end_note : :class:`Note` or None
See parameters
"""
def __init__(self, start_note=None, end_note=None):
super().__init__()
self._start_note = None
self._end_note = None
self.start_note = start_note
self.end_note = end_note
# maintain a list of attributes to update when cloning this instance
self._ref_attrs.extend(["start_note", "end_note"])
@property
def start_note(self):
return self._start_note
@start_note.setter
def start_note(self, note):
# make sure we received a note
if note:
if self.start:
# remove the slur from the current start time
self.start.remove_starting_object(self)
note.slur_starts.append(self)
self._start_note = note
@property
def end_note(self):
return self._end_note
@end_note.setter
def end_note(self, note):
# make sure we received a note
if note:
if self.end:
# remove the slur from the current end time
self.end.remove_ending_object(self)
if note.end:
# add it to the end time of the new end note
note.end.add_ending_object(self)
note.slur_stops.append(self)
self._end_note = note
def __str__(self):
start = "" if self.start_note is None else "start={}".format(self.start_note.id)
end = "" if self.end_note is None else "end={}".format(self.end_note.id)
return " ".join((super().__str__(), start, end)).strip()
[docs]class Tuplet(TimedObject):
"""Tuplets indicate musical grouping across notes.
Parameters
----------
start_note : :class:`Note`, optional
The note at which this tuplet starts. Defaults to None.
end_note : :class:`Note`, optional
The note at which this tuplet ends. Defaults to None.
Attributes
----------
start_note : :class:`Note` or None
See parameters
end_note : :class:`Note` or None
See parameters
"""
def __init__(self, start_note=None, end_note=None):
super().__init__()
self._start_note = None
self._end_note = None
self.start_note = start_note
self.end_note = end_note
# maintain a list of attributes to update when cloning this instance
self._ref_attrs.extend(["start_note", "end_note"])
@property
def start_note(self):
return self._start_note
@start_note.setter
def start_note(self, note):
# make sure we received a note
if note:
if note.start:
# remove the tuplet from the current start time
if self.start_note and self.start_note.start:
self.start_note.start.remove_starting_object(self)
# else:
# warnings.warn('Note has no start time')
note.tuplet_starts.append(self)
self._start_note = note
@property
def end_note(self):
return self._end_note
@end_note.setter
def end_note(self, note):
# make sure we received a note
if note:
if note.end:
if self.end_note and self.end_note.end:
# remove the tuplet from the currentend time
self.end_note.end.remove_ending_object(self)
# else:
# warnings.warn('Note has no end time')
note.tuplet_stops.append(self)
self._end_note = note
def __str__(self):
start = "" if self.start_note is None else "start={}".format(self.start_note.id)
end = "" if self.end_note is None else "end={}".format(self.end_note.id)
return " ".join((super().__str__(), start, end)).strip()
[docs]class Repeat(TimedObject):
"""Repeats represent a repeated section in the score, designated
by its start and end times.
"""
def __init__(self):
super().__init__()
[docs]class DaCapo(TimedObject):
"""A Da Capo sign."""
[docs]class Fine(TimedObject):
"""A Fine sign."""
[docs]class DalSegno(TimedObject):
"""A Dal Segno sign."""
[docs]class Segno(TimedObject):
"""A Segno sign."""
[docs]class ToCoda(TimedObject):
"""A To Coda sign."""
[docs]class Coda(TimedObject):
"""A Coda sign."""
[docs]class Fermata(TimedObject):
"""A Fermata sign.
Parameters
----------
ref : :class:`TimedObject` or None, optional
An object to which this fermata applies. In practice this is a
Note or a Barline. Defaults to None.
Attributes
----------
ref : :class:`TimedObject` or None
See parameters
"""
def __init__(self, ref=None):
super().__init__()
# ref(erent) can be a note or a barline
self.ref = ref
def __str__(self):
return f"{super().__str__()} ref={self.ref}"
[docs]class Ending(TimedObject):
"""Class that represents one part of a 1---2--- type ending of a
musical passage (a.k.a Volta brackets).
Parameters
----------
number : int
The number associated to this ending
Attributes
----------
number : int
See parameters
"""
def __init__(self, number):
super().__init__()
self.number = number
[docs]class Barline(TimedObject):
"""Class that represents the style of a barline"""
def __init__(self, style):
super().__init__()
self.style = style
[docs]class Measure(TimedObject):
"""A measure
Parameters
----------
number : int
The running count independent of measure regularity/ volta endings, continuously counting up all measures in a musicxml score file and always starting from one.
name : string, optional
The ID of the measure in a given musicxml score file. Can be a non-number in case of volta endings, irregular measures (i.e., pickup measures in the middle of the piece). Defaults to None
Attributes
----------
number : int
See parameters
name : str
See parameters
"""
def __init__(self, number=None, name=None):
super().__init__()
self.number = number
self.name = name
def __str__(self):
return f"{super().__str__()} number={self.number} name={self.name}"
@property
def page(self):
"""The page number on which this measure appears, or None if
there is no associated page.
Returns
-------
int or None
"""
page = next(self.start.iter_prev(Page, eq=True), None)
if page:
return page.number
else:
return None
@property
def system(self):
"""The system number in which this measure appears, or None if
there is no associated system.
Returns
-------
int or None
"""
system = next(self.start.iter_prev(System, eq=True), None)
if system:
return system.number
else:
return None
# TODO: add `incomplete` or `anacrusis` property
[docs]class TimeSignature(TimedObject):
"""A time signature.
Parameters
----------
beats : int
The number of beats in a measure (the numerator).
beat_type : int
The note type that defines the beat unit (the denominator).
(4 for quarter notes, 2 for half notes, etc.)
musical_beats : int
The number of beats according to musicologial standards
(2 if beats is 2 or 6; 3 if beats is 3 or 9; 4 if beats is 4 or 12;
else beats)
Attributes
----------
beats : int
See parameters
beat_type : int
See parameters
musical_beat : int
See parameters
"""
def __init__(self, beats, beat_type):
super().__init__()
self.beats = beats
self.beat_type = beat_type
self.musical_beats = ( # if a value is provided, otherwise default to beats
MUSICAL_BEATS[self.beats]
if self.beats in MUSICAL_BEATS.keys()
else self.beats
)
def __str__(self):
return f"{super().__str__()} {self.beats}/{self.beat_type}"
[docs]class Tempo(TimedObject):
"""A tempo indication.
Parameters
----------
bpm : number
The tempo indicated in rate per minute
unit : str or None, optional
The unit to which the specified rate correspnds. This is a
string that expreses a duration category, such as "q" for
quarter "h." for dotted half, and so on. When None, the unit
is assumed to be quarters. Defaults to None.
Attributes
----------
bpm : number
See parameters
unit : str or None
See parameters
"""
def __init__(self, bpm, unit=None):
super().__init__()
self.bpm = bpm
self.unit = unit
@property
def microseconds_per_quarter(self):
"""The number of microseconds per quarter under this tempo.
This is useful for MIDI representations.
Returns
-------
int
"""
return int(
np.round(60 * (10**6 / to_quarter_tempo(self.unit or "q", self.bpm)))
)
def __str__(self):
if self.unit:
return f"{super().__str__()} {self.unit}={self.bpm}"
else:
return f"{super().__str__()} bpm={self.bpm}"
[docs]class Staff(TimedObject):
"""A staff.
Parameters
----------
number : int
The staff number
lines : int, optional (default: 5)
Attributes
----------
number : int
See parameters
"""
def __init__(self, number, lines=5):
super().__init__()
self.number = number
self.lines = lines
def __str__(self):
return f"{super().__str__()} number={self.number} lines={self.lines}"
[docs]class KeySignature(TimedObject):
"""Key signature.
Parameters
----------
fifths : number
Number of sharps (positive) or flats (negative)
mode : str
Mode of the key, either 'major' or 'minor'
Attributes
----------
fifths : number
See parameters
mode : str
See parameters
"""
def __init__(self, fifths, mode):
super().__init__()
self.fifths = fifths
self.mode = mode
@property
def name(self):
"""The key signature name, where the root is uppercase, and an
trailing 'm' indicates minor modes (e.g. 'Am', 'G#').
Returns
-------
str
The key signature name
"""
return fifths_mode_to_key_name(self.fifths, self.mode)
def __str__(self):
return (
f"{super().__str__()} fifths={self.fifths}, mode={self.mode} ({self.name})"
)
[docs]class Transposition(TimedObject):
"""Represents a <transpose> tag that tells how to change all
(following) pitches of that part to put it to concert pitch (i.e.
sounding pitch).
Parameters
----------
diatonic : int
TODO
chromatic : int
The number of semi-tone steps to add or subtract to the pitch
to get to the (sounding) concert pitch.
Attributes
----------
diatonic : int
See parameters
chromatic : int
See parameters
"""
def __init__(self, diatonic, chromatic):
super().__init__()
self.diatonic = diatonic
self.chromatic = chromatic
def __str__(self):
return (
f"{super().__str__()} diatonic={self.diatonic}, chromatic={self.chromatic}"
)
[docs]class Words(TimedObject):
"""A textual element in the score.
Parameters
----------
text : str
The text
staff : int or None, optional
The staff to which the text is associated. Defaults to None
Attributes
----------
text : str
See parameters
staff : int or None, optional
See parameters
"""
def __init__(self, text, staff=None):
super().__init__()
self.text = text
self.staff = staff
def __str__(self):
return f'{super().__str__()} "{self.text}"'
[docs]class OctaveShiftDirection(TimedObject):
"""An octave shift direction.
Parameters
----------
"""
def __init__(self, shift_type, shift_size=8, staff=None):
super().__init__()
self.shift_type = shift_type
self.shift_size = shift_size
self.staff = staff
self.applied = False
def __str__(self):
return f'{super().__str__()} "{self.shift_type}"'
[docs]class Harmony(TimedObject):
"""A harmony element in the score not currently used.
Parameters
----------
text : str
The harmony text
Attributes
----------
text : str
See parameters
"""
def __init__(self, text):
super().__init__()
self.text = text
# assert issubclass(note, GenericNote)
def __str__(self):
return f'{super().__str__()} "{self.text}"'
[docs]class RomanNumeral(TimedObject):
"""A harmony element in the score usually for Roman Numerals.
Parameters
----------
text : str
The harmony text
Attributes
----------
text : str
See parameters
"""
def __init__(self, text):
super().__init__()
self.text = text
# assert issubclass(note, GenericNote)
def __str__(self):
return f'{super().__str__()} "{self.text}"'
[docs]class ChordSymbol(TimedObject):
"""A harmony element in the score usually for Chord Symbols."""
def __init__(self, root, kind, bass=None):
super().__init__()
self.kind = kind
self.root = root
self.bass = bass
def __str__(self):
return f'{super().__str__()} "{self.root + self.kind}"'
[docs]class Interval(object):
"""
An interval element usually used for transpositions
Parameters
----------
number : int
The interval number (e.g. 1, 2, 3, 4, 5, 6, 7, ...)
quality : str
The interval quality (e.g. M, m, P, A, d, dd, AA)
direction : str
The interval direction (e.g. up, down)
"""
def __init__(self, number, quality, direction="up"):
self.number = number
self.quality = quality
self.direction = direction
self.validate()
def validate(self):
number = self.number % 7
number = 7 if number == 0 else number
assert (
self.quality + str(number) in INTERVALCLASSES
), f"Interval {number}{self.quality} not found"
assert self.direction in [
"up",
"down",
], f"Interval direction {self.direction} not found"
def __str__(self):
return f'{super().__str__()} "{self.number}{self.quality}"'
[docs]class Direction(TimedObject):
"""Base class for performance directions in the score."""
def __init__(self, text=None, raw_text=None, staff=None):
super().__init__()
self.text = text if text is not None else ""
self.raw_text = raw_text
self.staff = staff
def __str__(self):
if self.raw_text is not None:
return f'{super().__str__()} "{self.text}" raw_text="{self.raw_text}"'
else:
return f'{super().__str__()} "{self.text}"'
[docs]class LoudnessDirection(Direction):
pass
[docs]class TempoDirection(Direction):
pass
[docs]class ArticulationDirection(Direction):
pass
[docs]class PedalDirection(Direction):
pass
[docs]class ConstantDirection(Direction):
pass
[docs]class DynamicDirection(Direction):
pass
[docs]class ImpulsiveDirection(Direction):
pass
[docs]class ConstantLoudnessDirection(ConstantDirection, LoudnessDirection):
pass
[docs]class ConstantTempoDirection(ConstantDirection, TempoDirection):
pass
[docs]class ConstantArticulationDirection(ConstantDirection, ArticulationDirection):
pass
[docs]class DynamicLoudnessDirection(DynamicDirection, LoudnessDirection):
def __init__(self, *args, wedge=False, **kwargs):
super().__init__(*args, **kwargs)
self.wedge = wedge
def __str__(self):
if self.wedge:
return f"{super().__str__()} wedge"
else:
return super().__str__()
[docs]class DynamicTempoDirection(DynamicDirection, TempoDirection):
pass
[docs]class IncreasingLoudnessDirection(DynamicLoudnessDirection):
pass
[docs]class DecreasingLoudnessDirection(DynamicLoudnessDirection):
pass
[docs]class IncreasingTempoDirection(DynamicTempoDirection):
pass
[docs]class DecreasingTempoDirection(DynamicTempoDirection):
pass
[docs]class ImpulsiveLoudnessDirection(ImpulsiveDirection, LoudnessDirection):
pass
[docs]class SustainPedalDirection(PedalDirection):
"""Represents a Sustain Pedal Direction"""
def __init__(self, line=False, *args, **kwargs):
super().__init__("sustain_pedal", *args, **kwargs)
self.line = line
[docs]class ResetTempoDirection(ConstantTempoDirection):
@property
def reference_tempo(self):
direction = None
for d in self.start.iter_prev(ConstantTempoDirection):
direction = d
return direction
[docs]class PartGroup(object):
"""Represents a grouping of several instruments, usually named,
and expressed in the score with a group symbol such as a brace or
a bracket. In symphonic scores, bracketed part groups usually
group families of instruments, such as woodwinds or brass, whereas
braces are often used to group multiple instances of the same
instrument. See the `MusicXML documentation
<https://usermanuals.musicxml.com/MusicXML/Content/ST-MusicXML-
group-symbol-value.htm>`_ for further information.
Parameters
----------
group_symbol : str or None, optional
The symbol used for grouping instruments.
Attributes
----------
group_symbol : str or None
name : str or None
number : int
parent : PartGroup or None
children : list of Part or PartGroup objects
"""
def __init__(self, group_symbol=None, group_name=None, number=None, id=None):
self.group_symbol = group_symbol
self.group_name = group_name
self.number = number
self.parent = None
self.id = id
self.children = []
def _pp(self, tree):
result = [
'{}PartGroup: group_name="{}" group_symbol="{}"'.format(
tree, self.group_name, self.group_symbol
)
]
tree.push()
N = len(self.children)
for i, child in enumerate(self.children):
result.append("{}".format(tree).rstrip())
if i == N - 1:
tree.last_item()
else:
tree.next_item()
result.extend(child._pp(tree))
tree.pop()
return result
[docs] def pretty(self):
"""Return a pretty representation of this object.
Returns
-------
str
A pretty representation
"""
return "\n".join(self._pp(PrettyPrintTree()))
[docs] def note_array(self, *args, **kwargs):
"""A structured array containing pitch, onset, duration, voice
and id for each note in each part of the PartGroup. The note
ids in this array include the number of the part to which they
belong.
See Part.note_array()
"""
return note_array_from_part_list(self.children, *args, **kwargs)
[docs] def rest_array(self, *args, **kwargs):
"""A structured array containing pitch, onset, duration, voice
and id for each note in each part of the PartGroup. The note
ids in this array include the number of the part to which they
belong.
See Part.note_array()
"""
return rest_array_from_part_list(self.children, *args, **kwargs)
[docs]class Score(object):
"""Main object for representing a score.
The `Score` object is basically an iterable that provides access to all
`Part` objects in a musical score.
Parameters
----------
id : str
The identifier of the score. In order to be compatible with MusicXML
the identifier should not start with a number.
partlist : `Part`, `PartGroup` or list of `Part` or `PartGroup` instances.
List of `Part` or `PartGroup` objects.
work_title: str, optional
Work title of the score, if applicable.
work_number: str, optional
Work number of the score, if applicable.
movement_title: str, optional
Movement title of the score, if applicable.
movement_number: str, optional
Movement number of the score, if applicable.
title : str, optional
Title of the score, from <credit-words> tag
subtitle: str, optional
Subtitle of the score.
composer: str, optional
Composer of the score.
lyricist: str, optional
Lyricist of the score.
copyright: str, optional.
Copyright notice of the score.
Attributes
----------
id : str
See parameters.
parts : list of `Part` objects
All `Part` objects.
part_structure: list of `Part` or `PartGrop`
List of all `Part` or `PartGroup` objects that specify the structure of
the score.
work_title: str
See parameters.
work_number: str
See parameters.
movement_title: str
See parameters.
movement_number: str
See parameters.
subtitle: str
See parameters.
composer: str
See parameters.
lyricist: str
See parameters.
copyright: str.
See parameters.
"""
id: Optional[str]
work_title: Optional[str]
work_number: Optional[str]
movement_title: Optional[str]
movement_number: Optional[str]
title: Optional[str]
subtitle: Optional[str]
composer: Optional[str]
lyricist: Optional[str]
copyright: Optional[str]
parts: List[Part]
part_structure: List[Union[Part, PartGroup]]
def __init__(
self,
partlist: Union[Part, PartGroup, Itertype[Union[Part, PartGroup]]],
id: Optional[str] = None,
work_title: Optional[str] = None,
work_number: Optional[str] = None,
movement_title: Optional[str] = None,
movement_number: Optional[str] = None,
title: Optional[str] = None,
subtitle: Optional[str] = None,
composer: Optional[str] = None,
lyricist: Optional[str] = None,
copyright: Optional[str] = None,
) -> None:
self.id = id
# Score Information (default from MuseScore/MusicXML)
self.work_title = work_title
self.work_number = work_number
self.movement_title = movement_title
self.movement_number = movement_number
self.title = title
self.subtitle = subtitle
self.composer = composer
self.lyricist = lyricist
self.copyright = copyright
# Flat list of parts
self.parts = list(iter_parts(partlist))
# List of Parts and PartGroups
if isinstance(partlist, (Part, PartGroup)):
self.part_structure = [partlist]
elif isinstance(partlist, Iterable):
self.part_structure = list(partlist)
else:
raise ValueError(
"`partlist` should be a list, a `Part` or a `PartGrop` but"
f" is {type(partlist)}."
)
def __getitem__(self, index: int) -> Part:
"""Get `Part in the score by index"""
return self.parts[index]
def __setitem__(self, index: int, part: Part) -> None:
"""Set `Part` in the score by index"""
# TODO: How to update the score structure as well?
self.parts[index] = part
def __iter__(self) -> Iterator[Part]:
self.iter_idx = 0
return self
def __next__(self) -> Part:
if self.iter_idx == len(self.parts):
raise StopIteration
res = self[self.iter_idx]
self.iter_idx += 1
return res
def __len__(self) -> int:
"""
The lenght of the score is the number of part objects in `self.parts`
"""
return len(self.parts)
[docs] def note_array(
self,
unique_id_per_part=True,
include_pitch_spelling=False,
include_key_signature=False,
include_time_signature=False,
include_metrical_position=False,
include_grace_notes=False,
include_staff=False,
include_divs_per_quarter=False,
**kwargs,
) -> np.ndarray:
"""
Get a note array that concatenates the note arrays of all Part/PartGroup
objects in the score.
"""
return note_array_from_part_list(
part_list=self.parts,
unique_id_per_part=unique_id_per_part,
include_pitch_spelling=include_pitch_spelling,
include_key_signature=include_key_signature,
include_time_signature=include_time_signature,
include_grace_notes=include_grace_notes,
include_metrical_position=include_metrical_position,
include_staff=include_staff,
include_divs_per_quarter=include_divs_per_quarter,
**kwargs,
)
# Alias for typing score-like objects
ScoreLike = Union[List[Union[Part, PartGroup]], Part, PartGroup, Score]
class ScoreVariant(object):
# non-public
def __init__(self, part, start_time=0):
self.t_unfold = start_time
self.segments = []
self.part = part
def add_segment(self, start, end):
self.segments.append((start, end, self.t_unfold))
self.t_unfold += end.t - start.t
@property
def segment_times(self):
"""
Return segment (start, end, offset) information for each of the segments in
the score variant.
"""
return [(s.t, e.t, o) for (s, e, o) in self.segments]
def __str__(self):
return f"{super().__str__()} {self.segment_times}"
def clone(self):
"""
Return a clone of the ScoreVariant
"""
clone = ScoreVariant(self.part, self.t_unfold)
clone.segments = self.segments[:]
return clone
def create_variant_part(self):
part = Part(self.part.id, part_name=self.part.part_name)
for start, end, offset in self.segments:
delta = offset - start.t
qd = self.part.quarter_durations(start.t, end.t)
for t, quarter in qd:
part.set_quarter_duration(t + delta, quarter)
# After creating the new part we need to replace references to
# objects in the old part to references in the new part
# (e.g. t.next, t.prev, note.tie_next). For this we keep track of
# correspondences between objects (timepoints, notes, measures,
# etc), in o_map
o_map = {}
o_new = set()
tp = start
while tp != end:
# make a new timepoint, corresponding to tp
tp_new = part.get_or_add_point(tp.t + delta)
o_gen = (o for oo in tp.starting_objects.values() for o in oo)
for o in o_gen:
# special cases:
# don't include some TimedObjects in the unfolded part
if isinstance(
o,
(
Repeat,
Ending,
ToCoda,
DaCapo,
DalSegno,
Segment,
System,
Page,
),
):
continue
# don't repeat time sig if it hasn't changed
elif isinstance(o, TimeSignature):
prev = next(tp_new.iter_prev(TimeSignature), None)
if (prev is not None) and (
(o.beats, o.beat_type) == (prev.beats, prev.beat_type)
):
continue
# don't repeat key sig if it hasn't changed
elif isinstance(o, KeySignature):
prev = next(tp_new.iter_prev(KeySignature), None)
if (prev is not None) and (
(o.fifths, o.mode) == (prev.fifths, prev.mode)
):
continue
# don't repeat clef if it hasn't changed
elif isinstance(o, Clef):
prev = next(tp_new.iter_prev(Clef), None)
if (prev is not None) and (
(o.sign, o.line, o.staff)
== (prev.sign, prev.line, prev.staff)
):
continue
# make a copy of the object
o_copy = copy(o)
# add it to the set of new objects (for which the refs will
# be replaced)
o_new.add(o_copy)
# keep track of the correspondence between o and o_copy
o_map[o] = o_copy
# add the start of the new object to the part
tp_new.add_starting_object(o_copy)
if o.end is not None:
# add the end of the object to the part
tp_end = part.get_or_add_point(o.end.t + delta)
tp_end.add_ending_object(o_copy)
tp = tp.next
if tp is None:
raise Exception(
"segment end not a successor of segment start, "
"invalid score variant"
)
# special case: fermata starting at end of segment should be
# included if it does not belong to a note, and comes at the end of
# a measure (o.ref == 'right')
for o in end.starting_objects[Fermata]:
if o.ref in (None, "right"):
o_copy = copy(o)
tp_new = part.get_or_add_point(end.t + delta)
tp_new.add_starting_object(o_copy)
# for each of the new objects, replace the references to the old
# objects to their corresponding new objects
for o in o_new:
o.replace_refs(o_map)
# replace prev/next references in timepoints
for tp, tp_next in iter_current_next(part._points):
tp.next = tp_next
tp_next.prev = tp
return part
[docs]def add_measures(part):
"""Add measures to a part.
This function adds Measure objects to the part according to any
time signatures present in the part. Any existing measures will be
untouched, and added measures will be delimited by the existing
measures.
The Part object will be modified in place.
Parameters
----------
part : :class:`Part`
Part instance
"""
timesigs = np.array(
[(ts.start.t, ts.beats) for ts in part.iter_all(TimeSignature)], dtype=int
)
if len(timesigs) == 0:
warnings.warn("No time signatures found, not adding measures")
return
start = part.first_point.t
end = part.last_point.t
if start == end:
return
# make sure we cover time from the start of the timeline
if len(timesigs) == 0 or timesigs[0, 0] > start:
timesigs = np.vstack(([[start, 4]], timesigs))
# in unlikely case of timesig at last point, remove it
if timesigs[-1, 0] >= end:
timesigs = timesigs[:-1]
ts_start_times = timesigs[:, 0]
beats_per_measure = timesigs[:, 1]
ts_end_times = ts_start_times[1:]
# make sure we cover time until the end of the timeline
if len(ts_end_times) == 0 or ts_end_times[-1] < end:
ts_end_times = np.r_[ts_end_times, end]
assert len(ts_start_times) == len(ts_end_times)
beat_map = part.beat_map
inv_beat_map = part.inv_beat_map
mcounter = 1
for ts_start, ts_end, measure_dur in zip(
ts_start_times, ts_end_times, beats_per_measure
):
pos = ts_start
while pos < ts_end:
measure_start = pos
measure_end_beats = min(beat_map(pos) + measure_dur, beat_map(end))
measure_end = min(ts_end, inv_beat_map(measure_end_beats))
# any existing measures between measure_start and measure_end
existing_measure = next(
part.iter_all(Measure, measure_start, measure_end), None
)
if existing_measure:
if existing_measure.start.t == measure_start:
assert existing_measure.end.t > pos
pos = existing_measure.end.t
if existing_measure.number != 0:
# if existing_measure is a match anacrusis measure,
# keep number 0
existing_measure.number = mcounter
mcounter += 1
continue
else:
measure_end = existing_measure.start.t
part.add(
Measure(number=mcounter, name=str(mcounter)),
int(measure_start),
int(measure_end),
)
# if measure exists but was not at measure_start,
# a filler measure is added with number mcounter
if existing_measure:
pos = existing_measure.end.t
existing_measure.number = mcounter + 1
mcounter = mcounter + 2
else:
pos = measure_end
mcounter += 1
[docs]def remove_grace_notes(part):
"""Remove all grace notes from a timeline.
The specified timeline object will be modified in place.
Parameters
----------
timeline : Timeline
The timeline from which to remove the grace notes
"""
for gn in list(part.iter_all(GraceNote)):
part.remove(gn)
[docs]def expand_grace_notes(part):
"""Expand grace note durations in a part.
The specified part object will be modified in place.
Parameters
----------
part : :class:`Part`
The part on which to expand the grace notes
"""
for gn in part.iter_all(GraceNote):
dur = symbolic_to_numeric_duration(gn.symbolic_duration, gn.start.quarter)
part.remove(gn, "end")
part.add(gn, end=gn.start.t + int(np.round(dur)))
[docs]def iter_parts(partlist):
"""Iterate over all Part instances in partlist, which is a list of
either Part or PartGroup instances. PartGroup instances contain
one or more parts or further partgroups, and are traversed in a
depth-first fashion.
This function is designed to take the result of
:func:`partitura.load_score_midi` and :func:`partitura.load_musicxml` as
input.
Parameters
----------
partlist : Score, list, Part, or PartGroup
A :class:`partitura.score.Part` object,
:class:`partitura.score.PartGroup` or a list of these
Yields
-------
:class:`Part` instances in `partlist`
"""
if not isinstance(partlist, (list, tuple, set)):
_partlist = [partlist]
elif isinstance(partlist, Score):
_partlist = partlist.parts
else:
_partlist = partlist
for el in _partlist:
if isinstance(el, Part):
yield el
else:
for eel in iter_parts(el.children):
yield eel
[docs]def repeats_to_start_end(repeats, first, last):
# non-public, deprecated, unused
"""Return pairs of (start, end) TimePoints corresponding to the start and
end times of each Repeat object. If any of the start or end attributes
are None, replace it with the end/start of the preceding/succeeding
Repeat, respectively, or `first` or `last`.
Parameters
----------
repeats : list
list of Repeat instances, possibly with None-valued start/end
attributes
first : TimePoint
The first TimePoint in the timeline
last : TimePoint
The last TimePoint in the timeline
Returns
-------
list
list of (start, end) TimePoints corresponding to each Repeat in
`repeats`
"""
t = first
starts = []
ends = []
for repeat in repeats:
starts.append(t if repeat.start is None else repeat.start)
if repeat.end is not None:
t = repeat.end
t = last
for repeat in reversed(repeats):
ends.append(t if repeat.end is None else repeat.end)
if repeat.start is not None:
t = repeat.start
ends.reverse()
return list(zip(starts, ends))
def _make_tied_note_id(prev_id):
# non-public
"""Create a derived note ID for newly created notes, by appending
letters to the ID. If the original ID has the form X-Y (e.g.
n1-1), then the letter will be appended to the X part.
Parameters
----------
prev_id : str
Original note ID
Returns
-------
str
Derived note ID
Examples
--------
>>> _make_tied_note_id('n0')
'n0a'
>>> _make_tied_note_id('n0a')
'n0b'
>>> _make_tied_note_id('n0-1')
'n0a-1'
"""
prev_id_parts = prev_id.split("-", 1)
prev_id_p1 = prev_id_parts[0]
if prev_id_p1:
if ord(prev_id_p1[-1]) < ord("a") - 1:
return "-".join(["{}a".format(prev_id_p1)] + prev_id_parts[1:])
else:
return "-".join(
["{}{}".format(prev_id_p1[:-1], chr(ord(prev_id[-1]) + 1))]
+ prev_id_parts[1:]
)
else:
return None
[docs]def tie_notes(part):
"""Find notes that span measure boundaries and notes with composite
durations, and split them adding ties.
Parameters
----------
part : :class:`Part`
Description of `part`
"""
# split and tie notes at measure boundaries
for note in list(part.iter_all(Note)):
next_measure = next(note.start.iter_next(Measure), None)
cur_note = note
note_end = cur_note.end
# keep the list of stopping slurs, we need to transfer them to the last
# tied note
slur_stops = cur_note.slur_stops
while next_measure and cur_note.end > next_measure.start:
part.remove(cur_note, "end")
cur_note.slur_stops = []
part.add(cur_note, None, next_measure.start.t)
cur_note.symbolic_duration = estimate_symbolic_duration(
next_measure.start.t - cur_note.start.t, cur_note.start.quarter
)
sym_dur = estimate_symbolic_duration(
note_end.t - next_measure.start.t, next_measure.start.quarter
)
if cur_note.id is not None:
note_id = _make_tied_note_id(cur_note.id)
else:
note_id = None
if isinstance(cur_note, UnpitchedNote):
next_note = UnpitchedNote(
cur_note.step,
cur_note.octave,
id=note_id,
voice=cur_note.voice,
staff=cur_note.staff,
symbolic_duration=sym_dur,
)
else:
next_note = Note(
note.step,
note.octave,
note.alter,
id=note_id,
voice=note.voice,
staff=note.staff,
symbolic_duration=sym_dur,
)
part.add(next_note, next_measure.start.t, note_end.t)
cur_note.tie_next = next_note
next_note.tie_prev = cur_note
cur_note = next_note
next_measure = next(cur_note.start.iter_next(Measure), None)
if cur_note != note:
for slur in slur_stops:
slur.end_note = cur_note
# then split/tie any notes that do not have a fractional/dot duration
divs_map = part.quarter_duration_map
max_splits = 3
failed = 0
succeeded = 0
for i, note in enumerate(list(part.iter_all(Note))):
if note.symbolic_duration is None:
splits = find_tie_split(
note.start.t, note.end.t, int(divs_map(note.start.t)), max_splits
)
if splits:
succeeded += 1
split_note(part, note, splits)
else:
failed += 1
[docs]def set_end_times(parts):
# non-public
"""Set missing end times of musical elements in a part to equal
the start times of the subsequent element of the same class. This
is useful for some classes
This function modifies the parts in place.
Parameters
----------
part : Part or PartGroup, or list of these
Parts to be processed
"""
for part in iter_parts(parts):
# page, system, loudnessdirection, tempodirection
_set_end_times(part, Page)
_set_end_times(part, System)
_set_end_times(part, ConstantLoudnessDirection)
_set_end_times(part, ConstantTempoDirection)
_set_end_times(part, ConstantArticulationDirection)
def _set_end_times(part, cls):
acc = []
t = None
for obj in part.iter_all(cls, include_subclasses=True):
if obj.start == t:
if obj.end is None:
acc.append(obj)
else:
for o in acc:
part.add(o, end=obj.start.t)
acc = []
if obj.end is None:
acc.append(obj)
t = obj.start
for o in acc:
part.add(o, end=part.last_point.t)
def split_note(part, note, splits):
# non-public
# TODO: we shouldn't do this, but for now it's a good sanity check
assert len(splits) > 0
# TODO: we shouldn't do this, but for now it's a good sanity check
assert note.symbolic_duration is None
part.remove(note)
orig_tie_next = note.tie_next
slur_stops = note.slur_stops
cur_note = note
start, end, sym_dur = splits.pop(0)
cur_note.symbolic_duration = sym_dur
part.add(cur_note, start, end)
while splits:
note.slur_stops = []
if cur_note.id is not None:
note_id = _make_tied_note_id(cur_note.id)
else:
note_id = None
next_note = Note(
note.step,
note.octave,
note.alter,
voice=note.voice,
id=note_id,
staff=note.staff,
)
cur_note.tie_next = next_note
next_note.tie_prev = cur_note
cur_note = next_note
start, end, sym_dur = splits.pop(0)
cur_note.symbolic_duration = sym_dur
part.add(cur_note, start, end)
cur_note.tie_next = orig_tie_next
if cur_note != note:
for slur in slur_stops:
slur.end_note = cur_note
[docs]def find_tuplets(part):
"""Identify tuplets in `part` and set their symbolic durations
explicitly.
This function adds `actual_notes` and `normal_notes` keys to
the symbolic duration of tuplet notes.
This function modifies the part in place.
Parameters
----------
part : :class:`Part`
Part instance
"""
# quick shot at finding tuplets intended to cover some common cases.
# are tuplets always in the same voice?
# quite arbitrary:
search_for_tuplets = [9, 7, 5, 3]
# only look for x:2 tuplets
normal_notes = 2
candidates = []
prev_end = None
# 1. group consecutive notes without symbolic_duration
for note in part.iter_all(GenericNote, include_subclasses=True):
if note.symbolic_duration is None:
if note.start.t == prev_end:
candidates[-1].append(note)
else:
candidates.append([note])
prev_end = note.end.t
# 2. within each group
for group in candidates:
# 3. search for the predefined list of tuplets
for actual_notes in search_for_tuplets:
if actual_notes > len(group):
# tuplet requires more notes than we have
continue
tup_start = 0
while tup_start <= (len(group) - actual_notes):
note_tuplet = group[tup_start : tup_start + actual_notes]
# durs = set(n.duration for n in group[:tuplet-1])
durs = set(n.duration for n in note_tuplet)
if len(durs) > 1:
# notes have different durations (possibly valid but not
# supported here)
# continue
tup_start += 1
else:
start = note_tuplet[0].start.t
end = note_tuplet[-1].end.t
total_dur = end - start
# total duration of tuplet notes must be integer-divisble by
# normal_notes
if total_dur % normal_notes > 0:
tup_start += 1
else:
# estimate duration type
dur_type = estimate_symbolic_duration(
total_dur // normal_notes, note_tuplet[0].start.quarter
)
if dur_type and dur_type.get("dots", 0) == 0:
# recognized duration without dots
dur_type["actual_notes"] = actual_notes
dur_type["normal_notes"] = normal_notes
for note in note_tuplet:
note.symbolic_duration = dur_type.copy()
start_note = note_tuplet[0]
stop_note = note_tuplet[-1]
tuplet = Tuplet(start_note, stop_note)
part.add(tuplet, start_note.start.t, stop_note.end.t)
tup_start += actual_notes
else:
tup_start += 1
[docs]def sanitize_part(part, tie_tolerance=0):
"""Find and remove incomplete structures in a part such as Tuplets
and Slurs without start or end and grace notes without a main
note.
This function modifies the part in place.
Parameters
----------
part : :class:`Part`
Part instance
tie_tolerange: int, optional
The maximum number of divs that separates notes that are tied together.
Ideally, it is 0, but not so nice scores happen.
"""
remove_grace_counter = 0
elements_to_remove = []
for gn in part.iter_all(GraceNote):
if gn.main_note is None:
for no in part.iter_all(
Note, include_subclasses=False, start=gn.start.t, end=gn.start.t + 1
):
if no.voice == gn.voice:
gn.last_grace_note_in_seq.grace_next = no
if gn.main_note is None:
elements_to_remove.append(gn)
remove_grace_counter += 1
remove_tuplet_counter = 0
for tp in part.iter_all(Tuplet):
if tp.end_note is None or tp.start_note is None:
elements_to_remove.append(tp)
remove_tuplet_counter += 1
remove_slur_counter = 0
for sl in part.iter_all(Slur):
if sl.end_note is None or sl.start_note is None:
elements_to_remove.append(sl)
remove_slur_counter += 1
for el in elements_to_remove:
part.remove(el)
remove_tie_counter = 0
for n in part.notes_tied:
if n.tie_next != None:
d = n.duration_tied
s = n.start.t
e = n.end_tied.t
if abs((e - s) - d) > tie_tolerance:
remove_tie_counter += 1
all_tied = n.tie_prev_notes + [n] + n.tie_next_notes
for tn in all_tied:
tn.tie_next = None
tn.tie_prev = None
warnings.warn(
"part_sanitize removed {} incomplete tuplets, "
"{} incomplete slurs, {} incomplete grace, "
"and {} wrong ties."
"notes".format(
remove_tuplet_counter,
remove_slur_counter,
remove_grace_counter,
remove_tie_counter,
),
stacklevel=2,
)
[docs]def assign_note_ids(parts, keep=False):
"""
Assigns new note IDs mainly used for loaders.
parts : list or score.PartGroup or score.Part
Some Partitura parts
keep : bool
Keep or given note IDs or assign new ones.
"""
if keep:
# Keep existing note id's
for p, part in enumerate(iter_parts(parts)):
for ni, n in enumerate(part.iter_all(GenericNote, include_subclasses=True)):
if isinstance(n, Rest):
n.id = "p{0}r{1}".format(p, ni) if n.id is None else n.id
else:
n.id = "p{0}n{1}".format(p, ni) if n.id is None else n.id
else:
# assign note ids to ensure uniqueness across all parts, discarding any
# existing note ids
ni = 0
ri = 0
for part in iter_parts(parts):
for n in part.iter_all(GenericNote, include_subclasses=True):
if isinstance(n, Rest):
n.id = "r{}".format(ri)
ri += 1
else:
n.id = "n{}".format(ni)
ni += 1
[docs]class Segment(TimedObject):
"""
Class that represents any segment between two navigation markers such as repetitions,
Volta brackets, or capo/fine/coda/segno directions.
Parameters
----------
id: string
unique, ordererd identifier string
to: list
list of ids of possible destinations
await_to:
list of ids of possible destinations after a jump
type : string, optional
String for the type of the segment (either "default" or "leap_start" and "leap_end"). A "leap" tuple has the effect of forcing the fastest (shortest) repetition unfolding after this segment, as is commonly expected after capo/fine/coda/segno directions.
info: string, optional
String to describe the segment, used only for printing (pretty_segments)
"""
def __init__(self, id, to, await_to, force_seq=False, type="default", info=""):
self.id = id
self.to = to
self.await_to = await_to
self.force_full_sequence = force_seq
self.type = type
self.info = info
[docs]def add_segments(part):
"""
Add segment objects to a part based on repetition and capo/fine/coda/segno directions.
Parameters
----------
part: part
A score part
"""
if len([seg for seg in part.iter_all(Segment)]) > 0:
# only add segments if no segments exist
pass
else:
boundaries = defaultdict(dict)
destinations = defaultdict(list)
valid_repeats = [
r
for r in part.iter_all(Repeat)
if r.start is not None and r.end is not None
]
valid_endings = [
r
for r in part.iter_all(Ending)
if r.start is not None and r.end is not None
]
for r in valid_repeats:
boundaries[r.start.t]["repeat_start"] = r
boundaries[r.end.t]["repeat_end"] = r
for v in valid_endings:
boundaries[v.start.t]["volta_start"] = v
boundaries[v.end.t]["volta_end"] = v
for c in part.iter_all(Coda):
boundaries[c.start.t]["coda"] = c
destinations["coda"].append(c.start.t)
for c in part.iter_all(ToCoda):
boundaries[c.start.t]["tocoda"] = c
for c in part.iter_all(DaCapo):
boundaries[c.start.t]["dacapo"] = c
for c in part.iter_all(Fine):
boundaries[c.start.t]["fine"] = c
for c in part.iter_all(Segno):
boundaries[c.start.t]["segno"] = c
destinations["segno"].append(c.start.t)
for c in part.iter_all(DalSegno):
boundaries[c.start.t]["dalsegno"] = c
boundaries[part.last_point.t]["end"] = None
boundaries[part.first_point.t]["start"] = None
boundary_times = list(boundaries.keys())
boundary_times.sort()
# for every segment get an id, its jump destinations and properties
init_character = 65
segment_info = dict()
for i, (s, e) in enumerate(zip(boundary_times[:-1], boundary_times[1:])):
segment_info[s] = {
"ID": chr(init_character + i),
"start": s,
"end": e,
"to": [],
"force_full_sequence": False,
"type": "default",
"info": list(),
"volta_numbers": list(),
}
segment_info[boundary_times[-1]] = {"ID": "END"}
current_volta_repeat_start = 0
current_volta_end = 0
current_volta_total_number = 0
for ss in boundary_times[:-1]:
se = segment_info[ss]["end"]
# loop through the boundaries at the end of current segment
for boundary_type in boundaries[se].keys():
# REPEATS
if boundary_type == "repeat_start":
segment_info[ss]["to"].append(segment_info[se]["ID"])
if boundary_type == "repeat_end":
if "volta_end" not in list(boundaries[se].keys()):
segment_info[ss]["to"].append(segment_info[se]["ID"])
repeat_start = boundaries[se][boundary_type].start.t
segment_info[ss]["to"].append(segment_info[repeat_start]["ID"])
segment_info[ss]["info"].append("repeat_end")
# VOLTA BRACKETS
if boundary_type == "volta_start":
if "volta_end" not in list(boundaries[se].keys()):
current_volta_total_number = 0
current_volta_end = se
for volta_number in range(
10
): # maximal expected number of volta brackets 10
if "volta_start" in list(
boundaries[current_volta_end].keys()
):
# add the beginning to the jump destinations
numbers = boundaries[current_volta_end][
"volta_start"
].number.split(",")
numbers = [str(int(n)) for n in numbers]
current_volta_total_number += len(numbers)
for no in numbers:
segment_info[ss]["to"].append(
no
+ "_Volta_"
+ segment_info[current_volta_end]["ID"]
)
segment_info[current_volta_end]["info"].append(
"volta " + ",".join(numbers)
)
segment_info[current_volta_end][
"volta_numbers"
] += numbers
# segment_info[bracket_end]["info"].append(str(len(numbers)))
# update the search time to the end of the ext bracket
current_volta_end = boundaries[current_volta_end][
"volta_start"
].end.t
if boundary_type == "volta_end":
current_volta_numbers = segment_info[ss]["volta_numbers"]
for vn in current_volta_numbers:
if vn != str(current_volta_total_number):
# if repeating volta bracket, jump back to start
# check if repeat exists (might not be for 3+ volta brackets)
if "repeat_end" in list(boundaries[se].keys()):
current_volta_repeat_start = max(
boundaries[se]["repeat_end"].start.t,
current_volta_repeat_start,
)
repeat_start = current_volta_repeat_start
segment_info[ss]["to"].append(
"Z_Volta_" + segment_info[repeat_start]["ID"]
)
if str(current_volta_total_number) in current_volta_numbers:
# else just go to the segment after the last
segment_info[ss]["to"].append(
segment_info[current_volta_end]["ID"]
)
# NAVIGATION SYMBOLS
# Navigation1_ = destinations that should only be used after all others
# Navigation2_ = destinations that are used *after* a jump
if boundary_type == "coda":
# if a coda symbol is passed just continue
segment_info[ss]["to"].append(segment_info[se]["ID"])
segment_info[se]["type"] = "leap_end"
segment_info[se]["info"].append("Coda")
if boundary_type == "tocoda":
segment_info[ss]["to"].append(segment_info[se]["ID"])
# find the coda and jump there
coda_time = destinations["coda"][0]
segment_info[ss]["to"].append(
"Navigation2_" + segment_info[coda_time]["ID"]
)
segment_info[ss]["type"] = "leap_start"
segment_info[ss]["info"].append("al coda")
if boundary_type == "segno":
# if a segno symbol is passed just continue
segment_info[ss]["to"].append(segment_info[se]["ID"])
segment_info[se]["type"] = "leap_end"
segment_info[se]["info"].append("segno")
if boundary_type == "dalsegno":
segment_info[ss]["to"].append(segment_info[se]["ID"])
# find the segno and jump there
segno_time = destinations["segno"][0]
segment_info[ss]["to"].append(
"Navigation1_" + segment_info[segno_time]["ID"]
)
segment_info[ss]["to"].append(
"Navigation2_" + segment_info[se]["ID"]
)
segment_info[ss]["type"] = "leap_start"
segment_info[ss]["info"].append("dal segno")
if boundary_type == "dacapo":
segment_info[ss]["to"].append(segment_info[se]["ID"])
# jump to the start
segment_info[ss]["to"].append(
"Navigation1_" + segment_info[part.first_point.t]["ID"]
)
segment_info[ss]["to"].append(
"Navigation2_" + segment_info[se]["ID"]
)
segment_info[ss]["type"] = "leap_start"
segment_info[ss]["info"].append("da capo")
if boundary_type == "fine":
segment_info[ss]["to"].append(segment_info[se]["ID"])
# jump to the start
segment_info[ss]["to"].append(
"Navigation2_" + segment_info[part.last_point.t]["ID"]
)
segment_info[ss]["info"].append("fine")
# GENERIC
if boundary_type == "end":
segment_info[ss]["to"].append(segment_info[se]["ID"])
# first segments is always a leap destination (da capo)
if ss == 0:
segment_info[ss]["type"] = "leap_end"
# clean up and ORDER all the jump destination information
for start_time in boundary_times[:-1]:
destinations = segment_info[start_time]["to"]
destinations_no_volta = [
dest
for dest in destinations
if "Volta_" not in dest and "Navigation" not in dest
]
destinations_volta = [dest for dest in destinations if "Volta_" in dest]
# dal segno and da capo
destinations_navigation1 = [
dest[12:] for dest in destinations if "Navigation1_" in dest
]
# al coda and fine
destinations_navigation2 = [
dest[12:] for dest in destinations if "Navigation2_" in dest
]
# sort the repeats by ascending segment ID
destinations_no_volta = list(set(destinations_no_volta))
# make sure the "END" destination is the last
destinations_except_await = (
destinations_volta + destinations_no_volta + destinations_navigation1
)
if "END" in destinations_except_await:
while "END" in destinations_no_volta:
destinations_no_volta.remove("END")
destinations_navigation1.append("END")
# sort repeat destinations by ascending ID
destinations_no_volta.sort()
# sort destinations by volta number
destinations_volta.sort()
# keep only the segment IDs
destinations_volta = [d[8:] for d in destinations_volta]
# don't jump to volta brackets w/t number
destinations_no_volta = [
d for d in destinations_no_volta if d not in destinations_volta
]
destinations_cleaned = (
destinations_volta + destinations_no_volta + destinations_navigation1
)
# if len(destinations_navigation2) > 0:
# # keep only jumps to the past
# await_to = [idx for idx in destinations_cleaned if idx <= segment_info[start_time]["ID"]]
# # add the waiting destinations
# await_to += destinations_navigation2
# else:
# await_to = destinations_cleaned
part.add(
Segment(
id=segment_info[start_time]["ID"],
to=destinations_cleaned,
await_to=destinations_navigation2, # await_to,
force_seq=segment_info[start_time]["force_full_sequence"],
type=segment_info[start_time]["type"],
info=", ".join(segment_info[start_time]["info"]),
),
segment_info[start_time]["start"],
segment_info[start_time]["end"],
)
[docs]def get_segments(part):
"""
Get dictionary of segment objects of a part.
Parameters
----------
part: part
A score part
Returns
-------
segments: dict
A dictionary of Segment objects indexed by segment IDs.
"""
return {seg.id: seg for seg in part.iter_all(Segment)}
[docs]def pretty_segments(part):
"""
Get a pretty string of all the segments in a part.
"""
add_segments(part)
segments = get_segments(part)
string_list = [
str(segments[p].id)
+ " -> (choice) "
+ "{:<8}".format(",".join(segments[p].to))
+ "\t segment "
+ "{:<20}".format(
str(part.beat_map(segments[p].start.t))
+ " - "
+ str(part.beat_map(segments[p].end.t))
)
+ "\t duration: "
+ "{:<6}".format(
str(part.beat_map(segments[p].end.t) - part.beat_map(segments[p].start.t))
)
+ "\t info: "
+ str(segments[p].info)
for p in segments.keys()
]
return "\n".join(string_list)
[docs]class Path:
"""
Path that represents a sequence of segments.
Parameters
----------
path : list
The string of segment IDs
segments : dict
A dictionary of available segments by segment ID
used_segment_jumps : defaultdict(list), optional
dictionary of used jumps per segment in this path
no_repeats : bool, optional
Flag to generate no repeating jump destinations with list_of_destinations_from_last_segment
all_repeats : bool, optional
Flag to generate all repeating jump destinations with list_of_destinations_from_last_segment
(lower prority than no_repeats)
jumped: bool
indicates the presence of a da capo, dal segno, or al coda jump in this path
"""
def __init__(
self,
path_list,
segments,
used_segment_jumps=None,
no_repeats=False,
all_repeats=False,
jumped=False,
):
self.path = path_list
self.segments = segments
if used_segment_jumps is None:
self.used_segment_jumps = defaultdict(list)
else:
self.used_segment_jumps = used_segment_jumps
self.ended = False
self.jumped = False
self.no_repeats = no_repeats
self.all_repeats = all_repeats
self.jumped = jumped
def __str__(self):
"""
return a string of segment IDs.
"""
return "-".join(self.path)
def __len__(self):
return len(self.path)
[docs] def pretty(self, part=None):
"""
create a pretty string describing this path instance.
If a corresponding part is given, the string will give
segment times in beats, else in divs.
"""
if part is None:
string_list = [
str(self.segments[p].id)
+ " -> (choice) "
+ ",".join(self.segments[p].to)
+ " \t segment "
+ str(self.segments[p].start.t)
+ " - "
+ str(self.segments[p].end.t)
+ "\t duration: "
+ str(self.segments[p].duration)
+ " \t type: "
+ str(self.segments[p].type)
for p in self.path
]
else:
string_list = [
str(self.segments[p].id)
+ " -> (choice) "
+ ",".join(self.segments[p].to)
+ " \t segment "
+ str(part.beat_map(self.segments[p].start.t))
+ " - "
+ str(part.beat_map(self.segments[p].end.t))
+ "\t duration: "
+ str(part.beat_map(self.segments[p].duration))
+ " \t type: "
+ str(self.segments[p].type)
for p in self.path
]
return "\n".join(string_list)
[docs] def copy(self):
"""
create a copy of this path instance.
"""
new_path = Path(
copy(self.path),
copy(self.segments),
no_repeats=self.no_repeats,
all_repeats=self.all_repeats,
jumped=self.jumped,
)
for key in self.used_segment_jumps:
for used_dest in self.used_segment_jumps[key]:
new_path.used_segment_jumps[key].append(used_dest)
return new_path
[docs] def make_copy_with_jump_to(self, destination, ignore_leap_info=True):
"""
create a copy of this path instance with an added jump.
If the jump is a leap (dal segno, da capo, al coda)
and leap information is not ignored,
set the new Path to subsequently follow the the shortest version.
"""
new_path = self.copy()
new_path.used_segment_jumps[new_path.path[-1]].append(destination)
new_path.path.append(destination)
if (
new_path.segments[destination].type == "leap_end"
and new_path.segments[new_path.path[-2]].type == "leap_start"
):
if not new_path.jumped:
new_path.jumped = True
for segid in new_path.segments.keys():
seg = new_path.segments[segid]
# if destinations await the second round, add them
if len(seg.await_to) > 0:
# keep only jumps to the past
to = [idx for idx in seg.to if idx <= seg.id]
# add the waiting destinations
to += seg.await_to
# replace destinations
seg.to = to
# delete used destinations
new_path.used_segment_jumps[segid] = list()
# add the jump destination to the used ones
new_path.used_segment_jumps[new_path.path[-2]].append(destination)
if not ignore_leap_info:
new_path.no_repeats = True
return new_path
@property
def list_of_destinations_from_last_segment(self):
destinations = list(self.segments[self.path[-1]].to)
previously_used_destinations = self.used_segment_jumps[self.path[-1]]
# only continue in order of the sequence, after full consumption, start at zero
# if the full or minimal sequence is forced,
# return only the single possible jump destination, else return possibly many.
if len(previously_used_destinations) != 0:
last_destination = previously_used_destinations[-1]
last_destination_count = previously_used_destinations.count(
last_destination
)
last_destination_index = [
i for i, n in enumerate(destinations * 100) if n == last_destination
][last_destination_count - 1]
last_destination_index %= len(destinations)
if self.no_repeats:
# currently this is in higher priority than the full sequence
return [destinations[-1]]
elif self.segments[self.path[-1]].force_full_sequence or self.all_repeats:
# if the full sequence should be used in general
if len(previously_used_destinations) == 0:
return [destinations[0]]
else:
# last_destination = previously_used_destinations[-1]
# last_destination_index = destinations.index(last_destination)
if last_destination_index < (len(destinations) - 1):
return [destinations[last_destination_index + 1]]
else:
return [destinations[0]]
else:
if len(previously_used_destinations) == 0:
return copy(destinations)
else:
# last_destination = previously_used_destinations[-1]
# last_destination_index = destinations.index(last_destination)
if last_destination_index < (len(destinations) - 1):
return copy(destinations[last_destination_index + 1 :])
else:
return copy(destinations)
[docs]def unfold_paths(path, paths, ignore_leap_info=True):
"""
Given a starting Path (at least one segment) recursively unfold into all possible
Paths with its segments. Ended Paths are stored in a list.
Parameters
----------
path : Path
a starting Path with at least one segment to be unfolded
paths : list
empty list to accumulate paths that are fully unfolded until an "end" keyword was found
"""
destinations = path.list_of_destinations_from_last_segment
for destination_id in destinations:
if destination_id == "END":
path.ended = True
paths.append(path)
else:
new_path = path.make_copy_with_jump_to(
destination_id, ignore_leap_info=ignore_leap_info
)
unfold_paths(new_path, paths, ignore_leap_info=ignore_leap_info)
[docs]def get_paths(part, no_repeats=False, all_repeats=False, ignore_leap_info=True):
"""
Get a list of paths and and a dictionary of segment objects of a part.
Common settings to get specific paths:
- default: all possible paths
(no_repeats = False, all_repeats = False, ignore_leap_info = True)
- default: all possible paths but without repetitions after leap
(no_repeats = False, all_repeats = False, ignore_leap_info = False)
- The longest possible path
(no_repeats = False, all_repeats = True, ignore_leap_info = True)
- The longest possible path but without repetitions after leap
(no_repeats = False, all_repeats = True, ignore_leap_info = False)
- The shortest possible path.
(no_repeats = True)
Note this might not be musically valid, e.g. a passing a "fine"
even a first time will stop this unfolding.
Parameters
----------
part: part
A score part
no_repeats : bool, optional
Flag to choose no repeating segments, i.e. the shortest path.
all_repeats : bool, optional
Flag to choose all repeating segments, i.e. the longest path.
(lower priority than the previous flag)
ignore_leap_info : bool, optional
If not ignored, Path changes to no_repeats = True if a leap is encountered.
(A leap is a used dal segno, da capo, or al coda marking)
Returns
-------
paths: list
A list of path objects
"""
add_segments(part)
segments = get_segments(part)
paths = list()
unfold_paths(
Path(["A"], segments, no_repeats=no_repeats, all_repeats=all_repeats),
paths,
ignore_leap_info=ignore_leap_info,
)
return paths
[docs]def new_part_from_path(path, part, update_ids=True):
"""
create a new Part from a Path and an underlying Part
Parameters
----------
path: Path
A Path object
part: part
A score part
update_ids : bool (optional)
Update note ids to reflect the repetitions. Note IDs will have
a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
represent the first and second repetition of 'n132' in the
input `part`. Defaults to False.
Returns
-------
new_part: part
A score part corresponding to the Path
"""
scorevariant = ScoreVariant(part)
for segment_id in path.path:
scorevariant.add_segment(
path.segments[segment_id].start, path.segments[segment_id].end
)
new_part = scorevariant.create_variant_part()
if update_ids:
update_note_ids_after_unfolding(new_part)
return new_part
[docs]def new_scorevariant_from_path(path, part):
"""
create a new Part from a Path and an underlying Part
Parameters
----------
path: Path
A Path object
part: part
A score part
Returns
-------
scorevariant: ScoreVariant
A ScoreVariant object with segments corresponding to the part
"""
scorevariant = ScoreVariant(part)
for segment_id in path.path:
scorevariant.add_segment(
path.segments[segment_id].start, path.segments[segment_id].end
)
return scorevariant
# UPDATED VERSION
[docs]def iter_unfolded_parts(part, update_ids=True):
"""Iterate over unfolded clones of `part`.
For each repeat construct in `part` the iterator produces two
clones, one with the repeat included and another without the
repeat. That means the number of items returned is two to the
power of the number of repeat constructs in the part.
The first item returned by the iterator is the version of the part
without any repeated sections, the last item is the version of the
part with all repeat constructs expanded.
Parameters
----------
part : :class:`Part`
Part to unfold
update_ids : bool (optional)
Update note ids to reflect the repetitions. Note IDs will have
a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
represent the first and second repetition of 'n132' in the
input `part`. Defaults to False.
Yields
------
"""
paths = get_paths(part, no_repeats=False, all_repeats=False, ignore_leap_info=True)
for p in paths:
yield new_part_from_path(p, part, update_ids=update_ids)
# UPDATED VERSION
[docs]def unfold_part_maximal(score: ScoreLike, update_ids=True, ignore_leaps=True):
"""Return the "maximally" unfolded part/score, that is, a copy of the
part where all segments marked with repeat signs are included
twice.
Parameters
----------
score : ScoreLike
The Part/Score to unfold.
update_ids : bool (optional)
Update note ids to reflect the repetitions. Note IDs will have
a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
represent the first and second repetition of 'n132' in the
input `part`. Defaults to False.
ignore_leaps : bool (optional)
If ignored, repetitions after a leap are unfolded fully.
A leap is a used dal segno, da capo, or al coda marking.
Defaults to True.
Returns
-------
unfolded_part : ScoreLike
The unfolded Part/Score
"""
if isinstance(score, Score):
# Copy needs to be deep, otherwise the recursion limit will be exceeded
old_recursion_depth = sys.getrecursionlimit()
sys.setrecursionlimit(10000)
# Deep copy of score
new_score = deepcopy(score)
# Reset recursion limit to previous value to avoid side effects
sys.setrecursionlimit(old_recursion_depth)
new_partlist = list()
for score in new_score.parts:
unfolded_part = unfold_part_maximal(
score, update_ids=update_ids, ignore_leaps=ignore_leaps
)
new_partlist.append(unfolded_part)
new_score.parts = new_partlist
return new_score
paths = get_paths(
score, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps
)
unfolded_part = new_part_from_path(paths[0], score, update_ids=update_ids)
return unfolded_part
[docs]def unfold_part_minimal(score: ScoreLike):
"""Return the "minimally" unfolded score/part, that is, a copy of the
part where all segments marked with repeat are included only once.
For voltas only the last volta segment is included.
Note this might not be musically valid, e.g. a passing a "fine" even a first time will stop this unfolding.
Warning: The unfolding of repeats is computed part-wise, inconsistent repeat markings of parts of a single result
in inconsistent unfoldings.
Parameters
----------
score: ScoreLike
The score/part to unfold.
Returns
-------
unfolded_score : ScoreLike
The unfolded Part
"""
if isinstance(score, Score):
# Copy needs to be deep, otherwise the recursion limit will be exceeded
old_recursion_depth = sys.getrecursionlimit()
sys.setrecursionlimit(10000)
# Deep copy of score
unfolded_score = deepcopy(score)
# Reset recursion limit to previous value to avoid side effects
sys.setrecursionlimit(old_recursion_depth)
new_partlist = list()
for part in unfolded_score.parts:
unfolded_part = unfold_part_minimal(part)
new_partlist.append(unfolded_part)
unfolded_score.parts = new_partlist
return unfolded_score
paths = get_paths(score, no_repeats=True, all_repeats=False, ignore_leap_info=True)
unfolded_score = new_part_from_path(paths[0], score, update_ids=False)
return unfolded_score
# UPDATED / UNCHANGED VERSION
[docs]def unfold_part_alignment(part, alignment):
"""Return the unfolded part given an alignment, that is, a copy
of the part where the segments are repeated according to the
repetitions in a performance.
Parameters
----------
part : :class:`Part`
The Part to unfold.
alignment : list of dictionaries
List of dictionaries containing an alignment (like the ones
obtained from a MatchFile (see `alignment_from_matchfile`).
Returns
-------
unfolded_part : :class:`Part`
The unfolded Part
"""
unfolded_parts = []
alignment_ids = []
for n in alignment:
if n["label"] == "match" or n["label"] == "deletion":
alignment_ids.append(n["score_id"])
score_variants = make_score_variants(part)
alignment_score_ids = np.zeros((len(alignment_ids), len(score_variants)))
unfolded_part_length = np.zeros(len(score_variants))
for j, sv in enumerate(score_variants):
u_part = sv.create_variant_part()
update_note_ids_after_unfolding(u_part)
unfolded_parts.append(u_part)
u_part_ids = [n.id for n in u_part.notes_tied]
unfolded_part_length[j] = len(u_part_ids)
for i, aid in enumerate(alignment_ids):
alignment_score_ids[i, j] = aid in u_part_ids
coverage = np.mean(alignment_score_ids, 0)
best_idx = np.where(coverage == coverage.max())[0]
if len(best_idx) > 1:
best_idx = best_idx[unfolded_part_length[best_idx].argmin()]
# append "-1" to alignment if the score_id's in alignment
if not any(["-1" in al.get("score_id", "") for al in alignment]):
for n in alignment:
if "score_id" in n:
n["score_id"] = f"{n['score_id']}-1"
return unfolded_parts[int(best_idx)]
# UPDATED
[docs]def make_score_variants(part):
# non-public (use unfold_part_maximal, or iter_unfolded_parts)
"""
Create a list of ScoreVariant objects, each representing a
distinct way to unfold the score, based on the repeat structure.
Parameters
----------
part : :class:`Part`
A part for which to make the score variants
Returns
-------
list
List of ScoreVariant objects
"""
paths = get_paths(part, no_repeats=False, all_repeats=False, ignore_leap_info=True)
svs = list()
for path in paths:
svs.append(new_scorevariant_from_path(path, part))
return svs
[docs]def merge_parts(parts, reassign="voice"):
"""Merge list of parts or PartGroup into a single part.
All parts are expected to have the same time signature
and quarter division.
All elements are merged, except elements with class:Barline,
Page, System, Clef, Measure, TimeSignature, KeySignature
that are only taken from the first part.
WARNING: this modifies the elements in the input, so the
original input should not be used anymore.
Parameters
----------
parts : PartGroup, list of parts and partGroups
The parts to merge
reassign: string (optional)
If "staff" the new part have as many staves as the sum
of the staves in parts, and the staff numbers get reassigned.
If "voice", the new part have only one staff, and as manually
voices as the sum of the voices in parts; the voice number
get reassigned.
Returns
-------
Part
A new part that contains the elements of the old parts
"""
# check if reassign has valid values
if reassign not in ["staff", "voice"]:
raise ValueError(
"Only 'staff' and 'voice' are supported ressign values. Found", reassign
)
# unfold grouppart and list of parts in a list of parts
if isinstance(parts, Score):
parts = parts.parts
else:
parts = list(iter_parts(parts))
# if there is only one part (it could be a list with one part or a partGroup with one part)
if len(parts) == 1:
return parts[0]
# check if there is only one division for all parts
parts_quarter_times = [p._quarter_times for p in parts]
parts_quarter_durations = [p._quarter_durations for p in parts]
if not all([len(qd) == 1 for qd in parts_quarter_durations]):
raise Exception(
"Merging parts with multiple divisions is not supported. Found divisions",
parts_quarter_durations,
"at times",
parts_quarter_times,
)
# pass from an array of array with one elements, to array of elements
parts_quarter_durations = [durs[0] for durs in parts_quarter_durations]
lcm = np.lcm.reduce(parts_quarter_durations)
time_multiplier_per_part = [int(lcm / d) for d in parts_quarter_durations]
# create a new part and fill it with all objects in other parts
new_part = Part(parts[0].id)
new_part._quarter_times = [0]
new_part._quarter_durations = [lcm]
note_arrays = [part.note_array(include_staff=True) for part in parts]
# find the maximum number of voices for each part (voice number start from 1)
maximum_voices = [
max(note_array["voice"], default=0)
if max(note_array["voice"], default=0) != 0
else 1
for note_array in note_arrays
]
# find the maximum number of staves for each part (staff number start from 0 but we force them to 1)
maximum_staves = [
max(note_array["staff"], default=0)
if max(note_array["staff"], default=0) != 0
else 1
for note_array in note_arrays
]
if reassign == "staff":
el_to_discard = (
Barline,
Page,
System,
Measure,
TimeSignature,
KeySignature,
DaCapo,
Fine,
Fermata,
Ending,
Tempo,
)
elif reassign == "voice":
el_to_discard = (
Barline,
Page,
System,
Clef,
Measure,
TimeSignature,
KeySignature,
DaCapo,
Fine,
Fermata,
Ending,
Tempo,
)
for p_ind, p in enumerate(parts):
for e in p.iter_all():
# full copy the first part and partially copy the others
# we don't copy elements like duplicate barlines, clefs or
# time signatures for others
# TODO : check DaCapo, Fine, Fermata, Ending, Tempo
if p_ind == 0 or not isinstance(
e,
el_to_discard,
): # a time multiplier is used to account for different divisions
new_start = e.start.t * time_multiplier_per_part[p_ind]
new_end = (
e.end.t * time_multiplier_per_part[p_ind]
if e.end is not None
else None
)
if reassign == "voice":
if isinstance(e, GenericNote):
e.voice = e.voice + sum(maximum_voices[:p_ind])
elif reassign == "staff":
if isinstance(e, (GenericNote, Words, Direction)):
e.staff = (e.staff if e.staff is not None else 1) + sum(
maximum_staves[:p_ind]
)
elif isinstance(
e, Clef
): # TODO: to update if "number" get changed in "staff"
e.staff = (e.staff if e.staff is not None else 1) + sum(
maximum_staves[:p_ind]
)
new_part.add(e, start=new_start, end=new_end)
# new_part.add(copy.deepcopy(e), start=new_start, end=new_end)
return new_part
[docs]def is_a_within_b(a, b, wholly=False):
"""
Returns a boolean indicating whether a is (wholly) within b.
Parameters
----------
a: TimePoint, TimedObject, int
Query object
b: TimedObject
Container object
wholly: bool
True = a needs to wholly contained in b
"""
contained = None
if not isinstance(b, TimedObject):
warnings.warn("b needs to be TimedObject")
if isinstance(a, TimePoint):
contained = a.t <= b.end.t and a.t >= b.start.t
elif isinstance(a, int):
contained = a <= b.end.t and a >= b.start.t
elif isinstance(a, TimedObject):
contained_start = a.start.t <= b.end.t and a.start.t >= b.start.t
contained_end = a.end.t <= b.end.t and a.end.t >= b.start.t
if wholly:
contained = contained_start and contained_end
else:
contained = contained_start or contained_end
else:
warnings.warn("a needs to be TimePoint, TimedObject, or int.")
return contained
[docs]class InvalidTimePointException(Exception):
"""Raised when a time point is instantiated with an invalid number."""
def __init__(self, message=None):
super().__init__(message)
if __name__ == "__main__":
import doctest
doctest.testmod()