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/importmei.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"""
|
|
4
4
|
This module contains methods for importing MEI files.
|
|
5
5
|
"""
|
|
6
|
+
from collections import OrderedDict
|
|
6
7
|
from lxml import etree
|
|
8
|
+
from fractions import Fraction
|
|
7
9
|
from xmlschema.names import XML_NAMESPACE
|
|
8
10
|
import partitura.score as score
|
|
9
11
|
from partitura.utils.music import (
|
|
@@ -68,6 +70,11 @@ class MeiParser(object):
|
|
|
68
70
|
self.parts = (
|
|
69
71
|
None # parts get initialized in create_parts() and filled in fill_parts()
|
|
70
72
|
)
|
|
73
|
+
# find the music tag inside the document
|
|
74
|
+
music_el = self.document.findall(self._ns_name("music", all=True))
|
|
75
|
+
if len(music_el) != 1:
|
|
76
|
+
raise Exception("Only MEI with a single <music> element are supported")
|
|
77
|
+
self.music_el = music_el[0]
|
|
71
78
|
self.repetitions = (
|
|
72
79
|
[]
|
|
73
80
|
) # to be filled when we encounter repetitions and process in the end
|
|
@@ -78,20 +85,21 @@ class MeiParser(object):
|
|
|
78
85
|
|
|
79
86
|
def create_parts(self):
|
|
80
87
|
# handle main scoreDef info: create the part list
|
|
81
|
-
main_partgroup_el = self.
|
|
88
|
+
main_partgroup_el = self.music_el.find(self._ns_name("staffGrp", all=True))
|
|
82
89
|
self.parts = self._handle_main_staff_group(main_partgroup_el)
|
|
83
90
|
|
|
84
91
|
def fill_parts(self):
|
|
85
92
|
# fill parts with the content of the score
|
|
86
|
-
scores_el = self.
|
|
93
|
+
scores_el = self.music_el.findall(self._ns_name("score", all=True))
|
|
87
94
|
if len(scores_el) != 1:
|
|
88
95
|
raise Exception("Only MEI with a single score element are supported")
|
|
89
96
|
sections_el = scores_el[0].findall(self._ns_name("section"))
|
|
90
97
|
position = 0
|
|
98
|
+
measure_number = 1
|
|
91
99
|
for section_el in sections_el:
|
|
92
100
|
# insert in parts all elements except ties
|
|
93
|
-
position = self._handle_section(
|
|
94
|
-
section_el, list(score.iter_parts(self.parts)), position
|
|
101
|
+
position, measure_number = self._handle_section(
|
|
102
|
+
section_el, list(score.iter_parts(self.parts)), position, measure_number
|
|
95
103
|
)
|
|
96
104
|
|
|
97
105
|
# handles ties
|
|
@@ -351,27 +359,39 @@ class MeiParser(object):
|
|
|
351
359
|
self._handle_clef(staffdef_el, position, part)
|
|
352
360
|
|
|
353
361
|
def _intsymdur_from_symbolic(self, symbolic_dur):
|
|
354
|
-
"""Produce a int symbolic dur (e.g.
|
|
355
|
-
|
|
362
|
+
"""Produce a int symbolic dur (e.g. 8 is a eight note), a dot number, and a tuplet modifier,
|
|
363
|
+
e.g., (2,3) means there are 3 notes in the space of 2 notes."""
|
|
356
364
|
intsymdur = SYMBOLIC_TO_INT_DURS[symbolic_dur["type"]]
|
|
357
365
|
# deals with tuplets
|
|
358
366
|
if symbolic_dur.get("actual_notes") is not None:
|
|
359
367
|
assert symbolic_dur.get("normal_notes") is not None
|
|
360
|
-
intsymdur = (
|
|
361
|
-
|
|
368
|
+
# intsymdur = (
|
|
369
|
+
# intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"]
|
|
370
|
+
# )
|
|
371
|
+
tuplet_modifier = (
|
|
372
|
+
symbolic_dur["normal_notes"],
|
|
373
|
+
symbolic_dur["actual_notes"],
|
|
362
374
|
)
|
|
375
|
+
else:
|
|
376
|
+
tuplet_modifier = None
|
|
363
377
|
# deals with dots
|
|
364
378
|
dots = symbolic_dur.get("dots") if symbolic_dur.get("dots") is not None else 0
|
|
365
|
-
return intsymdur, dots
|
|
379
|
+
return intsymdur, dots, tuplet_modifier
|
|
366
380
|
|
|
367
381
|
def _find_ppq(self):
|
|
368
382
|
"""Finds the ppq for MEI filed that do not explicitely encode this information"""
|
|
369
|
-
els_with_dur = self.
|
|
383
|
+
els_with_dur = self.music_el.xpath(".//*[@dur]")
|
|
370
384
|
durs = []
|
|
371
385
|
durs_ppq = []
|
|
372
386
|
for el in els_with_dur:
|
|
373
387
|
symbolic_duration = self._get_symbolic_duration(el)
|
|
374
|
-
intsymdur, dots = self._intsymdur_from_symbolic(
|
|
388
|
+
intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic(
|
|
389
|
+
symbolic_duration
|
|
390
|
+
)
|
|
391
|
+
if tuplet_mod is not None:
|
|
392
|
+
# consider time modifications keeping the numerator of the minimized fraction
|
|
393
|
+
minimized_fraction = Fraction(intsymdur * tuplet_mod[1], tuplet_mod[0])
|
|
394
|
+
intsymdur = minimized_fraction.numerator
|
|
375
395
|
# double the value if we have dots, to be sure be able to encode that with integers in partitura
|
|
376
396
|
durs.append(intsymdur * (2**dots))
|
|
377
397
|
durs_ppq.append(
|
|
@@ -574,9 +594,16 @@ class MeiParser(object):
|
|
|
574
594
|
duration = 0 if el.get("grace") is not None else int(el.get("dur.ppq"))
|
|
575
595
|
else:
|
|
576
596
|
# compute the duration from the symbolic duration
|
|
577
|
-
intsymdur, dots = self._intsymdur_from_symbolic(
|
|
597
|
+
intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic(
|
|
598
|
+
symbolic_duration
|
|
599
|
+
)
|
|
578
600
|
divs = part._quarter_durations[0] # divs is the same as ppq
|
|
579
|
-
|
|
601
|
+
if tuplet_mod is None:
|
|
602
|
+
tuplet_mod = (
|
|
603
|
+
1,
|
|
604
|
+
1,
|
|
605
|
+
) # if no tuplet modifier, set one that does not change the duration
|
|
606
|
+
duration = (divs * 4 * tuplet_mod[0]) / (intsymdur * tuplet_mod[1])
|
|
580
607
|
for d in range(dots):
|
|
581
608
|
duration = duration + 0.5 * duration
|
|
582
609
|
# sanity check to verify the divs are correctly set
|
|
@@ -728,6 +755,55 @@ class MeiParser(object):
|
|
|
728
755
|
)
|
|
729
756
|
# add mrest to the part
|
|
730
757
|
part.add(rest, position, position + parts_per_measure)
|
|
758
|
+
# return duration to update the position in the layer
|
|
759
|
+
return position + parts_per_measure
|
|
760
|
+
|
|
761
|
+
def _handle_multirest(self, multirest_el, position, voice, staff, part):
|
|
762
|
+
"""
|
|
763
|
+
Handles a rest that spawn multiple measures
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
multirest_el : lxml tree
|
|
768
|
+
A mrest element in the lxml tree.
|
|
769
|
+
position : int
|
|
770
|
+
The current position on the timeline.
|
|
771
|
+
voice : int
|
|
772
|
+
The voice of the section.
|
|
773
|
+
staff : int
|
|
774
|
+
The current staff also refers to a Part.
|
|
775
|
+
part : Partitura.Part
|
|
776
|
+
The created part to add elements to.
|
|
777
|
+
|
|
778
|
+
Returns
|
|
779
|
+
-------
|
|
780
|
+
position + duration : int
|
|
781
|
+
Next position on the timeline.
|
|
782
|
+
"""
|
|
783
|
+
# find id
|
|
784
|
+
multirest_id = multirest_el.attrib[self._ns_name("id", XML_NAMESPACE)]
|
|
785
|
+
# find how many measures
|
|
786
|
+
n_measures = int(multirest_el.attrib["num"])
|
|
787
|
+
if n_measures > 1:
|
|
788
|
+
raise Exception(
|
|
789
|
+
f"Multi-rests with more than 1 measure are not supported yet. Found one with {n_measures}."
|
|
790
|
+
)
|
|
791
|
+
# find closest time signature
|
|
792
|
+
last_ts = list(part.iter_all(cls=score.TimeSignature))[-1]
|
|
793
|
+
# find divs per measure
|
|
794
|
+
ppq = part.quarter_duration_map(position)
|
|
795
|
+
parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type)
|
|
796
|
+
|
|
797
|
+
# create dummy rest to insert in the timeline
|
|
798
|
+
rest = score.Rest(
|
|
799
|
+
id=multirest_id,
|
|
800
|
+
voice=voice,
|
|
801
|
+
staff=1,
|
|
802
|
+
symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq),
|
|
803
|
+
articulations=None,
|
|
804
|
+
)
|
|
805
|
+
# add mrest to the part
|
|
806
|
+
part.add(rest, position, position + parts_per_measure)
|
|
731
807
|
# now iterate
|
|
732
808
|
# return duration to update the position in the layer
|
|
733
809
|
return position + parts_per_measure
|
|
@@ -780,7 +856,20 @@ class MeiParser(object):
|
|
|
780
856
|
|
|
781
857
|
def _handle_space(self, e, position, part):
|
|
782
858
|
"""Moves current position."""
|
|
783
|
-
|
|
859
|
+
try:
|
|
860
|
+
space_id, duration, symbolic_duration = self._duration_info(e, part)
|
|
861
|
+
except (
|
|
862
|
+
KeyError
|
|
863
|
+
): # if the space don't have a duration, move to the end of the measure
|
|
864
|
+
# find closest time signature
|
|
865
|
+
last_ts = list(part.iter_all(cls=score.TimeSignature))[-1]
|
|
866
|
+
# find divs per measure
|
|
867
|
+
ppq = part.quarter_duration_map(position)
|
|
868
|
+
parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type)
|
|
869
|
+
# find divs elapsed since last barline
|
|
870
|
+
last_barline = list(part.iter_all(cls=pt.score.Measure))[-1]
|
|
871
|
+
duration = position - last_barline.start.t
|
|
872
|
+
|
|
784
873
|
return position + duration
|
|
785
874
|
|
|
786
875
|
def _handle_barline_symbols(self, measure_el, position: int, left_or_right: str):
|
|
@@ -823,6 +912,12 @@ class MeiParser(object):
|
|
|
823
912
|
new_position = self._handle_mrest(
|
|
824
913
|
e, position, ind_layer, ind_staff, part
|
|
825
914
|
)
|
|
915
|
+
elif e.tag == self._ns_name(
|
|
916
|
+
"multiRest"
|
|
917
|
+
): # rest that spawn more than one measure
|
|
918
|
+
new_position = self._handle_multirest(
|
|
919
|
+
e, position, ind_layer, ind_staff, part
|
|
920
|
+
)
|
|
826
921
|
elif e.tag == self._ns_name("beam"):
|
|
827
922
|
# TODO : add Beam element
|
|
828
923
|
# recursive call to the elements inside beam
|
|
@@ -846,7 +941,14 @@ class MeiParser(object):
|
|
|
846
941
|
position = new_position
|
|
847
942
|
return position
|
|
848
943
|
|
|
849
|
-
def _handle_staff_in_measure(
|
|
944
|
+
def _handle_staff_in_measure(
|
|
945
|
+
self,
|
|
946
|
+
staff_el,
|
|
947
|
+
staff_ind,
|
|
948
|
+
position: int,
|
|
949
|
+
part: pt.score.Part,
|
|
950
|
+
measure_number: int,
|
|
951
|
+
):
|
|
850
952
|
"""
|
|
851
953
|
Handles staffs inside a measure element.
|
|
852
954
|
|
|
@@ -860,6 +962,9 @@ class MeiParser(object):
|
|
|
860
962
|
The current position on the timeline.
|
|
861
963
|
part : Partitura.Part
|
|
862
964
|
The created partitura part object.
|
|
965
|
+
measure_number : int
|
|
966
|
+
The number of the measure. This number is independent of the measure name specified in the score.
|
|
967
|
+
It starts from 1 and always increases by 1 at each measure
|
|
863
968
|
|
|
864
969
|
Returns
|
|
865
970
|
-------
|
|
@@ -867,7 +972,9 @@ class MeiParser(object):
|
|
|
867
972
|
The final position on the timeline.
|
|
868
973
|
"""
|
|
869
974
|
# add measure
|
|
870
|
-
measure = score.Measure(
|
|
975
|
+
measure = score.Measure(
|
|
976
|
+
number=measure_number, name=staff_el.getparent().get("n")
|
|
977
|
+
)
|
|
871
978
|
part.add(measure, position)
|
|
872
979
|
|
|
873
980
|
layers_el = staff_el.findall(self._ns_name("layer"))
|
|
@@ -921,7 +1028,7 @@ class MeiParser(object):
|
|
|
921
1028
|
for dir_el in dir_els:
|
|
922
1029
|
self._handle_dir_element(dir_el, position)
|
|
923
1030
|
|
|
924
|
-
def _handle_section(self, section_el, parts, position: int):
|
|
1031
|
+
def _handle_section(self, section_el, parts, position: int, measure_number: int):
|
|
925
1032
|
"""
|
|
926
1033
|
Returns position and fills parts with elements.
|
|
927
1034
|
|
|
@@ -933,11 +1040,15 @@ class MeiParser(object):
|
|
|
933
1040
|
A list of partitura Parts.
|
|
934
1041
|
position : int
|
|
935
1042
|
The current position on the timeline.
|
|
1043
|
+
measure_number : int
|
|
1044
|
+
The current measure_number
|
|
936
1045
|
|
|
937
1046
|
Returns
|
|
938
1047
|
-------
|
|
939
1048
|
position : int
|
|
940
1049
|
The end position of the section.
|
|
1050
|
+
measure_number : int
|
|
1051
|
+
The number of the last measure.
|
|
941
1052
|
"""
|
|
942
1053
|
for i_el, element in enumerate(section_el):
|
|
943
1054
|
# handle measures
|
|
@@ -951,7 +1062,9 @@ class MeiParser(object):
|
|
|
951
1062
|
end_positions = []
|
|
952
1063
|
for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)):
|
|
953
1064
|
end_positions.append(
|
|
954
|
-
self._handle_staff_in_measure(
|
|
1065
|
+
self._handle_staff_in_measure(
|
|
1066
|
+
staff_el, i_s + 1, position, part, measure_number
|
|
1067
|
+
)
|
|
955
1068
|
)
|
|
956
1069
|
# handle directives (dir elements)
|
|
957
1070
|
self._handle_directives(element, position)
|
|
@@ -961,19 +1074,19 @@ class MeiParser(object):
|
|
|
961
1074
|
warnings.warn(
|
|
962
1075
|
f"Warning : parts have measures of different duration in measure {element.attrib[self._ns_name('id',XML_NAMESPACE)]}"
|
|
963
1076
|
)
|
|
964
|
-
# enlarge measures to the max
|
|
965
|
-
for part in parts:
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
part.remove(last_measure)
|
|
1077
|
+
# # enlarge measures to the max
|
|
1078
|
+
# for part in parts:
|
|
1079
|
+
# last_measure = list(part.iter_all(pt.score.Measure))[-1]
|
|
1080
|
+
# if last_measure.end.t != max_position:
|
|
1081
|
+
# part.add(
|
|
1082
|
+
# pt.score.Measure(number=last_measure.number),
|
|
1083
|
+
# max_position
|
|
1084
|
+
# )
|
|
1085
|
+
# part.remove(last_measure)
|
|
974
1086
|
position = max_position
|
|
975
1087
|
# handle right barline symbol
|
|
976
1088
|
self._handle_barline_symbols(element, position, "right")
|
|
1089
|
+
measure_number += 1
|
|
977
1090
|
# handle staffDef elements
|
|
978
1091
|
elif element.tag == self._ns_name("scoreDef"):
|
|
979
1092
|
# meter modifications
|
|
@@ -990,10 +1103,14 @@ class MeiParser(object):
|
|
|
990
1103
|
self._handle_keysig(element, position, part)
|
|
991
1104
|
# handle nested section
|
|
992
1105
|
elif element.tag == self._ns_name("section"):
|
|
993
|
-
position = self._handle_section(
|
|
1106
|
+
position, measure_number = self._handle_section(
|
|
1107
|
+
element, parts, position, measure_number
|
|
1108
|
+
)
|
|
994
1109
|
elif element.tag == self._ns_name("ending"):
|
|
995
1110
|
ending_start = position
|
|
996
|
-
position = self._handle_section(
|
|
1111
|
+
position, measure_number = self._handle_section(
|
|
1112
|
+
element, parts, position, measure_number
|
|
1113
|
+
)
|
|
997
1114
|
# insert the ending element
|
|
998
1115
|
ending_number = int(re.sub("[^0-9]", "", element.attrib["n"]))
|
|
999
1116
|
self._add_ending(ending_start, position, ending_number, parts)
|
|
@@ -1009,7 +1126,7 @@ class MeiParser(object):
|
|
|
1009
1126
|
else:
|
|
1010
1127
|
raise Exception(f"element {element.tag} is not yet supported")
|
|
1011
1128
|
|
|
1012
|
-
return position
|
|
1129
|
+
return position, measure_number
|
|
1013
1130
|
|
|
1014
1131
|
def _add_ending(self, start_ending, end_ending, ending_string, parts):
|
|
1015
1132
|
for part in score.iter_parts(parts):
|
|
@@ -1024,7 +1141,7 @@ class MeiParser(object):
|
|
|
1024
1141
|
all_notes = [
|
|
1025
1142
|
note
|
|
1026
1143
|
for part in score.iter_parts(part_list)
|
|
1027
|
-
for note in part.iter_all(cls=score.Note)
|
|
1144
|
+
for note in part.iter_all(cls=score.Note, include_subclasses=True)
|
|
1028
1145
|
]
|
|
1029
1146
|
all_notes_dict = {note.id: note for note in all_notes}
|
|
1030
1147
|
for tie_el in ties_el:
|
|
@@ -1052,6 +1169,7 @@ class MeiParser(object):
|
|
|
1052
1169
|
"WARNING : unmatched repetitions. adding a repetition start at position 0"
|
|
1053
1170
|
)
|
|
1054
1171
|
self.repetitions.insert(0, {"type": "start", "pos": 0})
|
|
1172
|
+
|
|
1055
1173
|
status = "stop"
|
|
1056
1174
|
sanitized_repetition_list = []
|
|
1057
1175
|
# check if start-stop are alternate
|
|
@@ -1082,11 +1200,19 @@ class MeiParser(object):
|
|
|
1082
1200
|
# check if ending with a start
|
|
1083
1201
|
if sanitized_repetition_list[-1] == "start":
|
|
1084
1202
|
print("WARNING : unmatched repetitions. Ignoring last start")
|
|
1203
|
+
## sanitize the found repetitions to remove duplicates
|
|
1204
|
+
sanitized_repetition_list = list(
|
|
1205
|
+
OrderedDict(
|
|
1206
|
+
(tuple(d.items()), d) for d in sanitized_repetition_list
|
|
1207
|
+
).values()
|
|
1208
|
+
)
|
|
1085
1209
|
self.repetitions = sanitized_repetition_list
|
|
1086
1210
|
|
|
1087
1211
|
## insert the repetitions to all parts
|
|
1088
1212
|
for rep_start, rep_stop in zip(self.repetitions[:-1:2], self.repetitions[1::2]):
|
|
1089
|
-
assert
|
|
1213
|
+
assert (
|
|
1214
|
+
rep_start["type"] == "start" and rep_stop["type"] == "stop"
|
|
1215
|
+
), "Something wrong with repetitions"
|
|
1090
1216
|
for part in score.iter_parts(self.parts):
|
|
1091
1217
|
part.add(score.Repeat(), rep_start["pos"], rep_stop["pos"])
|
|
1092
1218
|
|
partitura/io/importmidi.py
CHANGED
|
@@ -183,11 +183,10 @@ def load_performance_midi(
|
|
|
183
183
|
# end note if it's a 'note off' event or 'note on' with velocity 0
|
|
184
184
|
elif note_off or (note_on and msg.velocity == 0):
|
|
185
185
|
if note not in sounding_notes:
|
|
186
|
-
warnings.warn("ignoring MIDI message
|
|
186
|
+
warnings.warn(f"ignoring MIDI message {msg}")
|
|
187
187
|
continue
|
|
188
188
|
|
|
189
189
|
# append the note to the list associated with the channel
|
|
190
|
-
|
|
191
190
|
notes.append(
|
|
192
191
|
dict(
|
|
193
192
|
# id=f"n{len(notes)}",
|
|
@@ -473,6 +472,28 @@ or a list of these
|
|
|
473
472
|
else:
|
|
474
473
|
note_ids = [None for i in range(len(note_array))]
|
|
475
474
|
|
|
475
|
+
## sanitize time signature, when they are only present in one track, and no global is set
|
|
476
|
+
# find the number of ts per each track
|
|
477
|
+
number_of_time_sig_per_track = [
|
|
478
|
+
len(time_sigs_by_track[t]) for t in key_sigs_by_track.keys()
|
|
479
|
+
]
|
|
480
|
+
# if one track has 0 ts, and another has !=0 ts, and no global_time_sigs is present, sanitize
|
|
481
|
+
# all key signatures are copied to global, and the track ts are removed
|
|
482
|
+
if (
|
|
483
|
+
len(global_time_sigs) == 0
|
|
484
|
+
and min(number_of_time_sig_per_track) == 0
|
|
485
|
+
and max(number_of_time_sig_per_track) != 0
|
|
486
|
+
):
|
|
487
|
+
warnings.warn(
|
|
488
|
+
"Sanitizing time signatures. They will be shared across all tracks."
|
|
489
|
+
)
|
|
490
|
+
for ts in [
|
|
491
|
+
ts for ts_track in time_sigs_by_track.values() for ts in ts_track
|
|
492
|
+
]: # flattening all track time signatures to a list of ts
|
|
493
|
+
global_time_sigs.append(ts)
|
|
494
|
+
# now clear all track_ts
|
|
495
|
+
time_sigs_by_track.clear()
|
|
496
|
+
|
|
476
497
|
time_sigs_by_part = defaultdict(set)
|
|
477
498
|
for tr, ts_list in time_sigs_by_track.items():
|
|
478
499
|
for ts in ts_list:
|
partitura/io/importmusicxml.py
CHANGED
|
@@ -60,6 +60,22 @@ PEDAL_DIRECTIONS = {
|
|
|
60
60
|
"sustain_pedal": score.SustainPedalDirection,
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
TEMPO_DIRECTIONS = {
|
|
64
|
+
"Adagio": score.ConstantTempoDirection,
|
|
65
|
+
"Andante": score.ConstantTempoDirection,
|
|
66
|
+
"Andante amoroso": score.ConstantTempoDirection,
|
|
67
|
+
"Andante cantabile": score.ConstantTempoDirection,
|
|
68
|
+
"Andante grazioso": score.ConstantTempoDirection,
|
|
69
|
+
"Menuetto": score.ConstantTempoDirection,
|
|
70
|
+
"Allegretto grazioso": score.ConstantTempoDirection,
|
|
71
|
+
"Allegro moderato": score.ConstantTempoDirection,
|
|
72
|
+
"Allegro assai": score.ConstantTempoDirection,
|
|
73
|
+
"Allegro": score.ConstantTempoDirection,
|
|
74
|
+
"Allegretto": score.ConstantTempoDirection,
|
|
75
|
+
"Molto allegro": score.ConstantTempoDirection,
|
|
76
|
+
"Presto": score.ConstantTempoDirection,
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
OCTAVE_SHIFTS = {8: 1, 15: 2, 22: 3}
|
|
64
80
|
|
|
65
81
|
|
|
@@ -114,7 +130,6 @@ def _parse_partlist(partlist):
|
|
|
114
130
|
structure = []
|
|
115
131
|
current_group = None
|
|
116
132
|
part_dict = {}
|
|
117
|
-
|
|
118
133
|
for e in partlist:
|
|
119
134
|
if e.tag == "part-group":
|
|
120
135
|
if e.get("type") == "start":
|
|
@@ -146,7 +161,6 @@ def _parse_partlist(partlist):
|
|
|
146
161
|
part.part_abbreviation = next(
|
|
147
162
|
iter(e.xpath("part-abbreviation/text()")), None
|
|
148
163
|
)
|
|
149
|
-
|
|
150
164
|
part_dict[part_id] = part
|
|
151
165
|
|
|
152
166
|
if current_group is None:
|
|
@@ -239,6 +253,10 @@ def load_musicxml(
|
|
|
239
253
|
|
|
240
254
|
composer = None
|
|
241
255
|
scid = None
|
|
256
|
+
work_title = None
|
|
257
|
+
work_number = None
|
|
258
|
+
movement_title = None
|
|
259
|
+
movement_number = None
|
|
242
260
|
title = None
|
|
243
261
|
subtitle = None
|
|
244
262
|
lyricist = None
|
|
@@ -254,8 +272,20 @@ def load_musicxml(
|
|
|
254
272
|
tag="work-title",
|
|
255
273
|
as_type=str,
|
|
256
274
|
)
|
|
275
|
+
scidn = get_value_from_tag(
|
|
276
|
+
e=work_info_el,
|
|
277
|
+
tag="work-number",
|
|
278
|
+
as_type=str,
|
|
279
|
+
)
|
|
280
|
+
work_title = scid
|
|
281
|
+
work_number = scidn
|
|
257
282
|
|
|
258
|
-
|
|
283
|
+
movement_title_el = document.find(".//movement-title")
|
|
284
|
+
movement_number_el = document.find(".//movement-number")
|
|
285
|
+
if movement_title_el is not None:
|
|
286
|
+
movement_title = movement_title_el.text
|
|
287
|
+
if movement_number_el is not None:
|
|
288
|
+
movement_number = movement_number_el.text
|
|
259
289
|
|
|
260
290
|
score_identification_el = document.find("identification")
|
|
261
291
|
|
|
@@ -289,6 +319,10 @@ def load_musicxml(
|
|
|
289
319
|
scr = score.Score(
|
|
290
320
|
id=scid,
|
|
291
321
|
partlist=partlist,
|
|
322
|
+
work_number=work_number,
|
|
323
|
+
work_title=work_title,
|
|
324
|
+
movement_number=movement_number,
|
|
325
|
+
movement_title=movement_title,
|
|
292
326
|
title=title,
|
|
293
327
|
subtitle=subtitle,
|
|
294
328
|
composer=composer,
|
|
@@ -841,8 +875,9 @@ def _handle_direction(e, position, part, ongoing):
|
|
|
841
875
|
# first child of direction-type is dynamics, there may be subsequent
|
|
842
876
|
# dynamics items, so we loop:
|
|
843
877
|
for child in direction_type:
|
|
878
|
+
# check if child has no children, in which case continue
|
|
844
879
|
# interpret as score.Direction, fall back to score.Words
|
|
845
|
-
dyn_el = next(iter(child))
|
|
880
|
+
dyn_el = next(iter(child), None)
|
|
846
881
|
if dyn_el is not None:
|
|
847
882
|
direction = DYN_DIRECTIONS.get(dyn_el.tag, score.Words)(
|
|
848
883
|
dyn_el.tag, staff=staff
|
|
@@ -1137,7 +1172,7 @@ def _handle_sound(e, position, part):
|
|
|
1137
1172
|
if "tempo" in e.attrib:
|
|
1138
1173
|
tempo = score.Tempo(int(e.attrib["tempo"]), "q")
|
|
1139
1174
|
# part.add_starting_object(position, tempo)
|
|
1140
|
-
|
|
1175
|
+
(position, part, tempo)
|
|
1141
1176
|
|
|
1142
1177
|
|
|
1143
1178
|
def _handle_note(e, position, part, ongoing, prev_note, doc_order):
|
partitura/io/matchfile_utils.py
CHANGED
|
@@ -871,6 +871,35 @@ def format_time_signature_list(value: MatchTimeSignature) -> str:
|
|
|
871
871
|
return str(value)
|
|
872
872
|
|
|
873
873
|
|
|
874
|
+
class MatchTempoIndication(MatchParameter):
|
|
875
|
+
def __init__(
|
|
876
|
+
self,
|
|
877
|
+
value: str,
|
|
878
|
+
is_list: bool = False,
|
|
879
|
+
):
|
|
880
|
+
super().__init__()
|
|
881
|
+
self.value = self.from_string(value)[0]
|
|
882
|
+
self.is_list = is_list
|
|
883
|
+
|
|
884
|
+
def __str__(self):
|
|
885
|
+
return self.value
|
|
886
|
+
|
|
887
|
+
@classmethod
|
|
888
|
+
def from_string(cls, string: str) -> MatchTempoIndication:
|
|
889
|
+
content = interpret_as_list(string)
|
|
890
|
+
return content
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def interpret_as_tempo_indication(value: str) -> MatchTempoIndication:
|
|
894
|
+
tempo_indication = MatchTempoIndication.from_string(value)
|
|
895
|
+
return tempo_indication
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def format_tempo_indication(value: MatchTempoIndication) -> str:
|
|
899
|
+
value.is_list = False
|
|
900
|
+
return str(value)
|
|
901
|
+
|
|
902
|
+
|
|
874
903
|
## Miscellaneous utils
|
|
875
904
|
|
|
876
905
|
|
partitura/io/matchlines_v1.py
CHANGED
|
@@ -58,6 +58,9 @@ from partitura.io.matchfile_utils import (
|
|
|
58
58
|
format_key_signature_v1_0_0,
|
|
59
59
|
to_snake_case,
|
|
60
60
|
get_kwargs_from_matchline,
|
|
61
|
+
MatchTempoIndication,
|
|
62
|
+
interpret_as_tempo_indication,
|
|
63
|
+
format_tempo_indication,
|
|
61
64
|
)
|
|
62
65
|
|
|
63
66
|
# Define current version of the match file format
|
|
@@ -237,6 +240,11 @@ SCOREPROP_LINE = {
|
|
|
237
240
|
format_key_signature_v1_0_0,
|
|
238
241
|
MatchKeySignature,
|
|
239
242
|
),
|
|
243
|
+
"tempoIndication": (
|
|
244
|
+
interpret_as_tempo_indication,
|
|
245
|
+
format_tempo_indication,
|
|
246
|
+
MatchTempoIndication,
|
|
247
|
+
),
|
|
240
248
|
"beatSubDivision": (interpret_as_list_int, format_list, list),
|
|
241
249
|
"directions": (interpret_as_list, format_list, list),
|
|
242
250
|
}
|