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/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.document.find(self._ns_name("staffGrp", all=True))
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.document.findall(self._ns_name("score", all=True))
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. 12 is a eight note triplet) and a dot number by looking at the symbolic dur dictionary:
355
- i.e., symbol, eventual tuplet ancestors."""
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
- intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"]
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.document.xpath(".//*[@dur]")
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(symbolic_duration)
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(symbolic_duration)
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
- duration = divs * 4 / intsymdur
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
- space_id, duration, symbolic_duration = self._duration_info(e, part)
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(self, staff_el, staff_ind, position: int, part):
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(number=staff_el.getparent().get("n"))
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(staff_el, i_s + 1, position, part)
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
- last_measure = list(part.iter_all(pt.score.Measure))[-1]
967
- if last_measure.end.t != max_position:
968
- part.add(
969
- pt.score.Measure(number=last_measure.number),
970
- position,
971
- max_position,
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(element, parts, position)
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(element, parts, position)
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 rep_start["type"] == "start" and rep_stop["type"] == "stop"
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
 
@@ -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 %s" % msg)
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:
@@ -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
- title = scid
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
- _add_tempo_if_unique(position, part, tempo)
1175
+ (position, part, tempo)
1141
1176
 
1142
1177
 
1143
1178
  def _handle_note(e, position, part, ongoing, prev_note, doc_order):
@@ -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
 
@@ -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
  }