partitura 1.3.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- partitura/directions.py +3 -0
- partitura/io/__init__.py +41 -35
- partitura/io/exportmatch.py +51 -3
- partitura/io/exportmidi.py +8 -2
- partitura/io/importmatch.py +46 -35
- partitura/io/importmei.py +159 -33
- partitura/io/importmidi.py +23 -2
- partitura/io/importmusicxml.py +40 -5
- partitura/io/matchfile_utils.py +29 -0
- partitura/io/matchlines_v1.py +8 -0
- partitura/io/musescore.py +68 -31
- partitura/performance.py +178 -2
- partitura/score.py +107 -13
- partitura/utils/music.py +16 -7
- {partitura-1.3.1.dist-info → partitura-1.4.0.dist-info}/METADATA +2 -2
- {partitura-1.3.1.dist-info → partitura-1.4.0.dist-info}/RECORD +19 -19
- {partitura-1.3.1.dist-info → partitura-1.4.0.dist-info}/WHEEL +1 -1
- {partitura-1.3.1.dist-info → partitura-1.4.0.dist-info}/LICENSE +0 -0
- {partitura-1.3.1.dist-info → partitura-1.4.0.dist-info}/top_level.txt +0 -0
partitura/io/musescore.py
CHANGED
|
@@ -36,38 +36,70 @@ class FileImportException(Exception):
|
|
|
36
36
|
pass
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# break
|
|
46
|
-
|
|
47
|
-
result = shutil.which("musescore")
|
|
48
|
-
|
|
49
|
-
if result is None:
|
|
50
|
-
result = shutil.which("musescore3")
|
|
51
|
-
|
|
52
|
-
if result is None:
|
|
53
|
-
result = shutil.which("mscore")
|
|
54
|
-
|
|
39
|
+
def find_musescore_version(version=4):
|
|
40
|
+
"""Find the path to the MuseScore executable for a specific version.
|
|
41
|
+
If version is a empty string it tries to find an unspecified version of
|
|
42
|
+
MuseScore which is used in some systems.
|
|
43
|
+
"""
|
|
44
|
+
result = shutil.which(f"musescore{version}")
|
|
55
45
|
if result is None:
|
|
56
|
-
result = shutil.which("
|
|
57
|
-
|
|
46
|
+
result = shutil.which(f"mscore{version}")
|
|
58
47
|
if result is None:
|
|
59
48
|
if platform.system() == "Linux":
|
|
60
49
|
pass
|
|
61
|
-
|
|
62
50
|
elif platform.system() == "Darwin":
|
|
63
|
-
result = shutil.which(
|
|
64
|
-
|
|
51
|
+
result = shutil.which(
|
|
52
|
+
f"/Applications/MuseScore {version}.app/Contents/MacOS/mscore"
|
|
53
|
+
)
|
|
65
54
|
elif platform.system() == "Windows":
|
|
66
|
-
result = shutil.which(
|
|
55
|
+
result = shutil.which(
|
|
56
|
+
rf"C:\Program Files\MuseScore {version}\bin\MuseScore{version}.exe"
|
|
57
|
+
)
|
|
67
58
|
|
|
68
59
|
return result
|
|
69
60
|
|
|
70
61
|
|
|
62
|
+
def find_musescore():
|
|
63
|
+
"""Find the path to the MuseScore executable.
|
|
64
|
+
|
|
65
|
+
This function first tries to find the executable for MuseScore 4,
|
|
66
|
+
then for MuseScore 3, and finally for any version of MuseScore.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
str
|
|
71
|
+
Path to the MuseScore executable
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
MuseScoreNotFoundException
|
|
76
|
+
When no MuseScore executable was found
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
mscore_exec = find_musescore_version(version=4)
|
|
80
|
+
if not mscore_exec:
|
|
81
|
+
mscore_exec = find_musescore_version(version=3)
|
|
82
|
+
if mscore_exec:
|
|
83
|
+
warnings.warn(
|
|
84
|
+
"Only Musescore 3 is installed. Consider upgrading to musescore 4."
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
mscore_exec = find_musescore_version(version="")
|
|
88
|
+
if mscore_exec:
|
|
89
|
+
warnings.warn(
|
|
90
|
+
"A unspecified version of MuseScore was found. Consider upgrading to musescore 4."
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
raise MuseScoreNotFoundException()
|
|
94
|
+
# check if a screen is available (only on Linux)
|
|
95
|
+
if "DISPLAY" not in os.environ and platform.system() == "Linux":
|
|
96
|
+
raise MuseScoreNotFoundException(
|
|
97
|
+
"Musescore Executable was found, but a screen is missing. Musescore needs a screen to load scores"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return mscore_exec
|
|
101
|
+
|
|
102
|
+
|
|
71
103
|
@deprecated_alias(fn="filename")
|
|
72
104
|
@deprecated_parameter("ensure_list")
|
|
73
105
|
def load_via_musescore(
|
|
@@ -103,15 +135,22 @@ or a list of these
|
|
|
103
135
|
One or more part or partgroup objects
|
|
104
136
|
|
|
105
137
|
"""
|
|
138
|
+
if filename.endswith(".mscz"):
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
# open the file as text and check if the first symbol is "<" to avoid
|
|
142
|
+
# further processing in case of non-XML files
|
|
143
|
+
with open(filename, "r") as f:
|
|
144
|
+
if f.read(1) != "<":
|
|
145
|
+
raise FileImportException(
|
|
146
|
+
"File {} is not a valid XML file.".format(filename)
|
|
147
|
+
)
|
|
106
148
|
|
|
107
|
-
mscore_exec =
|
|
108
|
-
|
|
109
|
-
if not mscore_exec:
|
|
110
|
-
raise MuseScoreNotFoundException()
|
|
149
|
+
mscore_exec = find_musescore()
|
|
111
150
|
|
|
112
151
|
xml_fh = os.path.splitext(os.path.basename(filename))[0] + ".musicxml"
|
|
113
152
|
|
|
114
|
-
cmd = [mscore_exec, "-o", xml_fh, filename]
|
|
153
|
+
cmd = [mscore_exec, "-o", xml_fh, filename, "-f"]
|
|
115
154
|
|
|
116
155
|
try:
|
|
117
156
|
ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
@@ -167,10 +206,7 @@ def render_musescore(
|
|
|
167
206
|
out : Optional[PathLike]
|
|
168
207
|
Path to the output generated image (or None if no image was generated)
|
|
169
208
|
"""
|
|
170
|
-
mscore_exec =
|
|
171
|
-
|
|
172
|
-
if not mscore_exec:
|
|
173
|
-
return None
|
|
209
|
+
mscore_exec = find_musescore()
|
|
174
210
|
|
|
175
211
|
if fmt not in ("png", "pdf"):
|
|
176
212
|
warnings.warn("warning: unsupported output format")
|
|
@@ -193,6 +229,7 @@ def render_musescore(
|
|
|
193
229
|
"-o",
|
|
194
230
|
os.fspath(img_fh),
|
|
195
231
|
os.fspath(xml_fh),
|
|
232
|
+
"-f",
|
|
196
233
|
]
|
|
197
234
|
try:
|
|
198
235
|
ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
partitura/performance.py
CHANGED
|
@@ -85,7 +85,11 @@ class PerformedPart(object):
|
|
|
85
85
|
super().__init__()
|
|
86
86
|
self.id = id
|
|
87
87
|
self.part_name = part_name
|
|
88
|
-
self.notes =
|
|
88
|
+
self.notes = list(
|
|
89
|
+
map(
|
|
90
|
+
lambda n: n if isinstance(n, PerformedNote) else PerformedNote(n), notes
|
|
91
|
+
)
|
|
92
|
+
)
|
|
89
93
|
self.controls = controls or []
|
|
90
94
|
self.programs = programs or []
|
|
91
95
|
self.ppq = ppq
|
|
@@ -203,7 +207,11 @@ class PerformedPart(object):
|
|
|
203
207
|
if "id" not in note_array.dtype.names:
|
|
204
208
|
n_ids = ["n{0}".format(i) for i in range(len(note_array))]
|
|
205
209
|
else:
|
|
206
|
-
|
|
210
|
+
# Check if all ids are the same
|
|
211
|
+
if np.all(note_array["id"] == note_array["id"][0]):
|
|
212
|
+
n_ids = ["n{0}".format(i) for i in range(len(note_array))]
|
|
213
|
+
else:
|
|
214
|
+
n_ids = note_array["id"]
|
|
207
215
|
|
|
208
216
|
if "track" not in note_array.dtype.names:
|
|
209
217
|
tracks = np.zeros(len(note_array), dtype=int)
|
|
@@ -280,6 +288,174 @@ def adjust_offsets_w_sustain(
|
|
|
280
288
|
note["sound_off"] = offset
|
|
281
289
|
|
|
282
290
|
|
|
291
|
+
class PerformedNote:
|
|
292
|
+
"""
|
|
293
|
+
A dictionary-like object representing a performed note.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
pnote_dict : dict
|
|
298
|
+
A dictionary containing performed note information.
|
|
299
|
+
This information can contain the following fields:
|
|
300
|
+
"id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off".
|
|
301
|
+
If not provided, the default values will be used.
|
|
302
|
+
Pitch, note_on, and note_off are required.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def __init__(self, pnote_dict):
|
|
306
|
+
self.pnote_dict = pnote_dict
|
|
307
|
+
self.pnote_dict["id"] = self.pnote_dict.get("id", None)
|
|
308
|
+
self.pnote_dict["pitch"] = self.pnote_dict.get("pitch", self["midi_pitch"])
|
|
309
|
+
self.pnote_dict["note_on"] = self.pnote_dict.get("note_on", -1)
|
|
310
|
+
self.pnote_dict["note_off"] = self.pnote_dict.get("note_off", -1)
|
|
311
|
+
self.pnote_dict["sound_off"] = self.pnote_dict.get(
|
|
312
|
+
"sound_off", self["note_off"]
|
|
313
|
+
)
|
|
314
|
+
self.pnote_dict["track"] = self.pnote_dict.get("track", 0)
|
|
315
|
+
self.pnote_dict["channel"] = self.pnote_dict.get("channel", 1)
|
|
316
|
+
self.pnote_dict["velocity"] = self.pnote_dict.get("velocity", 60)
|
|
317
|
+
self._validate_values(pnote_dict)
|
|
318
|
+
self._accepted_keys = [
|
|
319
|
+
"id",
|
|
320
|
+
"pitch",
|
|
321
|
+
"note_on",
|
|
322
|
+
"note_off",
|
|
323
|
+
"velocity",
|
|
324
|
+
"track",
|
|
325
|
+
"channel",
|
|
326
|
+
"sound_off",
|
|
327
|
+
"note_on_tick",
|
|
328
|
+
"note_off_tick",
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
def __str__(self):
|
|
332
|
+
return f"PerformedNote: {self['id']}"
|
|
333
|
+
|
|
334
|
+
def __eq__(self, other):
|
|
335
|
+
if not isinstance(PerformedNote):
|
|
336
|
+
return False
|
|
337
|
+
if not self.keys() == other.keys():
|
|
338
|
+
return False
|
|
339
|
+
return np.all(
|
|
340
|
+
np.array([self[k] == other[k] for k in self.keys() if k in other.keys()])
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def keys(self):
|
|
344
|
+
return self.pnote_dict.keys()
|
|
345
|
+
|
|
346
|
+
def get(self, key, default=None):
|
|
347
|
+
return self.pnote_dict.get(key, default)
|
|
348
|
+
|
|
349
|
+
def __hash__(self):
|
|
350
|
+
return hash(self["id"])
|
|
351
|
+
|
|
352
|
+
def __lt__(self, other):
|
|
353
|
+
return self["note_on"] < other["note_on"]
|
|
354
|
+
|
|
355
|
+
def __le__(self, other):
|
|
356
|
+
return self["note_on"] <= other["note_on"]
|
|
357
|
+
|
|
358
|
+
def __gt__(self, other):
|
|
359
|
+
return self["note_on"] > other["note_on"]
|
|
360
|
+
|
|
361
|
+
def __ge__(self, other):
|
|
362
|
+
return self["note_on"] >= other["note_on"]
|
|
363
|
+
|
|
364
|
+
def __getitem__(self, key):
|
|
365
|
+
return self.pnote_dict.get(key, None)
|
|
366
|
+
|
|
367
|
+
def __setitem__(self, key, value):
|
|
368
|
+
if key not in self._accepted_keys:
|
|
369
|
+
raise KeyError(f"Key {key} not accepted for PerformedNote")
|
|
370
|
+
self._validate_values((key, value))
|
|
371
|
+
self.pnote_dict[key] = value
|
|
372
|
+
|
|
373
|
+
def __delitem__(self, key):
|
|
374
|
+
raise KeyError("Cannot delete items from PerformedNote")
|
|
375
|
+
|
|
376
|
+
def __iter__(self):
|
|
377
|
+
return iter(self.keys())
|
|
378
|
+
|
|
379
|
+
def __len__(self):
|
|
380
|
+
return len(self.keys())
|
|
381
|
+
|
|
382
|
+
def __contains__(self, key):
|
|
383
|
+
return key in self.keys()
|
|
384
|
+
|
|
385
|
+
def copy(self):
|
|
386
|
+
return PerformedNote(self.pnote_dict.copy())
|
|
387
|
+
|
|
388
|
+
def _validate_values(self, d):
|
|
389
|
+
if isinstance(d, dict):
|
|
390
|
+
dd = d
|
|
391
|
+
elif isinstance(d, tuple):
|
|
392
|
+
dd = {d[0]: d[1]}
|
|
393
|
+
else:
|
|
394
|
+
raise ValueError(f"Invalid value {d} provided for PerformedNote")
|
|
395
|
+
|
|
396
|
+
for key, value in dd.items():
|
|
397
|
+
if key == "pitch":
|
|
398
|
+
self._validate_pitch(value)
|
|
399
|
+
elif key == "note_on":
|
|
400
|
+
self._validate_note_on(value)
|
|
401
|
+
elif key == "note_off":
|
|
402
|
+
self._validate_note_off(value)
|
|
403
|
+
elif key == "velocity":
|
|
404
|
+
self._validate_velocity(value)
|
|
405
|
+
elif key == "sound_off":
|
|
406
|
+
self._validate_sound_off(value)
|
|
407
|
+
elif key == "note_on_tick":
|
|
408
|
+
self._validate_note_on_tick(value)
|
|
409
|
+
elif key == "note_off_tick":
|
|
410
|
+
self._validate_note_off_tick(value)
|
|
411
|
+
|
|
412
|
+
def _validate_sound_off(self, value):
|
|
413
|
+
if self.get("note_off", -1) < 0:
|
|
414
|
+
return
|
|
415
|
+
if value < 0:
|
|
416
|
+
raise ValueError(f"sound_off must be greater than or equal to 0")
|
|
417
|
+
if value < self.pnote_dict["note_off"]:
|
|
418
|
+
raise ValueError(f"sound_off must be greater or equal to note_off")
|
|
419
|
+
|
|
420
|
+
def _validate_note_on(self, value):
|
|
421
|
+
if value < 0:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Note on value provided is invalid, must be greater than or equal to 0"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def _validate_note_off(self, value):
|
|
427
|
+
if self.pnote_dict.get("note_on", -1) < 0:
|
|
428
|
+
return
|
|
429
|
+
if value < 0 or value < self.pnote_dict["note_on"]:
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Note off value provided is invalid, "
|
|
432
|
+
f"must be a positive value greater than or equal to 0 and greater or equal to note_on"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def _validate_note_on_tick(self, value):
|
|
436
|
+
if value < 0:
|
|
437
|
+
raise ValueError(
|
|
438
|
+
f"Note on tick value provided is invalid, must be greater than or equal to 0"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _validate_note_off_tick(self, value):
|
|
442
|
+
if self.pnote_dict.get("note_on_tick", -1) < 0:
|
|
443
|
+
return
|
|
444
|
+
if value < 0 or value < self.pnote_dict["note_on_tick"]:
|
|
445
|
+
raise ValueError(
|
|
446
|
+
f"Note off tick value provided is invalid, "
|
|
447
|
+
f"must be a positive value greater than or equal to 0 and greater or equal to note_on_tick"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _validate_pitch(self, value):
|
|
451
|
+
if value > 127 or value < 0:
|
|
452
|
+
raise ValueError(f"pitch must be between 0 and 127")
|
|
453
|
+
|
|
454
|
+
def _validate_velocity(self, value):
|
|
455
|
+
if value > 127 or value < 0:
|
|
456
|
+
raise ValueError(f"velocity must be between 0 and 127")
|
|
457
|
+
|
|
458
|
+
|
|
283
459
|
class Performance(object):
|
|
284
460
|
"""Main object for representing a performance.
|
|
285
461
|
|
partitura/score.py
CHANGED
|
@@ -9,14 +9,14 @@ object). This object serves as a timeline at which musical elements
|
|
|
9
9
|
are registered in terms of their start and end times.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from copy import copy
|
|
12
|
+
from copy import copy, deepcopy
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
from collections.abc import Iterable
|
|
15
15
|
from numbers import Number
|
|
16
16
|
|
|
17
17
|
# import copy
|
|
18
18
|
from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES
|
|
19
|
-
import warnings
|
|
19
|
+
import warnings, sys
|
|
20
20
|
import numpy as np
|
|
21
21
|
from scipy.interpolate import PPoly
|
|
22
22
|
from typing import Union, List, Optional, Iterator, Iterable as Itertype
|
|
@@ -615,6 +615,18 @@ class Part(object):
|
|
|
615
615
|
"""
|
|
616
616
|
return [e for e in self.iter_all(LoudnessDirection, include_subclasses=True)]
|
|
617
617
|
|
|
618
|
+
@property
|
|
619
|
+
def tempo_directions(self):
|
|
620
|
+
"""Return a list of all tempo direction in the part
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
list
|
|
625
|
+
List of TempoDirection objects
|
|
626
|
+
|
|
627
|
+
"""
|
|
628
|
+
return [e for e in self.iter_all(TempoDirection, include_subclasses=True)]
|
|
629
|
+
|
|
618
630
|
@property
|
|
619
631
|
def articulations(self):
|
|
620
632
|
"""Return a list of all Articulation markings in the part
|
|
@@ -2990,8 +3002,16 @@ class Score(object):
|
|
|
2990
3002
|
the identifier should not start with a number.
|
|
2991
3003
|
partlist : `Part`, `PartGroup` or list of `Part` or `PartGroup` instances.
|
|
2992
3004
|
List of `Part` or `PartGroup` objects.
|
|
2993
|
-
|
|
2994
|
-
|
|
3005
|
+
work_title: str, optional
|
|
3006
|
+
Work title of the score, if applicable.
|
|
3007
|
+
work_number: str, optional
|
|
3008
|
+
Work number of the score, if applicable.
|
|
3009
|
+
movement_title: str, optional
|
|
3010
|
+
Movement title of the score, if applicable.
|
|
3011
|
+
movement_number: str, optional
|
|
3012
|
+
Movement number of the score, if applicable.
|
|
3013
|
+
title : str, optional
|
|
3014
|
+
Title of the score, from <credit-words> tag
|
|
2995
3015
|
subtitle: str, optional
|
|
2996
3016
|
Subtitle of the score.
|
|
2997
3017
|
composer: str, optional
|
|
@@ -3010,7 +3030,13 @@ class Score(object):
|
|
|
3010
3030
|
part_structure: list of `Part` or `PartGrop`
|
|
3011
3031
|
List of all `Part` or `PartGroup` objects that specify the structure of
|
|
3012
3032
|
the score.
|
|
3013
|
-
|
|
3033
|
+
work_title: str
|
|
3034
|
+
See parameters.
|
|
3035
|
+
work_number: str
|
|
3036
|
+
See parameters.
|
|
3037
|
+
movement_title: str
|
|
3038
|
+
See parameters.
|
|
3039
|
+
movement_number: str
|
|
3014
3040
|
See parameters.
|
|
3015
3041
|
subtitle: str
|
|
3016
3042
|
See parameters.
|
|
@@ -3024,6 +3050,10 @@ class Score(object):
|
|
|
3024
3050
|
"""
|
|
3025
3051
|
|
|
3026
3052
|
id: Optional[str]
|
|
3053
|
+
work_title: Optional[str]
|
|
3054
|
+
work_number: Optional[str]
|
|
3055
|
+
movement_title: Optional[str]
|
|
3056
|
+
movement_number: Optional[str]
|
|
3027
3057
|
title: Optional[str]
|
|
3028
3058
|
subtitle: Optional[str]
|
|
3029
3059
|
composer: Optional[str]
|
|
@@ -3036,6 +3066,10 @@ class Score(object):
|
|
|
3036
3066
|
self,
|
|
3037
3067
|
partlist: Union[Part, PartGroup, Itertype[Union[Part, PartGroup]]],
|
|
3038
3068
|
id: Optional[str] = None,
|
|
3069
|
+
work_title: Optional[str] = None,
|
|
3070
|
+
work_number: Optional[str] = None,
|
|
3071
|
+
movement_title: Optional[str] = None,
|
|
3072
|
+
movement_number: Optional[str] = None,
|
|
3039
3073
|
title: Optional[str] = None,
|
|
3040
3074
|
subtitle: Optional[str] = None,
|
|
3041
3075
|
composer: Optional[str] = None,
|
|
@@ -3045,6 +3079,10 @@ class Score(object):
|
|
|
3045
3079
|
self.id = id
|
|
3046
3080
|
|
|
3047
3081
|
# Score Information (default from MuseScore/MusicXML)
|
|
3082
|
+
self.work_title = work_title
|
|
3083
|
+
self.work_number = work_number
|
|
3084
|
+
self.movement_title = movement_title
|
|
3085
|
+
self.movement_number = movement_number
|
|
3048
3086
|
self.title = title
|
|
3049
3087
|
self.subtitle = subtitle
|
|
3050
3088
|
self.composer = composer
|
|
@@ -4588,15 +4626,15 @@ def iter_unfolded_parts(part, update_ids=True):
|
|
|
4588
4626
|
|
|
4589
4627
|
|
|
4590
4628
|
# UPDATED VERSION
|
|
4591
|
-
def unfold_part_maximal(
|
|
4592
|
-
"""Return the "maximally" unfolded part, that is, a copy of the
|
|
4629
|
+
def unfold_part_maximal(score: ScoreLike, update_ids=True, ignore_leaps=True):
|
|
4630
|
+
"""Return the "maximally" unfolded part/score, that is, a copy of the
|
|
4593
4631
|
part where all segments marked with repeat signs are included
|
|
4594
4632
|
twice.
|
|
4595
4633
|
|
|
4596
4634
|
Parameters
|
|
4597
4635
|
----------
|
|
4598
|
-
|
|
4599
|
-
The Part to unfold.
|
|
4636
|
+
score : ScoreLike
|
|
4637
|
+
The Part/Score to unfold.
|
|
4600
4638
|
update_ids : bool (optional)
|
|
4601
4639
|
Update note ids to reflect the repetitions. Note IDs will have
|
|
4602
4640
|
a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
|
|
@@ -4609,19 +4647,75 @@ def unfold_part_maximal(part, update_ids=True, ignore_leaps=True):
|
|
|
4609
4647
|
|
|
4610
4648
|
Returns
|
|
4611
4649
|
-------
|
|
4612
|
-
unfolded_part :
|
|
4613
|
-
The unfolded Part
|
|
4650
|
+
unfolded_part : ScoreLike
|
|
4651
|
+
The unfolded Part/Score
|
|
4614
4652
|
|
|
4615
4653
|
"""
|
|
4654
|
+
if isinstance(score, Score):
|
|
4655
|
+
# Copy needs to be deep, otherwise the recursion limit will be exceeded
|
|
4656
|
+
old_recursion_depth = sys.getrecursionlimit()
|
|
4657
|
+
sys.setrecursionlimit(10000)
|
|
4658
|
+
# Deep copy of score
|
|
4659
|
+
new_score = deepcopy(score)
|
|
4660
|
+
# Reset recursion limit to previous value to avoid side effects
|
|
4661
|
+
sys.setrecursionlimit(old_recursion_depth)
|
|
4662
|
+
new_partlist = list()
|
|
4663
|
+
for score in new_score.parts:
|
|
4664
|
+
unfolded_part = unfold_part_maximal(
|
|
4665
|
+
score, update_ids=update_ids, ignore_leaps=ignore_leaps
|
|
4666
|
+
)
|
|
4667
|
+
new_partlist.append(unfolded_part)
|
|
4668
|
+
new_score.parts = new_partlist
|
|
4669
|
+
return new_score
|
|
4616
4670
|
|
|
4617
4671
|
paths = get_paths(
|
|
4618
|
-
|
|
4672
|
+
score, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps
|
|
4619
4673
|
)
|
|
4620
4674
|
|
|
4621
|
-
unfolded_part = new_part_from_path(paths[0],
|
|
4675
|
+
unfolded_part = new_part_from_path(paths[0], score, update_ids=update_ids)
|
|
4622
4676
|
return unfolded_part
|
|
4623
4677
|
|
|
4624
4678
|
|
|
4679
|
+
def unfold_part_minimal(score: ScoreLike):
|
|
4680
|
+
"""Return the "minimally" unfolded score/part, that is, a copy of the
|
|
4681
|
+
part where all segments marked with repeat are included only once.
|
|
4682
|
+
For voltas only the last volta segment is included.
|
|
4683
|
+
Note this might not be musically valid, e.g. a passing a "fine" even a first time will stop this unfolding.
|
|
4684
|
+
Warning: The unfolding of repeats is computed part-wise, inconsistent repeat markings of parts of a single result
|
|
4685
|
+
in inconsistent unfoldings.
|
|
4686
|
+
|
|
4687
|
+
Parameters
|
|
4688
|
+
----------
|
|
4689
|
+
score: ScoreLike
|
|
4690
|
+
The score/part to unfold.
|
|
4691
|
+
|
|
4692
|
+
Returns
|
|
4693
|
+
-------
|
|
4694
|
+
unfolded_score : ScoreLike
|
|
4695
|
+
The unfolded Part
|
|
4696
|
+
|
|
4697
|
+
"""
|
|
4698
|
+
if isinstance(score, Score):
|
|
4699
|
+
# Copy needs to be deep, otherwise the recursion limit will be exceeded
|
|
4700
|
+
old_recursion_depth = sys.getrecursionlimit()
|
|
4701
|
+
sys.setrecursionlimit(10000)
|
|
4702
|
+
# Deep copy of score
|
|
4703
|
+
unfolded_score = deepcopy(score)
|
|
4704
|
+
# Reset recursion limit to previous value to avoid side effects
|
|
4705
|
+
sys.setrecursionlimit(old_recursion_depth)
|
|
4706
|
+
new_partlist = list()
|
|
4707
|
+
for part in unfolded_score.parts:
|
|
4708
|
+
unfolded_part = unfold_part_minimal(part)
|
|
4709
|
+
new_partlist.append(unfolded_part)
|
|
4710
|
+
unfolded_score.parts = new_partlist
|
|
4711
|
+
return unfolded_score
|
|
4712
|
+
|
|
4713
|
+
paths = get_paths(score, no_repeats=True, all_repeats=False, ignore_leap_info=True)
|
|
4714
|
+
|
|
4715
|
+
unfolded_score = new_part_from_path(paths[0], score, update_ids=False)
|
|
4716
|
+
return unfolded_score
|
|
4717
|
+
|
|
4718
|
+
|
|
4625
4719
|
# UPDATED / UNCHANGED VERSION
|
|
4626
4720
|
def unfold_part_alignment(part, alignment):
|
|
4627
4721
|
"""Return the unfolded part given an alignment, that is, a copy
|
partitura/utils/music.py
CHANGED
|
@@ -488,8 +488,15 @@ def transpose(score: ScoreLike, interval: Interval) -> ScoreLike:
|
|
|
488
488
|
Transposed score.
|
|
489
489
|
"""
|
|
490
490
|
import partitura.score as s
|
|
491
|
+
import sys
|
|
491
492
|
|
|
493
|
+
# Copy needs to be deep, otherwise the recursion limit will be exceeded
|
|
494
|
+
old_recursion_depth = sys.getrecursionlimit()
|
|
495
|
+
sys.setrecursionlimit(10000)
|
|
496
|
+
# Deep copy of score
|
|
492
497
|
new_score = copy.deepcopy(score)
|
|
498
|
+
# Reset recursion limit to previous value to avoid side effects
|
|
499
|
+
sys.setrecursionlimit(old_recursion_depth)
|
|
493
500
|
if isinstance(score, s.Score):
|
|
494
501
|
for part in new_score.parts:
|
|
495
502
|
transpose(part, interval)
|
|
@@ -686,6 +693,8 @@ def midi_ticks_to_seconds(
|
|
|
686
693
|
|
|
687
694
|
SIGN_TO_ALTER = {
|
|
688
695
|
"n": 0,
|
|
696
|
+
"ns": 1,
|
|
697
|
+
"nf": -1,
|
|
689
698
|
"#": 1,
|
|
690
699
|
"s": 1,
|
|
691
700
|
"ss": 2,
|
|
@@ -3252,18 +3261,17 @@ def slice_ppart_by_time(
|
|
|
3252
3261
|
# create a new (empty) instance of a PerformedPart
|
|
3253
3262
|
# single dummy note added to be able to set sustain_pedal_threshold in __init__
|
|
3254
3263
|
# -> check `adjust_offsets_w_sustain` in partitura.performance
|
|
3255
|
-
ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0}])
|
|
3264
|
+
# ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0, "pitch": 0}])
|
|
3256
3265
|
|
|
3257
3266
|
# get ppq if PerformedPart contains it,
|
|
3258
3267
|
# else skip time_tick info when e.g. created with 'load_performance_midi'
|
|
3259
3268
|
try:
|
|
3260
3269
|
ppq = ppart.ppq
|
|
3261
|
-
ppart_slice.ppq = ppq
|
|
3262
3270
|
except AttributeError:
|
|
3263
3271
|
ppq = None
|
|
3264
3272
|
|
|
3273
|
+
controls_slice = []
|
|
3265
3274
|
if ppart.controls:
|
|
3266
|
-
controls_slice = []
|
|
3267
3275
|
for cc in ppart.controls:
|
|
3268
3276
|
if cc["time"] >= start_time and cc["time"] <= end_time:
|
|
3269
3277
|
new_cc = cc.copy()
|
|
@@ -3271,10 +3279,9 @@ def slice_ppart_by_time(
|
|
|
3271
3279
|
if ppq:
|
|
3272
3280
|
new_cc["time_tick"] = int(2 * ppq * cc["time"])
|
|
3273
3281
|
controls_slice.append(new_cc)
|
|
3274
|
-
ppart_slice.controls = controls_slice
|
|
3275
3282
|
|
|
3283
|
+
programs_slice = []
|
|
3276
3284
|
if ppart.programs:
|
|
3277
|
-
programs_slice = []
|
|
3278
3285
|
for pr in ppart.programs:
|
|
3279
3286
|
if pr["time"] >= start_time and pr["time"] <= end_time:
|
|
3280
3287
|
new_pr = pr.copy()
|
|
@@ -3282,7 +3289,6 @@ def slice_ppart_by_time(
|
|
|
3282
3289
|
if ppq:
|
|
3283
3290
|
new_pr["time_tick"] = int(2 * ppq * pr["time"])
|
|
3284
3291
|
programs_slice.append(new_pr)
|
|
3285
|
-
ppart_slice.programs = programs_slice
|
|
3286
3292
|
|
|
3287
3293
|
notes_slice = []
|
|
3288
3294
|
note_id = 0
|
|
@@ -3326,7 +3332,10 @@ def slice_ppart_by_time(
|
|
|
3326
3332
|
else:
|
|
3327
3333
|
break
|
|
3328
3334
|
|
|
3329
|
-
|
|
3335
|
+
# Create slice PerformedPart
|
|
3336
|
+
ppart_slice = PerformedPart(
|
|
3337
|
+
notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq
|
|
3338
|
+
)
|
|
3330
3339
|
|
|
3331
3340
|
# set threshold property after creating notes list to update 'sound_offset' values
|
|
3332
3341
|
ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: partitura
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A package for handling symbolic musical information
|
|
5
5
|
Home-page: https://github.com/CPJKU/partitura
|
|
6
|
-
Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier
|
|
6
|
+
Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu
|
|
7
7
|
Author-email: partitura-users@googlegroups.com
|
|
8
8
|
License: Apache 2.0
|
|
9
9
|
Keywords: music notation musicxml midi
|